Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,8 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"resolve": "^1.20.0",
"uuid": "^13.0.0"
"uuid": "^13.0.0",
"ws": "^8.14.2"
},
"devDependencies": {
"@keystone-6/core": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';

export function SchemaRefreshListener() {
const router = useRouter();

useEffect(() => {
// Only connect in development mode
if (process.env.NODE_ENV !== 'development') return;

const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const socket = new WebSocket(`${protocol}//${window.location.host}/__keystone/schema-refresh`);

socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'SCHEMA_CHANGED') {
// Reload the page to reflect schema changes
router.reload();
}
};

return () => {
socket.close();
};
}, [router]);

return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { AppProps } from 'next/app'
import type { AdminConfig, FieldViews } from '../../../../types'
import { ErrorBoundary } from '../../../../admin-ui/components'
import { KeystoneProvider } from '../../../../admin-ui/context'
import { SchemaRefreshListener } from './SchemaRefreshListener'

type AppConfig = {
adminConfig: AdminConfig
Expand All @@ -14,6 +15,7 @@ export const getApp =
({ Component, pageProps }: AppProps) => {
return (
<KeystoneProvider {...props}>
<SchemaRefreshListener />
<ErrorBoundary>
<Component {...pageProps} />
</ErrorBoundary>
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/lib/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin
// @ts-expect-error
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js'
import type { KeystoneContext, KeystoneConfig } from '../types'
import { createSchemaRefreshServer, SchemaRefreshServer } from './schemaRefresh'

/*
NOTE: This creates the main Keystone express server, including the
Expand Down Expand Up @@ -57,9 +58,12 @@ export async function createExpressServer(
expressServer: express.Express
apolloServer: ApolloServer<KeystoneContext>
httpServer: Server
schemaRefreshServer: SchemaRefreshServer
}> {
const expressServer = express()
const httpServer = createServer(expressServer)
const schemaRefreshServer = createSchemaRefreshServer()
schemaRefreshServer.start(httpServer)

if (config.server.cors !== null) {
expressServer.use(cors(config.server.cors))
Expand Down Expand Up @@ -105,5 +109,5 @@ export async function createExpressServer(
})
)

return { expressServer, apolloServer, httpServer }
return { expressServer, apolloServer, httpServer, schemaRefreshServer }
}
52 changes: 52 additions & 0 deletions packages/core/src/lib/schemaRefresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import WebSocket from 'ws';
import { Server } from 'http';
import { KeystoneConfig } from '../types';
import { System } from '../lib/system';

export interface SchemaRefreshServer {
start(httpServer: Server): void;
stop(): void;
notifySchemaChange(): void;
}

export function createSchemaRefreshServer(): SchemaRefreshServer {
let wss: WebSocket.Server | null = null;

return {
start(httpServer: Server) {
wss = new WebSocket.Server({ noServer: true });

httpServer.on('upgrade', (request, socket, head) => {
if (request.url === '/__keystone/schema-refresh') {
wss!.handleUpgrade(request, socket, head, (ws) => {
wss!.emit('connection', ws, request);
});
}
});

wss.on('connection', (ws) => {
console.log('Schema refresh client connected');
ws.on('close', () => {
console.log('Schema refresh client disconnected');
});
});
},

stop() {
if (wss) {
wss.close();
wss = null;
}
},

notifySchemaChange() {
if (wss) {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ type: 'SCHEMA_CHANGED' }));
}
});
}
}
};
}
12 changes: 11 additions & 1 deletion packages/core/src/scripts/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '../artifacts'
import { printPrismaSchema } from '../lib/core/prisma-schema-printer'
import { createExpressServer } from '../lib/express'
import { SchemaRefreshServer } from '../lib/schemaRefresh'
import { createAdminUIMiddlewareWithNextApp } from '../lib/middleware'
import { withMigrate } from '../lib/migrations'
import { confirmPrompt } from '../lib/prompts'
Expand Down Expand Up @@ -142,6 +143,7 @@ export async function dev(
const httpServer = app ? createServer(app) : null
let expressServer: express.Express | null = null
let hasAddedAdminUIMiddleware = false
let schemaRefreshServer: SchemaRefreshServer | null = null
const isReady = () => !server || (expressServer !== null && hasAddedAdminUIMiddleware)

const initKeystone = async () => {
Expand Down Expand Up @@ -247,10 +249,11 @@ export async function dev(
}

log('✨ Creating server')
const { apolloServer, expressServer } = await createExpressServer(
const { apolloServer, expressServer, schemaRefreshServer: newSchemaRefreshServer } = await createExpressServer(
system.config,
keystone.context
)
schemaRefreshServer = newSchemaRefreshServer
log(`✅ GraphQL API ready`)

return {
Expand Down Expand Up @@ -363,6 +366,13 @@ export async function dev(
expressServer = servers.expressServer
const prevApolloServer = lastApolloServer
lastApolloServer = servers.apolloServer
// Update schema refresh server
if (schemaRefreshServer) {
schemaRefreshServer.stop()
}
schemaRefreshServer = servers.schemaRefreshServer
// Notify clients about schema change
schemaRefreshServer.notifySchemaChange()
await prevApolloServer.stop()
}
}
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.