Skip to content

Commit b84498b

Browse files
fix: correctly resolve hmr dep ids and fallback to url (#18840)
Co-authored-by: 翠 / green <[email protected]>
1 parent 1172d65 commit b84498b

File tree

6 files changed

+112
-32
lines changed

6 files changed

+112
-32
lines changed

packages/vite/src/node/plugins/importAnalysis.ts

+59-24
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
321321
url: string,
322322
pos: number,
323323
forceSkipImportAnalysis: boolean = false,
324-
): Promise<[string, string]> => {
324+
): Promise<[string, string | null]> => {
325325
url = stripBase(url, base)
326326

327327
let importerFile = importer
@@ -355,7 +355,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
355355
if (!resolved || resolved.meta?.['vite:alias']?.noResolved) {
356356
// in ssr, we should let node handle the missing modules
357357
if (ssr) {
358-
return [url, url]
358+
return [url, null]
359359
}
360360
// fix#9534, prevent the importerModuleNode being stopped from propagating updates
361361
importerModule.isSelfAccepting = false
@@ -396,32 +396,35 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
396396
url = injectQuery(url, versionMatch[1])
397397
}
398398
}
399+
}
399400

401+
try {
402+
// delay setting `isSelfAccepting` until the file is actually used (#7870)
403+
// We use an internal function to avoid resolving the url again
404+
const depModule = await moduleGraph._ensureEntryFromUrl(
405+
unwrapId(url),
406+
canSkipImportAnalysis(url) || forceSkipImportAnalysis,
407+
resolved,
408+
)
400409
// check if the dep has been hmr updated. If yes, we need to attach
401410
// its last updated timestamp to force the browser to fetch the most
402411
// up-to-date version of this module.
403-
try {
404-
// delay setting `isSelfAccepting` until the file is actually used (#7870)
405-
// We use an internal function to avoid resolving the url again
406-
const depModule = await moduleGraph._ensureEntryFromUrl(
407-
unwrapId(url),
408-
canSkipImportAnalysis(url) || forceSkipImportAnalysis,
409-
resolved,
410-
)
411-
if (depModule.lastHMRTimestamp > 0) {
412-
url = injectQuery(url, `t=${depModule.lastHMRTimestamp}`)
413-
}
414-
} catch (e: any) {
415-
// it's possible that the dep fails to resolve (non-existent import)
416-
// attach location to the missing import
417-
e.pos = pos
418-
throw e
412+
if (
413+
environment.config.consumer === 'client' &&
414+
depModule.lastHMRTimestamp > 0
415+
) {
416+
url = injectQuery(url, `t=${depModule.lastHMRTimestamp}`)
419417
}
420-
421-
// prepend base
422-
if (!ssr) url = joinUrlSegments(base, url)
418+
} catch (e: any) {
419+
// it's possible that the dep fails to resolve (non-existent import)
420+
// attach location to the missing import
421+
e.pos = pos
422+
throw e
423423
}
424424

425+
// prepend base
426+
if (!ssr) url = joinUrlSegments(base, url)
427+
425428
return [url, resolved.id]
426429
}
427430

@@ -547,7 +550,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
547550
}
548551

549552
// normalize
550-
const [url, resolvedId] = await normalizeUrl(specifier, start)
553+
let [url, resolvedId] = await normalizeUrl(specifier, start)
554+
resolvedId = resolvedId || url
551555

552556
// record as safe modules
553557
// safeModulesPath should not include the base prefix.
@@ -751,9 +755,40 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
751755
// normalize and rewrite accepted urls
752756
const normalizedAcceptedUrls = new Set<string>()
753757
for (const { url, start, end } of acceptedUrls) {
754-
const [normalized] = await moduleGraph.resolveUrl(toAbsoluteUrl(url))
758+
let [normalized, resolvedId] = await normalizeUrl(url, start).catch(
759+
() => [],
760+
)
761+
if (resolvedId) {
762+
const mod = moduleGraph.getModuleById(resolvedId)
763+
if (!mod) {
764+
this.error(
765+
`module was not found for ${JSON.stringify(resolvedId)}`,
766+
start,
767+
)
768+
return
769+
}
770+
normalized = mod.url
771+
} else {
772+
try {
773+
// this fallback is for backward compat and will be removed in Vite 7
774+
const [resolved] = await moduleGraph.resolveUrl(toAbsoluteUrl(url))
775+
normalized = resolved
776+
if (resolved) {
777+
this.warn({
778+
message:
779+
`Failed to resolve ${JSON.stringify(url)} from ${importer}.` +
780+
' An id should be written. Did you pass a URL?',
781+
pos: start,
782+
})
783+
}
784+
} catch {
785+
this.error(`Failed to resolve ${JSON.stringify(url)}`, start)
786+
return
787+
}
788+
}
755789
normalizedAcceptedUrls.add(normalized)
756-
str().overwrite(start, end, JSON.stringify(normalized), {
790+
const hmrAccept = normalizeHmrUrl(normalized)
791+
str().overwrite(start, end, JSON.stringify(hmrAccept), {
757792
contentOnly: true,
758793
})
759794
}

playground/hmr/__tests__/hmr.spec.ts

+23
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,29 @@ if (!isBuild) {
779779
}, '[wow]1')
780780
})
781781

782+
test('handle virtual module accept updates', async () => {
783+
await page.goto(viteTestUrl)
784+
const el = await page.$('.virtual-dep')
785+
expect(await el.textContent()).toBe('0')
786+
editFile('importedVirtual.js', (code) => code.replace('[success]', '[wow]'))
787+
await untilUpdated(async () => {
788+
const el = await page.$('.virtual-dep')
789+
return await el.textContent()
790+
}, '[wow]')
791+
})
792+
793+
test('invalidate virtual module and accept', async () => {
794+
await page.goto(viteTestUrl)
795+
const el = await page.$('.virtual-dep')
796+
expect(await el.textContent()).toBe('0')
797+
const btn = await page.$('.virtual-update-dep')
798+
btn.click()
799+
await untilUpdated(async () => {
800+
const el = await page.$('.virtual-dep')
801+
return await el.textContent()
802+
}, '[wow]2')
803+
})
804+
782805
test('keep hmr reload after missing import on server startup', async () => {
783806
const file = 'missing-import/a.js'
784807
const importCode = "import 'missing-modules'"

playground/hmr/hmr.ts

+17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { virtual } from 'virtual:file'
2+
import { virtual as virtualDep } from 'virtual:file-dep'
23
import { foo as depFoo, nestedFoo } from './hmrDep'
34
import './importing-updated'
45
import './file-delete-restore'
@@ -14,17 +15,29 @@ text('.app', foo)
1415
text('.dep', depFoo)
1516
text('.nested', nestedFoo)
1617
text('.virtual', virtual)
18+
text('.virtual-dep', virtualDep)
1719
text('.soft-invalidation', softInvalidationMsg)
1820
setImgSrc('#logo', logo)
1921
setImgSrc('#logo-no-inline', logoNoInline)
2022

23+
text('.virtual-dep', 0)
24+
2125
const btn = document.querySelector('.virtual-update') as HTMLButtonElement
2226
btn.onclick = () => {
2327
if (import.meta.hot) {
2428
import.meta.hot.send('virtual:increment')
2529
}
2630
}
2731

32+
const btnDep = document.querySelector(
33+
'.virtual-update-dep',
34+
) as HTMLButtonElement
35+
btnDep.onclick = () => {
36+
if (import.meta.hot) {
37+
import.meta.hot.send('virtual:increment', '-dep')
38+
}
39+
}
40+
2841
if (import.meta.hot) {
2942
import.meta.hot.accept(({ foo }) => {
3043
console.log('(self-accepting 1) foo is now:', foo)
@@ -55,6 +68,10 @@ if (import.meta.hot) {
5568
handleDep('single dep', foo, nestedFoo)
5669
})
5770

71+
import.meta.hot.accept('virtual:file-dep', ({ virtual }) => {
72+
text('.virtual-dep', virtual)
73+
})
74+
5875
import.meta.hot.accept(['./hmrDep'], ([{ foo, nestedFoo }]) => {
5976
handleDep('multi deps', foo, nestedFoo)
6077
})

playground/hmr/index.html

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
/>
77
</div>
88
<button class="virtual-update">update virtual module</button>
9+
<button class="virtual-update-dep">update virtual module via accept</button>
910

1011
<script type="module" src="./invalidation/root.js"></script>
1112
<script type="module" src="./hmr.ts"></script>
@@ -24,6 +25,7 @@
2425
<div class="custom"></div>
2526
<div class="toRemove"></div>
2627
<div class="virtual"></div>
28+
<div class="virtual-dep"></div>
2729
<div class="soft-invalidation"></div>
2830
<div class="invalidation-parent"></div>
2931
<div class="invalidation-root"></div>

playground/hmr/modules.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
declare module 'virtual:file' {
22
export const virtual: string
33
}
4+
5+
declare module 'virtual:file-dep' {
6+
export const virtual: string
7+
}

playground/hmr/vite.config.ts

+7-8
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,22 @@ function virtualPlugin(): Plugin {
4545
return {
4646
name: 'virtual-file',
4747
resolveId(id) {
48-
if (id === 'virtual:file') {
49-
return '\0virtual:file'
48+
if (id.startsWith('virtual:file')) {
49+
return '\0' + id
5050
}
5151
},
5252
load(id) {
53-
if (id === '\0virtual:file') {
53+
if (id.startsWith('\0virtual:file')) {
5454
return `\
5555
import { virtual as _virtual } from "/importedVirtual.js";
5656
export const virtual = _virtual + '${num}';`
5757
}
5858
},
5959
configureServer(server) {
60-
server.environments.client.hot.on('virtual:increment', async () => {
61-
const mod =
62-
await server.environments.client.moduleGraph.getModuleByUrl(
63-
'\0virtual:file',
64-
)
60+
server.environments.client.hot.on('virtual:increment', async (suffix) => {
61+
const mod = await server.environments.client.moduleGraph.getModuleById(
62+
'\0virtual:file' + (suffix || ''),
63+
)
6564
if (mod) {
6665
num++
6766
server.environments.client.reloadModule(mod)

0 commit comments

Comments
 (0)