Skip to content

Commit 1e2b8af

Browse files
committed
fix(optimize-deps): ensure consistent browserHash between in-memory and persisted metadata
When the dev server restarts mid–asset waterfall and loads cached `_metadata.json`, inconsistent `browserHash` values can cause duplicate instances of the same dependency to be loaded (e.g. multiple React copies). This patch sets the `browserHash` eagerly in memory before resolving `scanProcessing`, ensuring both the in-memory metadata and the persisted metadata use the same value.
1 parent 3a92bc7 commit 1e2b8af

File tree

5 files changed

+99
-28
lines changed

5 files changed

+99
-28
lines changed

packages/vite/src/node/optimizer/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,11 +1330,11 @@ function getDepHash(environment: Environment): {
13301330
}
13311331
}
13321332

1333-
function getOptimizedBrowserHash(
1333+
export function getOptimizedBrowserHash(
13341334
hash: string,
13351335
deps: Record<string, string>,
13361336
timestamp = '',
1337-
) {
1337+
): string {
13381338
return getHash(hash + JSON.stringify(deps) + timestamp)
13391339
}
13401340

packages/vite/src/node/optimizer/optimizer.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
depsLogString,
1616
discoverProjectDependencies,
1717
extractExportsData,
18+
getOptimizedBrowserHash,
1819
getOptimizedDepPath,
1920
initDepsOptimizerMetadata,
2021
loadCachedDepOptimizationMetadata,
@@ -237,6 +238,20 @@ export function createDepsOptimizer(
237238
const knownDeps = prepareKnownDeps()
238239
startNextDiscoveredBatch()
239240

241+
// Ensure consistent browserHash between in-memory and persisted metadata.
242+
// By setting it eagerly here (before scanProcessing resolves), both the
243+
// current server and any subsequent server loading _metadata.json will
244+
// produce the same browserHash for these deps, avoiding mismatches during
245+
// mid-load restarts.
246+
metadata.browserHash = getOptimizedBrowserHash(
247+
metadata.hash,
248+
depsFromOptimizedDepInfo(knownDeps),
249+
)
250+
251+
for (const dep of Object.keys(metadata.discovered)) {
252+
metadata.discovered[dep].browserHash = metadata.browserHash
253+
}
254+
240255
// For dev, we run the scanner and the first optimization
241256
// run on the background
242257
optimizationResult = runOptimizeDeps(environment, knownDeps)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
331331
depsOptimizer &&
332332
moduleListContains(depsOptimizer.options.exclude, url)
333333
) {
334+
// Wait for scanning to complete to ensure stable browserHash and metadata
335+
// This prevents inconsistent hashes between in-memory and persisted metadata
334336
await depsOptimizer.scanProcessing
335337

336338
// if the dependency encountered in the optimized file was excluded from the optimization

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

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,11 @@ export function resolvePlugin(
214214
}
215215
}
216216

217-
let res: string | PartialResolvedId | undefined
217+
let res:
218+
| string
219+
| PartialResolvedId
220+
| undefined
221+
| Promise<PartialResolvedId | undefined>
218222

219223
// resolve pre-bundled deps requests, these could be resolved by
220224
// tryFileResolve or /fs/ resolution but these files may not yet
@@ -233,7 +237,7 @@ export function resolvePlugin(
233237
// always return here even if res doesn't exist since /@fs/ is explicit
234238
// if the file doesn't exist it should be a 404.
235239
debug?.(`[@fs] ${colors.cyan(id)} -> ${colors.dim(res)}`)
236-
return ensureVersionQuery(res, id, options, depsOptimizer)
240+
return await ensureVersionQuery(res, id, options, depsOptimizer)
237241
}
238242

239243
// URL
@@ -246,7 +250,7 @@ export function resolvePlugin(
246250
const fsPath = path.resolve(root, id.slice(1))
247251
if ((res = tryFsResolve(fsPath, options))) {
248252
debug?.(`[url] ${colors.cyan(id)} -> ${colors.dim(res)}`)
249-
return ensureVersionQuery(res, id, options, depsOptimizer)
253+
return await ensureVersionQuery(res, id, options, depsOptimizer)
250254
}
251255
}
252256

@@ -268,6 +272,9 @@ export function resolvePlugin(
268272
// Optimized files could not yet exist in disk, resolve to the full path
269273
// Inject the current browserHash version if the path doesn't have one
270274
if (!options.isBuild && !DEP_VERSION_RE.test(normalizedFsPath)) {
275+
// Wait for scanning to complete to ensure stable browserHash
276+
// This prevents inconsistent hashes between in-memory and persisted metadata
277+
await depsOptimizer.scanProcessing
271278
const browserHash = optimizedDepInfoFromFile(
272279
depsOptimizer.metadata,
273280
normalizedFsPath,
@@ -287,7 +294,7 @@ export function resolvePlugin(
287294
}
288295

289296
if ((res = tryFsResolve(fsPath, options))) {
290-
res = ensureVersionQuery(res, id, options, depsOptimizer)
297+
res = await ensureVersionQuery(res, id, options, depsOptimizer)
291298
debug?.(`[relative] ${colors.cyan(id)} -> ${colors.dim(res)}`)
292299

293300
if (!options.idOnly && !options.scan && options.isBuild) {
@@ -318,7 +325,7 @@ export function resolvePlugin(
318325
const fsPath = path.resolve(basedir, id)
319326
if ((res = tryFsResolve(fsPath, options))) {
320327
debug?.(`[drive-relative] ${colors.cyan(id)} -> ${colors.dim(res)}`)
321-
return ensureVersionQuery(res, id, options, depsOptimizer)
328+
return await ensureVersionQuery(res, id, options, depsOptimizer)
322329
}
323330
}
324331

@@ -328,7 +335,7 @@ export function resolvePlugin(
328335
(res = tryFsResolve(id, options))
329336
) {
330337
debug?.(`[fs] ${colors.cyan(id)} -> ${colors.dim(res)}`)
331-
return ensureVersionQuery(res, id, options, depsOptimizer)
338+
return await ensureVersionQuery(res, id, options, depsOptimizer)
332339
}
333340

334341
// external
@@ -378,15 +385,12 @@ export function resolvePlugin(
378385
return res
379386
}
380387

381-
if (
382-
(res = tryNodeResolve(
383-
id,
384-
importer,
385-
options,
386-
depsOptimizer,
387-
external,
388-
))
389-
) {
388+
res = tryNodeResolve(id, importer, options, depsOptimizer, external)
389+
if (res) {
390+
// Handle both sync and async returns
391+
if (res instanceof Promise) {
392+
return await res
393+
}
390394
return res
391395
}
392396

@@ -528,18 +532,21 @@ function resolveSubpathImports(
528532
return importsPath + postfix
529533
}
530534

531-
function ensureVersionQuery(
535+
async function ensureVersionQuery(
532536
resolved: string,
533537
id: string,
534538
options: InternalResolveOptions,
535539
depsOptimizer?: DepsOptimizer,
536-
): string {
540+
): Promise<string> {
537541
if (
538542
!options.isBuild &&
539543
!options.scan &&
540544
depsOptimizer &&
541545
!(resolved === normalizedClientEntry || resolved === normalizedEnvEntry)
542546
) {
547+
// Wait for scanning to complete to ensure stable browserHash
548+
// This prevents inconsistent hashes between in-memory and persisted metadata
549+
await depsOptimizer.scanProcessing
543550
// Ensure that direct imports of node_modules have the same version query
544551
// as if they would have been imported through a bare import
545552
// Use the original id to do the check as the resolved id may be the real
@@ -699,7 +706,7 @@ export function tryNodeResolve(
699706
options: InternalResolveOptions,
700707
depsOptimizer?: DepsOptimizer,
701708
externalize?: boolean,
702-
): PartialResolvedId | undefined {
709+
): PartialResolvedId | undefined | Promise<PartialResolvedId | undefined> {
703710
const { root, dedupe, isBuild, preserveSymlinks, packageCache } = options
704711

705712
// check for deep import, e.g. "my-lib/foo"
@@ -833,6 +840,20 @@ export function tryNodeResolve(
833840
// can cache it without re-validation, but only do so for known js types.
834841
// otherwise we may introduce duplicated modules for externalized files
835842
// from pre-bundled deps.
843+
844+
// If we need to access browserHash, we must wait for scanning to complete
845+
// to ensure stable browserHash and prevent inconsistent hashes between
846+
// in-memory and persisted metadata
847+
if (depsOptimizer.scanProcessing) {
848+
return depsOptimizer.scanProcessing.then(() => {
849+
const versionHash = depsOptimizer.metadata.browserHash
850+
if (versionHash && isJsType && resolved) {
851+
resolved = injectQuery(resolved, `v=${versionHash}`)
852+
}
853+
return processResult({ id: resolved! })
854+
})
855+
}
856+
836857
const versionHash = depsOptimizer.metadata.browserHash
837858
if (versionHash && isJsType) {
838859
resolved = injectQuery(resolved, `v=${versionHash}`)
@@ -854,8 +875,8 @@ export async function tryOptimizedResolve(
854875
preserveSymlinks?: boolean,
855876
packageCache?: PackageCache,
856877
): Promise<string | undefined> {
857-
// TODO: we need to wait until scanning is done here as this function
858-
// is used in the preAliasPlugin to decide if an aliased dep is optimized,
878+
// Wait for scanning to complete to ensure stable browserHash and metadata
879+
// This function is used in the preAliasPlugin to decide if an aliased dep is optimized,
859880
// and avoid replacing the bare import with the resolved path.
860881
// We should be able to remove this in the future
861882
await depsOptimizer.scanProcessing
@@ -1116,12 +1137,14 @@ function tryResolveBrowserMapping(
11161137
if (browserMappedPath) {
11171138
if (
11181139
(res = bareImportRE.test(browserMappedPath)
1119-
? tryNodeResolve(
1120-
browserMappedPath,
1121-
importer,
1122-
options,
1123-
undefined,
1124-
undefined,
1140+
? (
1141+
tryNodeResolve(
1142+
browserMappedPath,
1143+
importer,
1144+
options,
1145+
undefined,
1146+
undefined,
1147+
) as PartialResolvedId | undefined
11251148
)?.id
11261149
: tryFsResolve(path.join(pkg.dir, browserMappedPath), options))
11271150
) {

playground/optimize-deps/__tests__/optimize-deps.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, test } from 'vitest'
22
import {
3+
browser,
34
browserErrors,
45
browserLogs,
56
getColor,
@@ -8,6 +9,7 @@ import {
89
page,
910
readDepOptimizationMetadata,
1011
serverLogs,
12+
viteServer,
1113
viteTestUrl,
1214
} from '~utils'
1315

@@ -354,3 +356,32 @@ test('dependency with external sub-dependencies', async () => {
354356
.poll(() => page.textContent('.dep-cjs-with-external-deps-node-builtin'))
355357
.toBe('foo bar')
356358
})
359+
360+
test.runIf(isServe)(
361+
'the metadata written by the dep optimizer should match the metadata in memory',
362+
async () => {
363+
await viteServer.waitForRequestsIdle()
364+
const metadata = readDepOptimizationMetadata()
365+
366+
let page = await browser.newPage()
367+
const response = page.waitForResponse(/\/cjs\.js/)
368+
await page.goto(viteTestUrl)
369+
370+
const content = await (await response).text()
371+
const reactBrowserHash = content.match(
372+
/from "\/node_modules\/\.vite\/deps\/react\.js\?v=([^"&]*)"/,
373+
)?.[1]
374+
expect(reactBrowserHash).toBe(metadata.browserHash)
375+
376+
await viteServer.restart()
377+
378+
page = await browser.newPage()
379+
const responseAfterRestart = page.waitForResponse(/cjs\.js/)
380+
await page.goto(viteTestUrl)
381+
const contentAfterRestart = await (await responseAfterRestart).text()
382+
const reactBrowserHashAfterRestart = contentAfterRestart.match(
383+
/from "\/node_modules\/\.vite\/deps\/react\.js\?v=([^"&]*)"/,
384+
)?.[1]
385+
expect(reactBrowserHashAfterRestart).toBe(metadata.browserHash)
386+
},
387+
)

0 commit comments

Comments
 (0)