diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 245685b3674ef0..8ebe0ab1fc78e2 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -218,9 +218,18 @@ export function updateModules( export async function handleFileAddUnlink( file: string, server: ViteDevServer, + isUnlink: boolean, ): Promise { const modules = [...(server.moduleGraph.getModulesByFile(file) || [])] + if (isUnlink) { + for (const deletedMod of modules) { + deletedMod.importedModules.forEach((importedMod) => { + importedMod.importers.delete(deletedMod) + }) + } + } + modules.push(...getAffectedGlobModules(file, server)) if (modules.length > 0) { diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index e887ce2e6ba793..adca608e9ca0d4 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -577,9 +577,9 @@ export async function _createServer( } } - const onFileAddUnlink = async (file: string) => { + const onFileAddUnlink = async (file: string, isUnlink: boolean) => { file = normalizePath(file) - await handleFileAddUnlink(file, server) + await handleFileAddUnlink(file, server, isUnlink) await onHMRUpdate(file, true) } @@ -591,8 +591,8 @@ export async function _createServer( await onHMRUpdate(file, false) }) - watcher.on('add', onFileAddUnlink) - watcher.on('unlink', onFileAddUnlink) + watcher.on('add', (file) => onFileAddUnlink(file, false)) + watcher.on('unlink', (file) => onFileAddUnlink(file, true)) ws.on('vite:invalidate', async ({ path, message }: InvalidatePayload) => { const mod = moduleGraph.urlToModuleMap.get(path) diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts index 880f56c7f252f2..02371a8362dca1 100644 --- a/playground/hmr/__tests__/hmr.spec.ts +++ b/playground/hmr/__tests__/hmr.spec.ts @@ -793,6 +793,57 @@ if (import.meta.hot) { ) }) + test('delete file should not break hmr', async () => { + await page.goto(viteTestUrl) + + await untilUpdated( + () => page.textContent('.intermediate-file-delete-display'), + 'count is 1', + ) + + // add state + await page.click('.intermediate-file-delete-increment') + await untilUpdated( + () => page.textContent('.intermediate-file-delete-display'), + 'count is 2', + ) + + // update import, hmr works + editFile('intermediate-file-delete/index.js', (code) => + code.replace("from './re-export.js'", "from './display.js'"), + ) + editFile('intermediate-file-delete/display.js', (code) => + code.replace('count is ${count}', 'count is ${count}!'), + ) + await untilUpdated( + () => page.textContent('.intermediate-file-delete-display'), + 'count is 2!', + ) + + // remove unused file, page reload because it's considered entry point now + removeFile('intermediate-file-delete/re-export.js') + await untilUpdated( + () => page.textContent('.intermediate-file-delete-display'), + 'count is 1!', + ) + + // re-add state + await page.click('.intermediate-file-delete-increment') + await untilUpdated( + () => page.textContent('.intermediate-file-delete-display'), + 'count is 2!', + ) + + // hmr works after file deletion + editFile('intermediate-file-delete/display.js', (code) => + code.replace('count is ${count}!', 'count is ${count}'), + ) + await untilUpdated( + () => page.textContent('.intermediate-file-delete-display'), + 'count is 2', + ) + }) + test('import.meta.hot?.accept', async () => { const el = await page.$('.optional-chaining') await untilBrowserLogAfter( diff --git a/playground/hmr/hmr.ts b/playground/hmr/hmr.ts index 6267a588be3c9c..3fd552c1e598eb 100644 --- a/playground/hmr/hmr.ts +++ b/playground/hmr/hmr.ts @@ -4,6 +4,7 @@ import './importing-updated' import './invalidation/parent' import './file-delete-restore' import './optional-chaining/parent' +import './intermediate-file-delete' import logo from './logo.svg' export const foo = 1 diff --git a/playground/hmr/index.html b/playground/hmr/index.html index 53e3390ac59168..9fac186d584d71 100644 --- a/playground/hmr/index.html +++ b/playground/hmr/index.html @@ -33,4 +33,6 @@
+ +
diff --git a/playground/hmr/intermediate-file-delete/display.js b/playground/hmr/intermediate-file-delete/display.js new file mode 100644 index 00000000000000..3ab1936b0c9009 --- /dev/null +++ b/playground/hmr/intermediate-file-delete/display.js @@ -0,0 +1 @@ +export const displayCount = (count) => `count is ${count}` diff --git a/playground/hmr/intermediate-file-delete/index.js b/playground/hmr/intermediate-file-delete/index.js new file mode 100644 index 00000000000000..4137a300f2be32 --- /dev/null +++ b/playground/hmr/intermediate-file-delete/index.js @@ -0,0 +1,17 @@ +import { displayCount } from './re-export.js' + +const button = document.querySelector('.intermediate-file-delete-increment') + +const render = () => { + document.querySelector('.intermediate-file-delete-display').textContent = + displayCount(Number(button.textContent)) +} + +render() + +button.addEventListener('click', () => { + button.textContent = `${Number(button.textContent) + 1}` + render() +}) + +if (import.meta.hot) import.meta.hot.accept() diff --git a/playground/hmr/intermediate-file-delete/re-export.js b/playground/hmr/intermediate-file-delete/re-export.js new file mode 100644 index 00000000000000..b2dade525c0675 --- /dev/null +++ b/playground/hmr/intermediate-file-delete/re-export.js @@ -0,0 +1 @@ +export * from './display.js'