Something went wrong. Try again.
write Deno web servers with Vite
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187// 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<InitMessage>();
// buffer hot payloads until the module runner connectsconst pendingHotPayloads: HotPayload[] = [];let onHotPayload: ((payload: HotPayload) => void) | undefined;
const workerChannel = new BroadcastChannel(WORKER_CHANNEL);
workerChannel.onmessage = ({ data }: MessageEvent<WorkerChannelMessage>) => { 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<SerializedError> = 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 staleconst 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<Deno.ServeDefaultExport> => { // 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<EntryModule>(entry); return mod.default;};
process.on('disconnect', () => { Deno.exit(0);});
// start module-scope work immediately, including after restartsrunner.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' } }); } },);