// Deno child entrypoint: evaluates server modules through Vite and listens on a local port import process from 'node:process'; import { loadModuleRunner } from './module-runner.ts'; import { ERROR_HEADER, type HostMessage, type HotMessage, type InitMessage, type RunnerMessage, type SerializedError, WORKER_CHANNEL, type WorkerChannelMessage, } from './protocol.ts'; type HotPayload = HotMessage['payload']; const post = (message: RunnerMessage) => { process.send!(message); }; const init = Promise.withResolvers(); // buffer hot payloads until the module runner connects const pendingHotPayloads: HotPayload[] = []; let onHotPayload: ((payload: HotPayload) => void) | undefined; const workerChannel = new BroadcastChannel(WORKER_CHANNEL); workerChannel.onmessage = ({ data }: MessageEvent) => { if (data.type === 'invoke') { post(data); } }; process.on('message', (message: HostMessage) => { switch (message.type) { case 'init': { init.resolve(message); break; } case 'hot': { if (onHotPayload) { onHotPayload(message.payload); } else { pendingHotPayloads.push(message.payload); } break; } case 'result': { workerChannel.postMessage(message); break; } } }); const { entry, moduleRunner: moduleRunnerUrl } = await init.promise; const createRunner = await loadModuleRunner(moduleRunnerUrl); const serializeError = (error: unknown): SerializedError => { if (!(error instanceof Error)) { return { message: String(error), stack: '' }; } // Vite attaches these to transform errors const { frame, id, loc, plugin, pluginCode }: Partial = error; return { message: error.message, stack: error.stack ?? '', frame, id, loc, plugin, pluginCode }; }; // Vite retains SSR self-accepting boundaries when hot.accept() is removed or never reached. // updates without a matching runner callback leave importers stale const isUnacceptedUpdate = (payload: HotPayload): boolean => { return payload.type === 'update' && payload.updates.some((update) => { if (update.type !== 'js-update') { return false; } const callbacks = runner.hmrClient?.hotModulesMap.get(update.path)?.callbacks ?? []; return !callbacks.some(({ deps }) => deps.includes(update.acceptedPath)); }); }; const runner = createRunner({ transport: { connect({ onMessage }) { const receive = (payload: HotPayload) => { if (isUnacceptedUpdate(payload)) { post({ type: 'restart' }); return; } hotPayloadCount++; onMessage(payload); }; onHotPayload = receive; // defer until `runner` is initialized queueMicrotask(() => { for (const payload of pendingHotPayloads.splice(0)) { receive(payload); } }); }, send(payload) { post({ type: 'hot', payload }); }, }, hmr: { logger: { debug(...messages) { post({ type: 'log', level: 'info', message: messages.join(' ') }); }, error(error) { post({ type: 'log', level: 'error', message: error instanceof Error ? error.message : error }); }, }, }, }); type EntryModule = { default: Deno.ServeDefaultExport }; let hotPayloadCount = 0; let clearedAtPayload = 0; const loadEntry = async (): Promise => { // read the current exports without per-request IPC. runner.import() can re-evaluate a soft-invalidated // entry even when a self-accepting dependency should stop HMR propagation const node = runner.evaluatedModules.getModuleByUrl(entry); if (node?.promise) { try { const mod: EntryModule = await node.promise; return mod.default; } catch { // Vite's `cache` response for an unchanged entry preserves failed evaluations after dependency updates. // clear once per hot payload so persistent failures don't re-evaluate modules on every request if (clearedAtPayload !== hotPayloadCount) { clearedAtPayload = hotPayloadCount; runner.evaluatedModules.clear(); } } } const mod = await runner.import(entry); return mod.default; }; process.on('disconnect', () => { Deno.exit(0); }); // start module-scope work immediately, including after restarts runner.import(entry).catch((error) => { const { message } = serializeError(error); post({ type: 'log', level: 'error', message: `[deno-server] failed to evaluate entrypoint: ${message}`, }); }); Deno.serve( { hostname: '127.0.0.1', port: 0, onListen({ port }) { post({ type: 'ready', port }); }, }, async (request, info) => { try { const handler = await loadEntry(); return await handler.fetch(request, info); } catch (error) { // log locally because upgrades bypass Vite's error middleware if (request.headers.has('upgrade')) { console.error(error); return new Response(null, { status: 500 }); } return Response.json(serializeError(error), { status: 500, headers: { [ERROR_HEADER]: '1' } }); } }, );