Skip to content

Commit 71f08e7

Browse files
authoredSep 19, 2022
fix(hmr): dedupe virtual modules in module graph (#10144)
1 parent c948e7d commit 71f08e7

File tree

14 files changed

+106
-31
lines changed

14 files changed

+106
-31
lines changed
 

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

+10-15
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ import {
1515
CLIENT_DIR,
1616
CLIENT_PUBLIC_PATH,
1717
DEP_VERSION_RE,
18-
FS_PREFIX,
19-
NULL_BYTE_PLACEHOLDER,
20-
VALID_ID_PREFIX
18+
FS_PREFIX
2119
} from '../constants'
2220
import {
2321
debugHmr,
@@ -42,7 +40,8 @@ import {
4240
stripBomTag,
4341
timeFrom,
4442
transformStableResult,
45-
unwrapId
43+
unwrapId,
44+
wrapId
4645
} from '../utils'
4746
import type { ResolvedConfig } from '../config'
4847
import type { Plugin } from '../plugin'
@@ -330,8 +329,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
330329
// prefix it to make it valid. We will strip this before feeding it
331330
// back into the transform pipeline
332331
if (!url.startsWith('.') && !url.startsWith('/')) {
333-
url =
334-
VALID_ID_PREFIX + resolved.id.replace('\0', NULL_BYTE_PLACEHOLDER)
332+
url = wrapId(resolved.id)
335333
}
336334

337335
// make the URL browser-valid if not SSR
@@ -361,7 +359,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
361359
try {
362360
// delay setting `isSelfAccepting` until the file is actually used (#7870)
363361
const depModule = await moduleGraph.ensureEntryFromUrl(
364-
url,
362+
unwrapId(url),
365363
ssr,
366364
canSkipImportAnalysis(url)
367365
)
@@ -536,9 +534,9 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
536534
}
537535

538536
// record for HMR import chain analysis
539-
// make sure to normalize away base
540-
const urlWithoutBase = url.replace(base, '/')
541-
importedUrls.add(urlWithoutBase)
537+
// make sure to unwrap and normalize away base
538+
const hmrUrl = unwrapId(url.replace(base, '/'))
539+
importedUrls.add(hmrUrl)
542540

543541
if (enablePartialAccept && importedBindings) {
544542
extractImportedBindings(
@@ -551,7 +549,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
551549

552550
if (!isDynamicImport) {
553551
// for pre-transforming
554-
staticImportedUrls.add({ url: urlWithoutBase, id: resolvedId })
552+
staticImportedUrls.add({ url: hmrUrl, id: resolvedId })
555553
}
556554
} else if (!importer.startsWith(clientDir)) {
557555
if (!importer.includes('node_modules')) {
@@ -712,10 +710,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
712710
// by the deps optimizer
713711
if (config.server.preTransformRequests && staticImportedUrls.size) {
714712
staticImportedUrls.forEach(({ url, id }) => {
715-
url = unwrapId(removeImportQuery(url)).replace(
716-
NULL_BYTE_PLACEHOLDER,
717-
'\0'
718-
)
713+
url = removeImportQuery(url)
719714
transformRequest(url, server, { ssr }).catch((e) => {
720715
if (e?.code === ERR_OUTDATED_OPTIMIZED_DEP) {
721716
// This are expected errors

‎packages/vite/src/node/server/middlewares/indexHtml.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,15 @@ import {
1919
} from '../../plugins/html'
2020
import type { ResolvedConfig, ViteDevServer } from '../..'
2121
import { send } from '../send'
22-
import {
23-
CLIENT_PUBLIC_PATH,
24-
FS_PREFIX,
25-
NULL_BYTE_PLACEHOLDER,
26-
VALID_ID_PREFIX
27-
} from '../../constants'
22+
import { CLIENT_PUBLIC_PATH, FS_PREFIX } from '../../constants'
2823
import {
2924
cleanUrl,
3025
ensureWatchedFile,
3126
fsPathFromId,
3227
injectQuery,
3328
normalizePath,
34-
processSrcSetSync
29+
processSrcSetSync,
30+
wrapId
3531
} from '../../utils'
3632
import type { ModuleGraph } from '../moduleGraph'
3733

@@ -144,7 +140,7 @@ const devHtmlHook: IndexHtmlTransformHook = async (
144140
// and ids are properly handled
145141
const validPath = `${htmlPath}${trailingSlash ? 'index.html' : ''}`
146142
proxyModulePath = `\0${validPath}`
147-
proxyModuleUrl = `${VALID_ID_PREFIX}${NULL_BYTE_PLACEHOLDER}${validPath}`
143+
proxyModuleUrl = wrapId(proxyModulePath)
148144
}
149145

150146
const s = new MagicString(html)

‎packages/vite/src/node/ssr/ssrModuleLoader.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { transformRequest } from '../server/transformRequest'
1212
import type { InternalResolveOptions } from '../plugins/resolve'
1313
import { tryNodeResolve } from '../plugins/resolve'
1414
import { hookNodeResolve } from '../plugins/ssrRequireHook'
15-
import { NULL_BYTE_PLACEHOLDER } from '../constants'
1615
import {
1716
ssrDynamicImportKey,
1817
ssrExportAllKey,
@@ -38,7 +37,7 @@ export async function ssrLoadModule(
3837
urlStack: string[] = [],
3938
fixStacktrace?: boolean
4039
): Promise<SSRModule> {
41-
url = unwrapId(url).replace(NULL_BYTE_PLACEHOLDER, '\0')
40+
url = unwrapId(url)
4241

4342
// when we instantiate multiple dependency modules in parallel, they may
4443
// point to shared modules. We need to avoid duplicate instantiation attempts
@@ -138,7 +137,7 @@ async function instantiateModule(
138137
return nodeImport(dep, mod.file!, resolveOptions)
139138
}
140139
// convert to rollup URL because `pendingImports`, `moduleGraph.urlToModuleMap` requires that
141-
dep = unwrapId(dep).replace(NULL_BYTE_PLACEHOLDER, '\0')
140+
dep = unwrapId(dep)
142141
if (!isCircular(dep) && !pendingImports.get(dep)?.some(isCircular)) {
143142
pendingDeps.push(dep)
144143
if (pendingDeps.length === 1) {

‎packages/vite/src/node/utils.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
DEFAULT_EXTENSIONS,
2626
ENV_PUBLIC_PATH,
2727
FS_PREFIX,
28+
NULL_BYTE_PLACEHOLDER,
2829
OPTIMIZABLE_ENTRY_RE,
2930
VALID_ID_PREFIX,
3031
loopbackHosts,
@@ -53,10 +54,24 @@ export function slash(p: string): string {
5354
return p.replace(/\\/g, '/')
5455
}
5556

56-
// Strip valid id prefix. This is prepended to resolved Ids that are
57-
// not valid browser import specifiers by the importAnalysis plugin.
57+
/**
58+
* Prepend `/@id/` and replace null byte so the id is URL-safe.
59+
* This is prepended to resolved ids that are not valid browser
60+
* import specifiers by the importAnalysis plugin.
61+
*/
62+
export function wrapId(id: string): string {
63+
return id.startsWith(VALID_ID_PREFIX)
64+
? id
65+
: VALID_ID_PREFIX + id.replace('\0', NULL_BYTE_PLACEHOLDER)
66+
}
67+
68+
/**
69+
* Undo {@link wrapId}'s `/@id/` and null byte replacements.
70+
*/
5871
export function unwrapId(id: string): string {
59-
return id.startsWith(VALID_ID_PREFIX) ? id.slice(VALID_ID_PREFIX.length) : id
72+
return id.startsWith(VALID_ID_PREFIX)
73+
? id.slice(VALID_ID_PREFIX.length).replace(NULL_BYTE_PLACEHOLDER, '\0')
74+
: id
6075
}
6176

6277
export const flattenId = (id: string): string =>

‎playground/hmr/__tests__/hmr.spec.ts

+11
Original file line numberDiff line numberDiff line change
@@ -627,4 +627,15 @@ if (!isBuild) {
627627
btn = await page.$('button')
628628
expect(await btn.textContent()).toBe('Compteur 0')
629629
})
630+
631+
test('handle virtual module updates', async () => {
632+
await page.goto(viteTestUrl)
633+
const el = await page.$('.virtual')
634+
expect(await el.textContent()).toBe('[success]')
635+
editFile('importedVirtual.js', (code) => code.replace('[success]', '[wow]'))
636+
await untilUpdated(async () => {
637+
const el = await page.$('.virtual')
638+
return await el.textContent()
639+
}, '[wow]')
640+
})
630641
}

‎playground/hmr/hmr.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
// @ts-ignore
2+
import { virtual } from 'virtual:file'
13
import { foo as depFoo, nestedFoo } from './hmrDep'
24
import './importing-updated'
35

46
export const foo = 1
57
text('.app', foo)
68
text('.dep', depFoo)
79
text('.nested', nestedFoo)
10+
text('.virtual', virtual)
811

912
if (import.meta.hot) {
1013
import.meta.hot.accept(({ foo }) => {

‎playground/hmr/importedVirtual.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const virtual = '[success]'

‎playground/hmr/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<div class="dep"></div>
2020
<div class="nested"></div>
2121
<div class="custom"></div>
22+
<div class="virtual"></div>
2223
<div class="custom-communication"></div>
2324
<div class="css-prev"></div>
2425
<div class="css-post"></div>

‎playground/hmr/vite.config.ts

+13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ export default defineConfig({
1919
client.send('custom:remote-add-result', { result: a + b })
2020
})
2121
}
22+
},
23+
{
24+
name: 'virtual-file',
25+
resolveId(id) {
26+
if (id === 'virtual:file') {
27+
return '\0virtual:file'
28+
}
29+
},
30+
load(id) {
31+
if (id === '\0virtual:file') {
32+
return 'import { virtual } from "/importedVirtual.js"; export { virtual };'
33+
}
34+
}
2235
}
2336
]
2437
})

‎playground/ssr-html/__tests__/ssr-html.spec.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fetch from 'node-fetch'
22
import { describe, expect, test } from 'vitest'
33
import { port } from './serve'
4-
import { page } from '~utils'
4+
import { editFile, isServe, page, untilUpdated } from '~utils'
55

66
const url = `http://localhost:${port}`
77

@@ -39,3 +39,19 @@ describe('injected inline scripts', () => {
3939
}
4040
})
4141
})
42+
43+
describe.runIf(isServe)('hmr', () => {
44+
test('handle virtual module updates', async () => {
45+
await page.goto(url)
46+
const el = await page.$('.virtual')
47+
expect(await el.textContent()).toBe('[success]')
48+
editFile('src/importedVirtual.js', (code) =>
49+
code.replace('[success]', '[wow]')
50+
)
51+
await page.waitForNavigation()
52+
await untilUpdated(async () => {
53+
const el = await page.$('.virtual')
54+
return await el.textContent()
55+
}, '[wow]')
56+
})
57+
})

‎playground/ssr-html/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
</head>
1313
<body>
1414
<h1>SSR Dynamic HTML</h1>
15+
<div class="virtual"></div>
1516
</body>
1617
</html>

‎playground/ssr-html/server.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,22 @@ export async function createServer(root = process.cwd(), hmrPort) {
4848
port: hmrPort
4949
}
5050
},
51-
appType: 'custom'
51+
appType: 'custom',
52+
plugins: [
53+
{
54+
name: 'virtual-file',
55+
resolveId(id) {
56+
if (id === 'virtual:file') {
57+
return '\0virtual:file'
58+
}
59+
},
60+
load(id) {
61+
if (id === '\0virtual:file') {
62+
return 'import { virtual } from "/src/importedVirtual.js"; export { virtual };'
63+
}
64+
}
65+
}
66+
]
5267
})
5368
// use vite's connect instance as middleware
5469
app.use(vite.middlewares)

‎playground/ssr-html/src/app.js

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
import { virtual } from 'virtual:file'
2+
13
const p = document.createElement('p')
24
p.innerHTML = '✅ Dynamically injected script from file'
35
document.body.appendChild(p)
6+
7+
text('.virtual', virtual)
8+
9+
function text(el, text) {
10+
document.querySelector(el).textContent = text
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const virtual = '[success]'

0 commit comments

Comments
 (0)
Please sign in to comment.