diff --git a/.changeset/red-masks-drop.md b/.changeset/red-masks-drop.md new file mode 100644 index 000000000..3564fa9e8 --- /dev/null +++ b/.changeset/red-masks-drop.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix `tsconfig.json` update causing the server to crash diff --git a/packages/astro/src/content/vite-plugin-content-imports.ts b/packages/astro/src/content/vite-plugin-content-imports.ts index 4643e0922..15ca6d956 100644 --- a/packages/astro/src/content/vite-plugin-content-imports.ts +++ b/packages/astro/src/content/vite-plugin-content-imports.ts @@ -148,9 +148,15 @@ export const _internal = { hasContentFlag(modUrl, DATA_FLAG) || Boolean(getContentRendererByViteId(modUrl, settings)) ) { - const mod = await viteServer.moduleGraph.getModuleByUrl(modUrl); - if (mod) { - viteServer.moduleGraph.invalidateModule(mod); + try { + const mod = await viteServer.moduleGraph.getModuleByUrl(modUrl); + if (mod) { + viteServer.moduleGraph.invalidateModule(mod); + } + } catch (e: any) { + // The server may be closed due to a restart caused by this file change + if (e.code === 'ERR_CLOSED_SERVER') break; + throw e; } } } diff --git a/packages/astro/src/core/module-loader/vite.ts b/packages/astro/src/core/module-loader/vite.ts index f58b2e720..48ec230f0 100644 --- a/packages/astro/src/core/module-loader/vite.ts +++ b/packages/astro/src/core/module-loader/vite.ts @@ -1,19 +1,52 @@ import { EventEmitter } from 'node:events'; +import path from 'node:path'; import type * as vite from 'vite'; import type { ModuleLoader, ModuleLoaderEventEmitter } from './loader.js'; export function createViteLoader(viteServer: vite.ViteDevServer): ModuleLoader { const events = new EventEmitter() as ModuleLoaderEventEmitter; - viteServer.watcher.on('add', (...args) => events.emit('file-add', args)); - viteServer.watcher.on('unlink', (...args) => events.emit('file-unlink', args)); - viteServer.watcher.on('change', (...args) => events.emit('file-change', args)); + let isTsconfigUpdated = false; + function isTsconfigUpdate(filePath: string) { + const result = path.basename(filePath) === 'tsconfig.json'; + if (result) isTsconfigUpdated = true; + return result; + } - wrapMethod(viteServer.ws, 'send', (msg) => { + // Skip event emit on tsconfig change as Vite restarts the server, and we don't + // want to trigger unnecessary work that will be invalidated shortly. + viteServer.watcher.on('add', (...args) => { + if (!isTsconfigUpdate(args[0])) { + events.emit('file-add', args); + } + }); + viteServer.watcher.on('unlink', (...args) => { + if (!isTsconfigUpdate(args[0])) { + events.emit('file-unlink', args); + } + }); + viteServer.watcher.on('change', (...args) => { + if (!isTsconfigUpdate(args[0])) { + events.emit('file-change', args); + } + }); + + const _wsSend = viteServer.ws.send; + viteServer.ws.send = function (...args: any) { + // If the tsconfig changed, Vite will trigger a reload as it invalidates the module. + // However in Astro, the whole server is restarted when the tsconfig changes. If we + // do a restart and reload at the same time, the browser will refetch and the server + // is not ready yet, causing a blank page. Here we block that reload from happening. + if (isTsconfigUpdated) { + isTsconfigUpdated = false; + return; + } + const msg = args[0] as vite.HMRPayload; if (msg?.type === 'error') { events.emit('hmr-error', msg); } - }); + _wsSend.apply(this, args); + }; return { import(src) { @@ -56,11 +89,3 @@ export function createViteLoader(viteServer: vite.ViteDevServer): ModuleLoader { events, }; } - -function wrapMethod(object: any, method: string, newFn: (...args: any[]) => void) { - const orig = object[method]; - object[method] = function (...args: any[]) { - newFn.apply(this, args); - return orig.apply(this, args); - }; -}