mirror of
https://github.com/vitejs/vite.git
synced 2024-11-21 14:48:41 +00:00
fix(hmr): dedupe virtual modules in module graph (#10144)
This commit is contained in:
parent
c948e7d7fc
commit
71f08e766c
@ -15,9 +15,7 @@ import {
|
||||
CLIENT_DIR,
|
||||
CLIENT_PUBLIC_PATH,
|
||||
DEP_VERSION_RE,
|
||||
FS_PREFIX,
|
||||
NULL_BYTE_PLACEHOLDER,
|
||||
VALID_ID_PREFIX
|
||||
FS_PREFIX
|
||||
} from '../constants'
|
||||
import {
|
||||
debugHmr,
|
||||
@ -42,7 +40,8 @@ import {
|
||||
stripBomTag,
|
||||
timeFrom,
|
||||
transformStableResult,
|
||||
unwrapId
|
||||
unwrapId,
|
||||
wrapId
|
||||
} from '../utils'
|
||||
import type { ResolvedConfig } from '../config'
|
||||
import type { Plugin } from '../plugin'
|
||||
@ -330,8 +329,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
|
||||
// prefix it to make it valid. We will strip this before feeding it
|
||||
// back into the transform pipeline
|
||||
if (!url.startsWith('.') && !url.startsWith('/')) {
|
||||
url =
|
||||
VALID_ID_PREFIX + resolved.id.replace('\0', NULL_BYTE_PLACEHOLDER)
|
||||
url = wrapId(resolved.id)
|
||||
}
|
||||
|
||||
// make the URL browser-valid if not SSR
|
||||
@ -361,7 +359,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
|
||||
try {
|
||||
// delay setting `isSelfAccepting` until the file is actually used (#7870)
|
||||
const depModule = await moduleGraph.ensureEntryFromUrl(
|
||||
url,
|
||||
unwrapId(url),
|
||||
ssr,
|
||||
canSkipImportAnalysis(url)
|
||||
)
|
||||
@ -536,9 +534,9 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
|
||||
}
|
||||
|
||||
// record for HMR import chain analysis
|
||||
// make sure to normalize away base
|
||||
const urlWithoutBase = url.replace(base, '/')
|
||||
importedUrls.add(urlWithoutBase)
|
||||
// make sure to unwrap and normalize away base
|
||||
const hmrUrl = unwrapId(url.replace(base, '/'))
|
||||
importedUrls.add(hmrUrl)
|
||||
|
||||
if (enablePartialAccept && importedBindings) {
|
||||
extractImportedBindings(
|
||||
@ -551,7 +549,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
|
||||
|
||||
if (!isDynamicImport) {
|
||||
// for pre-transforming
|
||||
staticImportedUrls.add({ url: urlWithoutBase, id: resolvedId })
|
||||
staticImportedUrls.add({ url: hmrUrl, id: resolvedId })
|
||||
}
|
||||
} else if (!importer.startsWith(clientDir)) {
|
||||
if (!importer.includes('node_modules')) {
|
||||
@ -712,10 +710,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
|
||||
// by the deps optimizer
|
||||
if (config.server.preTransformRequests && staticImportedUrls.size) {
|
||||
staticImportedUrls.forEach(({ url, id }) => {
|
||||
url = unwrapId(removeImportQuery(url)).replace(
|
||||
NULL_BYTE_PLACEHOLDER,
|
||||
'\0'
|
||||
)
|
||||
url = removeImportQuery(url)
|
||||
transformRequest(url, server, { ssr }).catch((e) => {
|
||||
if (e?.code === ERR_OUTDATED_OPTIMIZED_DEP) {
|
||||
// This are expected errors
|
||||
|
@ -19,19 +19,15 @@ import {
|
||||
} from '../../plugins/html'
|
||||
import type { ResolvedConfig, ViteDevServer } from '../..'
|
||||
import { send } from '../send'
|
||||
import {
|
||||
CLIENT_PUBLIC_PATH,
|
||||
FS_PREFIX,
|
||||
NULL_BYTE_PLACEHOLDER,
|
||||
VALID_ID_PREFIX
|
||||
} from '../../constants'
|
||||
import { CLIENT_PUBLIC_PATH, FS_PREFIX } from '../../constants'
|
||||
import {
|
||||
cleanUrl,
|
||||
ensureWatchedFile,
|
||||
fsPathFromId,
|
||||
injectQuery,
|
||||
normalizePath,
|
||||
processSrcSetSync
|
||||
processSrcSetSync,
|
||||
wrapId
|
||||
} from '../../utils'
|
||||
import type { ModuleGraph } from '../moduleGraph'
|
||||
|
||||
@ -144,7 +140,7 @@ const devHtmlHook: IndexHtmlTransformHook = async (
|
||||
// and ids are properly handled
|
||||
const validPath = `${htmlPath}${trailingSlash ? 'index.html' : ''}`
|
||||
proxyModulePath = `\0${validPath}`
|
||||
proxyModuleUrl = `${VALID_ID_PREFIX}${NULL_BYTE_PLACEHOLDER}${validPath}`
|
||||
proxyModuleUrl = wrapId(proxyModulePath)
|
||||
}
|
||||
|
||||
const s = new MagicString(html)
|
||||
|
@ -12,7 +12,6 @@ import { transformRequest } from '../server/transformRequest'
|
||||
import type { InternalResolveOptions } from '../plugins/resolve'
|
||||
import { tryNodeResolve } from '../plugins/resolve'
|
||||
import { hookNodeResolve } from '../plugins/ssrRequireHook'
|
||||
import { NULL_BYTE_PLACEHOLDER } from '../constants'
|
||||
import {
|
||||
ssrDynamicImportKey,
|
||||
ssrExportAllKey,
|
||||
@ -38,7 +37,7 @@ export async function ssrLoadModule(
|
||||
urlStack: string[] = [],
|
||||
fixStacktrace?: boolean
|
||||
): Promise<SSRModule> {
|
||||
url = unwrapId(url).replace(NULL_BYTE_PLACEHOLDER, '\0')
|
||||
url = unwrapId(url)
|
||||
|
||||
// when we instantiate multiple dependency modules in parallel, they may
|
||||
// point to shared modules. We need to avoid duplicate instantiation attempts
|
||||
@ -138,7 +137,7 @@ async function instantiateModule(
|
||||
return nodeImport(dep, mod.file!, resolveOptions)
|
||||
}
|
||||
// convert to rollup URL because `pendingImports`, `moduleGraph.urlToModuleMap` requires that
|
||||
dep = unwrapId(dep).replace(NULL_BYTE_PLACEHOLDER, '\0')
|
||||
dep = unwrapId(dep)
|
||||
if (!isCircular(dep) && !pendingImports.get(dep)?.some(isCircular)) {
|
||||
pendingDeps.push(dep)
|
||||
if (pendingDeps.length === 1) {
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
DEFAULT_EXTENSIONS,
|
||||
ENV_PUBLIC_PATH,
|
||||
FS_PREFIX,
|
||||
NULL_BYTE_PLACEHOLDER,
|
||||
OPTIMIZABLE_ENTRY_RE,
|
||||
VALID_ID_PREFIX,
|
||||
loopbackHosts,
|
||||
@ -53,10 +54,24 @@ export function slash(p: string): string {
|
||||
return p.replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
// Strip valid id prefix. This is prepended to resolved Ids that are
|
||||
// not valid browser import specifiers by the importAnalysis plugin.
|
||||
/**
|
||||
* Prepend `/@id/` and replace null byte so the id is URL-safe.
|
||||
* This is prepended to resolved ids that are not valid browser
|
||||
* import specifiers by the importAnalysis plugin.
|
||||
*/
|
||||
export function wrapId(id: string): string {
|
||||
return id.startsWith(VALID_ID_PREFIX)
|
||||
? id
|
||||
: VALID_ID_PREFIX + id.replace('\0', NULL_BYTE_PLACEHOLDER)
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo {@link wrapId}'s `/@id/` and null byte replacements.
|
||||
*/
|
||||
export function unwrapId(id: string): string {
|
||||
return id.startsWith(VALID_ID_PREFIX) ? id.slice(VALID_ID_PREFIX.length) : id
|
||||
return id.startsWith(VALID_ID_PREFIX)
|
||||
? id.slice(VALID_ID_PREFIX.length).replace(NULL_BYTE_PLACEHOLDER, '\0')
|
||||
: id
|
||||
}
|
||||
|
||||
export const flattenId = (id: string): string =>
|
||||
|
@ -627,4 +627,15 @@ if (!isBuild) {
|
||||
btn = await page.$('button')
|
||||
expect(await btn.textContent()).toBe('Compteur 0')
|
||||
})
|
||||
|
||||
test('handle virtual module updates', async () => {
|
||||
await page.goto(viteTestUrl)
|
||||
const el = await page.$('.virtual')
|
||||
expect(await el.textContent()).toBe('[success]')
|
||||
editFile('importedVirtual.js', (code) => code.replace('[success]', '[wow]'))
|
||||
await untilUpdated(async () => {
|
||||
const el = await page.$('.virtual')
|
||||
return await el.textContent()
|
||||
}, '[wow]')
|
||||
})
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
// @ts-ignore
|
||||
import { virtual } from 'virtual:file'
|
||||
import { foo as depFoo, nestedFoo } from './hmrDep'
|
||||
import './importing-updated'
|
||||
|
||||
@ -5,6 +7,7 @@ export const foo = 1
|
||||
text('.app', foo)
|
||||
text('.dep', depFoo)
|
||||
text('.nested', nestedFoo)
|
||||
text('.virtual', virtual)
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(({ foo }) => {
|
||||
|
1
playground/hmr/importedVirtual.js
Normal file
1
playground/hmr/importedVirtual.js
Normal file
@ -0,0 +1 @@
|
||||
export const virtual = '[success]'
|
@ -19,6 +19,7 @@
|
||||
<div class="dep"></div>
|
||||
<div class="nested"></div>
|
||||
<div class="custom"></div>
|
||||
<div class="virtual"></div>
|
||||
<div class="custom-communication"></div>
|
||||
<div class="css-prev"></div>
|
||||
<div class="css-post"></div>
|
||||
|
@ -19,6 +19,19 @@ export default defineConfig({
|
||||
client.send('custom:remote-add-result', { result: a + b })
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'virtual-file',
|
||||
resolveId(id) {
|
||||
if (id === 'virtual:file') {
|
||||
return '\0virtual:file'
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if (id === '\0virtual:file') {
|
||||
return 'import { virtual } from "/importedVirtual.js"; export { virtual };'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
import fetch from 'node-fetch'
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { port } from './serve'
|
||||
import { page } from '~utils'
|
||||
import { editFile, isServe, page, untilUpdated } from '~utils'
|
||||
|
||||
const url = `http://localhost:${port}`
|
||||
|
||||
@ -39,3 +39,19 @@ describe('injected inline scripts', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe.runIf(isServe)('hmr', () => {
|
||||
test('handle virtual module updates', async () => {
|
||||
await page.goto(url)
|
||||
const el = await page.$('.virtual')
|
||||
expect(await el.textContent()).toBe('[success]')
|
||||
editFile('src/importedVirtual.js', (code) =>
|
||||
code.replace('[success]', '[wow]')
|
||||
)
|
||||
await page.waitForNavigation()
|
||||
await untilUpdated(async () => {
|
||||
const el = await page.$('.virtual')
|
||||
return await el.textContent()
|
||||
}, '[wow]')
|
||||
})
|
||||
})
|
||||
|
@ -12,5 +12,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>SSR Dynamic HTML</h1>
|
||||
<div class="virtual"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -48,7 +48,22 @@ export async function createServer(root = process.cwd(), hmrPort) {
|
||||
port: hmrPort
|
||||
}
|
||||
},
|
||||
appType: 'custom'
|
||||
appType: 'custom',
|
||||
plugins: [
|
||||
{
|
||||
name: 'virtual-file',
|
||||
resolveId(id) {
|
||||
if (id === 'virtual:file') {
|
||||
return '\0virtual:file'
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if (id === '\0virtual:file') {
|
||||
return 'import { virtual } from "/src/importedVirtual.js"; export { virtual };'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
// use vite's connect instance as middleware
|
||||
app.use(vite.middlewares)
|
||||
|
@ -1,3 +1,11 @@
|
||||
import { virtual } from 'virtual:file'
|
||||
|
||||
const p = document.createElement('p')
|
||||
p.innerHTML = '✅ Dynamically injected script from file'
|
||||
document.body.appendChild(p)
|
||||
|
||||
text('.virtual', virtual)
|
||||
|
||||
function text(el, text) {
|
||||
document.querySelector(el).textContent = text
|
||||
}
|
||||
|
1
playground/ssr-html/src/importedVirtual.js
Normal file
1
playground/ssr-html/src/importedVirtual.js
Normal file
@ -0,0 +1 @@
|
||||
export const virtual = '[success]'
|
Loading…
Reference in New Issue
Block a user