From d0b571b3e491e74d8e2a7066cf74c556d6e66db6 Mon Sep 17 00:00:00 2001 From: patrickpircher Date: Fri, 29 Nov 2024 17:56:51 +0100 Subject: [PATCH 1/8] fix hot accept virtual module --- .../vite/src/node/plugins/importAnalysis.ts | 15 ++++++++++-- playground/hmr/__tests__/hmr.spec.ts | 23 +++++++++++++++++++ playground/hmr/hmr.ts | 17 ++++++++++++++ playground/hmr/index.html | 2 ++ playground/hmr/modules.d.ts | 4 ++++ playground/hmr/vite.config.ts | 15 ++++++------ 6 files changed, 66 insertions(+), 10 deletions(-) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index f13d920fb83647..ab404c285b59f2 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -753,9 +753,20 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // normalize and rewrite accepted urls const normalizedAcceptedUrls = new Set() for (const { url, start, end } of acceptedUrls) { - const [normalized] = await moduleGraph.resolveUrl(toAbsoluteUrl(url)) + let normalized + if (url.startsWith('.')) { + const [resolved] = await moduleGraph.resolveUrl(toAbsoluteUrl(url)) + normalized = resolved + } else { + const resolved = await this.resolve( + url, + importerModule.id || undefined, + ) + normalized = resolved?.id || url + } normalizedAcceptedUrls.add(normalized) - str().overwrite(start, end, JSON.stringify(normalized), { + const hmrAccept = normalizeHmrUrl(normalized) + str().overwrite(start, end, JSON.stringify(hmrAccept), { contentOnly: true, }) } diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts index f474f16801afc3..29acfee7aa7df6 100644 --- a/playground/hmr/__tests__/hmr.spec.ts +++ b/playground/hmr/__tests__/hmr.spec.ts @@ -779,6 +779,29 @@ if (!isBuild) { }, '[wow]1') }) + test('handle virtual module accept updates', async () => { + await page.goto(viteTestUrl) + const el = await page.$('.virtual-dep') + expect(await el.textContent()).toBe('0') + editFile('importedVirtual.js', (code) => code.replace('[success]', '[wow]')) + await untilUpdated(async () => { + const el = await page.$('.virtual-dep') + return await el.textContent() + }, '[wow]') + }) + + test('invalidate virtual module and accept', async () => { + await page.goto(viteTestUrl) + const el = await page.$('.virtual-dep') + expect(await el.textContent()).toBe('0') + const btn = await page.$('.virtual-update-dep') + btn.click() + await untilUpdated(async () => { + const el = await page.$('.virtual-dep') + return await el.textContent() + }, '[wow]2') + }) + test('keep hmr reload after missing import on server startup', async () => { const file = 'missing-import/a.js' const importCode = "import 'missing-modules'" diff --git a/playground/hmr/hmr.ts b/playground/hmr/hmr.ts index 39e75317f811cf..3e459566ad151a 100644 --- a/playground/hmr/hmr.ts +++ b/playground/hmr/hmr.ts @@ -1,4 +1,5 @@ import { virtual } from 'virtual:file' +import { virtual as virtualDep } from 'virtual:file-dep' import { foo as depFoo, nestedFoo } from './hmrDep' import './importing-updated' import './file-delete-restore' @@ -14,10 +15,13 @@ text('.app', foo) text('.dep', depFoo) text('.nested', nestedFoo) text('.virtual', virtual) +text('.virtual-dep', virtualDep) text('.soft-invalidation', softInvalidationMsg) setImgSrc('#logo', logo) setImgSrc('#logo-no-inline', logoNoInline) +text('.virtual-dep', 0) + const btn = document.querySelector('.virtual-update') as HTMLButtonElement btn.onclick = () => { if (import.meta.hot) { @@ -25,6 +29,15 @@ btn.onclick = () => { } } +const btnDep = document.querySelector( + '.virtual-update-dep', +) as HTMLButtonElement +btnDep.onclick = () => { + if (import.meta.hot) { + import.meta.hot.send('virtual:increment', '-dep') + } +} + if (import.meta.hot) { import.meta.hot.accept(({ foo }) => { console.log('(self-accepting 1) foo is now:', foo) @@ -55,6 +68,10 @@ if (import.meta.hot) { handleDep('single dep', foo, nestedFoo) }) + import.meta.hot.accept('virtual:file-dep', ({ virtual }) => { + text('.virtual-dep', virtual) + }) + import.meta.hot.accept(['./hmrDep'], ([{ foo, nestedFoo }]) => { handleDep('multi deps', foo, nestedFoo) }) diff --git a/playground/hmr/index.html b/playground/hmr/index.html index 9c057916dd528d..9132a019009e7b 100644 --- a/playground/hmr/index.html +++ b/playground/hmr/index.html @@ -6,6 +6,7 @@ /> + @@ -24,6 +25,7 @@
+
diff --git a/playground/hmr/modules.d.ts b/playground/hmr/modules.d.ts index 122559a692ef20..e880082076b638 100644 --- a/playground/hmr/modules.d.ts +++ b/playground/hmr/modules.d.ts @@ -1,3 +1,7 @@ declare module 'virtual:file' { export const virtual: string } + +declare module 'virtual:file-dep' { + export const virtual: string +} diff --git a/playground/hmr/vite.config.ts b/playground/hmr/vite.config.ts index 94b07092b58f15..9ee8024ee2bf44 100644 --- a/playground/hmr/vite.config.ts +++ b/playground/hmr/vite.config.ts @@ -45,23 +45,22 @@ function virtualPlugin(): Plugin { return { name: 'virtual-file', resolveId(id) { - if (id === 'virtual:file') { - return '\0virtual:file' + if (id.startsWith('virtual:file')) { + return '\0' + id } }, load(id) { - if (id === '\0virtual:file') { + if (id.startsWith('\0virtual:file')) { return `\ import { virtual as _virtual } from "/importedVirtual.js"; export const virtual = _virtual + '${num}';` } }, configureServer(server) { - server.environments.client.hot.on('virtual:increment', async () => { - const mod = - await server.environments.client.moduleGraph.getModuleByUrl( - '\0virtual:file', - ) + server.environments.client.hot.on('virtual:increment', async (suffix) => { + const mod = await server.environments.client.moduleGraph.getModuleById( + '\0virtual:file' + (suffix || ''), + ) if (mod) { num++ server.environments.client.reloadModule(mod) From 0ba0ae59cf3133c61b003c86f06680439e497d9c Mon Sep 17 00:00:00 2001 From: patrickpircher Date: Fri, 6 Dec 2024 10:08:45 +0100 Subject: [PATCH 2/8] always use resolve id and fallback to url --- packages/vite/src/node/plugins/importAnalysis.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index ab404c285b59f2..347b0a4caddb2b 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -754,15 +754,14 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const normalizedAcceptedUrls = new Set() for (const { url, start, end } of acceptedUrls) { let normalized - if (url.startsWith('.')) { + const resolved = await this.resolve(url, importerModule.id || undefined) + if (resolved?.id) { + const mod = moduleGraph.getModuleById(resolved.id) + normalized = mod?.url + } + if (!normalized) { const [resolved] = await moduleGraph.resolveUrl(toAbsoluteUrl(url)) normalized = resolved - } else { - const resolved = await this.resolve( - url, - importerModule.id || undefined, - ) - normalized = resolved?.id || url } normalizedAcceptedUrls.add(normalized) const hmrAccept = normalizeHmrUrl(normalized) From cc1bf21ddd89c3d52fa1d2318c6db5a971f9b051 Mon Sep 17 00:00:00 2001 From: Patrick Pircher Date: Mon, 9 Dec 2024 08:52:43 +0100 Subject: [PATCH 3/8] use importer directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 翠 / green --- packages/vite/src/node/plugins/importAnalysis.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 347b0a4caddb2b..3c2bd98973c0af 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -754,7 +754,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const normalizedAcceptedUrls = new Set() for (const { url, start, end } of acceptedUrls) { let normalized - const resolved = await this.resolve(url, importerModule.id || undefined) + const resolved = await this.resolve(url, importer) if (resolved?.id) { const mod = moduleGraph.getModuleById(resolved.id) normalized = mod?.url From be05e5bc02753e8c1cd59ec1991c2110c2f34a27 Mon Sep 17 00:00:00 2001 From: Patrick Pircher Date: Wed, 11 Dec 2024 10:48:28 +0100 Subject: [PATCH 4/8] no fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 翠 / green --- .../vite/src/node/plugins/importAnalysis.ts | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 3c2bd98973c0af..92f168dd43c5f0 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -757,11 +757,29 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const resolved = await this.resolve(url, importer) if (resolved?.id) { const mod = moduleGraph.getModuleById(resolved.id) - normalized = mod?.url - } - if (!normalized) { - const [resolved] = await moduleGraph.resolveUrl(toAbsoluteUrl(url)) - normalized = resolved + if (!mod) { + this.error( + `module was not found for ${JSON.stringify(resolved.id)}`, + start, + ) + return + } + normalized = mod.url + } else { + try { + const [resolved] = await moduleGraph.resolveUrl(toAbsoluteUrl(url)) + if (resolved) { + this.warn({ + message: + `Failed to resolve ${JSON.stringify(url)} from ${importer}.` + + ' An id should be written. Did you pass a URL?', + pos: start, + }) + continue + } + } catch {} + this.error(`Failed to resolve ${JSON.stringify(url)}`, start) + return } normalizedAcceptedUrls.add(normalized) const hmrAccept = normalizeHmrUrl(normalized) From 12d66f1344fb3888a5547e172249b984112512c0 Mon Sep 17 00:00:00 2001 From: Patrick Pircher Date: Wed, 11 Dec 2024 13:02:42 +0100 Subject: [PATCH 5/8] use normalizeUrl --- .../vite/src/node/plugins/importAnalysis.ts | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 92f168dd43c5f0..481eb4af66fd5f 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -398,32 +398,32 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { url = injectQuery(url, versionMatch[1]) } } + } - // check if the dep has been hmr updated. If yes, we need to attach - // its last updated timestamp to force the browser to fetch the most - // up-to-date version of this module. - try { - // delay setting `isSelfAccepting` until the file is actually used (#7870) - // We use an internal function to avoid resolving the url again - const depModule = await moduleGraph._ensureEntryFromUrl( - unwrapId(url), - canSkipImportAnalysis(url) || forceSkipImportAnalysis, - resolved, - ) - if (depModule.lastHMRTimestamp > 0) { - url = injectQuery(url, `t=${depModule.lastHMRTimestamp}`) - } - } catch (e: any) { - // it's possible that the dep fails to resolve (non-existent import) - // attach location to the missing import - e.pos = pos - throw e + // check if the dep has been hmr updated. If yes, we need to attach + // its last updated timestamp to force the browser to fetch the most + // up-to-date version of this module. + try { + // delay setting `isSelfAccepting` until the file is actually used (#7870) + // We use an internal function to avoid resolving the url again + const depModule = await moduleGraph._ensureEntryFromUrl( + unwrapId(url), + canSkipImportAnalysis(url) || forceSkipImportAnalysis, + resolved, + ) + if (depModule.lastHMRTimestamp > 0) { + url = injectQuery(url, `t=${depModule.lastHMRTimestamp}`) } - - // prepend base - if (!ssr) url = joinUrlSegments(base, url) + } catch (e: any) { + // it's possible that the dep fails to resolve (non-existent import) + // attach location to the missing import + e.pos = pos + throw e } + // prepend base + if (!ssr) url = joinUrlSegments(base, url) + return [url, resolved.id] } @@ -753,13 +753,12 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // normalize and rewrite accepted urls const normalizedAcceptedUrls = new Set() for (const { url, start, end } of acceptedUrls) { - let normalized - const resolved = await this.resolve(url, importer) - if (resolved?.id) { - const mod = moduleGraph.getModuleById(resolved.id) + let [normalized, resolvedId] = await normalizeUrl(url, start) + if (resolvedId) { + const mod = moduleGraph.getModuleById(resolvedId) if (!mod) { this.error( - `module was not found for ${JSON.stringify(resolved.id)}`, + `module was not found for ${JSON.stringify(resolvedId)}`, start, ) return From b81e140eaee87c4c6a3aa72a7d1fc768db5c2f24 Mon Sep 17 00:00:00 2001 From: Patrick Pircher Date: Thu, 12 Dec 2024 06:40:17 +0100 Subject: [PATCH 6/8] query param is for browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 翠 / green --- packages/vite/src/node/plugins/importAnalysis.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 481eb4af66fd5f..7b9c7b2f8b1f3f 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -400,9 +400,6 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { } } - // check if the dep has been hmr updated. If yes, we need to attach - // its last updated timestamp to force the browser to fetch the most - // up-to-date version of this module. try { // delay setting `isSelfAccepting` until the file is actually used (#7870) // We use an internal function to avoid resolving the url again @@ -411,7 +408,10 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { canSkipImportAnalysis(url) || forceSkipImportAnalysis, resolved, ) - if (depModule.lastHMRTimestamp > 0) { + // check if the dep has been hmr updated. If yes, we need to attach + // its last updated timestamp to force the browser to fetch the most + // up-to-date version of this module. + if (environment.config.consumer === 'client' && depModule.lastHMRTimestamp > 0) { url = injectQuery(url, `t=${depModule.lastHMRTimestamp}`) } } catch (e: any) { From ff09f8be3a6711afc71345e447caf356b80ea772 Mon Sep 17 00:00:00 2001 From: Patrick Pircher Date: Thu, 12 Dec 2024 06:44:24 +0100 Subject: [PATCH 7/8] fix lint --- packages/vite/src/node/plugins/importAnalysis.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 7b9c7b2f8b1f3f..05ce16f6d94bd6 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -411,7 +411,10 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // check if the dep has been hmr updated. If yes, we need to attach // its last updated timestamp to force the browser to fetch the most // up-to-date version of this module. - if (environment.config.consumer === 'client' && depModule.lastHMRTimestamp > 0) { + if ( + environment.config.consumer === 'client' && + depModule.lastHMRTimestamp > 0 + ) { url = injectQuery(url, `t=${depModule.lastHMRTimestamp}`) } } catch (e: any) { From d5d7b9480e608a22ac286b81cd82608ef58eab3e Mon Sep 17 00:00:00 2001 From: Patrick Pircher Date: Tue, 17 Dec 2024 14:05:24 +0100 Subject: [PATCH 8/8] allow fallback --- packages/vite/src/node/plugins/importAnalysis.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 05ce16f6d94bd6..0fc5041f920a17 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -769,7 +769,9 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { normalized = mod.url } else { try { + // this fallback is for backward compat and will be removed in Vite 7 const [resolved] = await moduleGraph.resolveUrl(toAbsoluteUrl(url)) + normalized = resolved if (resolved) { this.warn({ message: @@ -777,11 +779,11 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { ' An id should be written. Did you pass a URL?', pos: start, }) - continue } - } catch {} - this.error(`Failed to resolve ${JSON.stringify(url)}`, start) - return + } catch { + this.error(`Failed to resolve ${JSON.stringify(url)}`, start) + return + } } normalizedAcceptedUrls.add(normalized) const hmrAccept = normalizeHmrUrl(normalized)