Skip to content

Commit e7712ff

Browse files
authored
fix: ensure version query for direct node_modules imports (#9848)
1 parent cc8800a commit e7712ff

9 files changed

Lines changed: 90 additions & 28 deletions

File tree

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import type {
88
} from '..'
99
import type { Plugin } from '../plugin'
1010
import { createIsConfiguredAsSsrExternal } from '../ssr/ssrExternal'
11-
import { bareImportRE, isOptimizable, moduleListContains } from '../utils'
11+
import {
12+
bareImportRE,
13+
cleanUrl,
14+
isOptimizable,
15+
moduleListContains
16+
} from '../utils'
1217
import { getDepsOptimizer } from '../optimizer'
1318
import { tryOptimizedResolve } from './resolve'
1419

@@ -48,7 +53,7 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin {
4853
})
4954
if (resolved && !depsOptimizer.isOptimizedDepFile(resolved.id)) {
5055
const optimizeDeps = depsOptimizer.options
51-
const resolvedId = resolved.id
56+
const resolvedId = cleanUrl(resolved.id)
5257
const isVirtual = resolvedId === id || resolvedId.includes('\0')
5358
if (
5459
!isVirtual &&

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

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import { resolve as _resolveExports } from 'resolve.exports'
66
import { hasESMSyntax } from 'mlly'
77
import type { Plugin } from '../plugin'
88
import {
9+
CLIENT_ENTRY,
910
DEFAULT_EXTENSIONS,
1011
DEFAULT_MAIN_FIELDS,
1112
DEP_VERSION_RE,
13+
ENV_ENTRY,
1214
FS_PREFIX,
1315
OPTIMIZABLE_ENTRY_RE,
1416
SPECIAL_QUERY_RE
@@ -42,12 +44,17 @@ import type { SSROptions } from '..'
4244
import type { PackageCache, PackageData } from '../packages'
4345
import { loadPackageData, resolvePackageData } from '../packages'
4446

47+
const normalizedClientEntry = normalizePath(CLIENT_ENTRY)
48+
const normalizedEnvEntry = normalizePath(ENV_ENTRY)
49+
4550
// special id for paths marked with browser: false
4651
// https://github.com/defunctzombie/package-browser-field-spec#ignore-a-module
4752
export const browserExternalId = '__vite-browser-external'
4853
// special id for packages that are optional peer deps
4954
export const optionalPeerDepId = '__vite-optional-peer-dep'
5055

56+
const nodeModulesInPathRE = /(^|\/)node_modules\//
57+
5158
const isDebug = process.env.DEBUG
5259
const debug = createDebugger('vite:resolve-details', {
5360
onlyWhenFocused: true
@@ -155,14 +162,38 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin {
155162
return optimizedPath
156163
}
157164

165+
const ensureVersionQuery = (resolved: string): string => {
166+
if (
167+
!options.isBuild &&
168+
depsOptimizer &&
169+
!(
170+
resolved === normalizedClientEntry ||
171+
resolved === normalizedEnvEntry
172+
)
173+
) {
174+
// Ensure that direct imports of node_modules have the same version query
175+
// as if they would have been imported through a bare import
176+
// Use the original id to do the check as the resolved id may be the real
177+
// file path after symlinks resolution
178+
const isNodeModule = !!normalizePath(id).match(nodeModulesInPathRE)
179+
if (isNodeModule && !resolved.match(DEP_VERSION_RE)) {
180+
const versionHash = depsOptimizer.metadata.browserHash
181+
if (versionHash && OPTIMIZABLE_ENTRY_RE.test(resolved)) {
182+
resolved = injectQuery(resolved, `v=${versionHash}`)
183+
}
184+
}
185+
}
186+
return resolved
187+
}
188+
158189
// explicit fs paths that starts with /@fs/*
159190
if (asSrc && id.startsWith(FS_PREFIX)) {
160191
const fsPath = fsPathFromId(id)
161192
res = tryFsResolve(fsPath, options)
162193
isDebug && debug(`[@fs] ${colors.cyan(id)} -> ${colors.dim(res)}`)
163194
// always return here even if res doesn't exist since /@fs/ is explicit
164195
// if the file doesn't exist it should be a 404
165-
return res || fsPath
196+
return ensureVersionQuery(res || fsPath)
166197
}
167198

168199
// URL
@@ -171,7 +202,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin {
171202
const fsPath = path.resolve(root, id.slice(1))
172203
if ((res = tryFsResolve(fsPath, options))) {
173204
isDebug && debug(`[url] ${colors.cyan(id)} -> ${colors.dim(res)}`)
174-
return res
205+
return ensureVersionQuery(res)
175206
}
176207
}
177208

@@ -201,26 +232,6 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin {
201232
return normalizedFsPath
202233
}
203234

204-
const pathFromBasedir = normalizedFsPath.slice(basedir.length)
205-
if (pathFromBasedir.startsWith('/node_modules/')) {
206-
// normalize direct imports from node_modules to bare imports, so the
207-
// hashing logic is shared and we avoid duplicated modules #2503
208-
const bareImport = pathFromBasedir.slice('/node_modules/'.length)
209-
if (
210-
(res = tryNodeResolve(
211-
bareImport,
212-
importer,
213-
options,
214-
targetWeb,
215-
depsOptimizer,
216-
ssr
217-
)) &&
218-
res.id.startsWith(normalizedFsPath)
219-
) {
220-
return res
221-
}
222-
}
223-
224235
if (
225236
targetWeb &&
226237
(res = tryResolveBrowserMapping(fsPath, importer, options, true))
@@ -229,6 +240,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin {
229240
}
230241

231242
if ((res = tryFsResolve(fsPath, options))) {
243+
res = ensureVersionQuery(res)
232244
isDebug &&
233245
debug(`[relative] ${colors.cyan(id)} -> ${colors.dim(res)}`)
234246
const pkg = importer != null && idToPkgMap.get(importer)
@@ -250,7 +262,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin {
250262
if ((res = tryFsResolve(fsPath, options))) {
251263
isDebug &&
252264
debug(`[drive-relative] ${colors.cyan(id)} -> ${colors.dim(res)}`)
253-
return res
265+
return ensureVersionQuery(res)
254266
}
255267
}
256268

@@ -260,7 +272,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin {
260272
(res = tryFsResolve(id, options))
261273
) {
262274
isDebug && debug(`[fs] ${colors.cyan(id)} -> ${colors.dim(res)}`)
263-
return res
275+
return ensureVersionQuery(res)
264276
}
265277

266278
// external
@@ -405,7 +417,7 @@ function tryFsResolve(
405417

406418
let res: string | undefined
407419

408-
// if we fould postfix exist, we should first try resolving file with postfix. details see #4703.
420+
// if there is a postfix, try resolving it as a complete path first (#4703)
409421
if (
410422
postfix &&
411423
(res = tryResolveFile(

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,12 @@ test('flatten id should generate correctly', async () => {
146146
expect(await page.textContent('.clonedeep-dot')).toBe('clonedeep-dot')
147147
})
148148

149+
test('non optimized module is not duplicated', async () => {
150+
expect(
151+
await page.textContent('.non-optimized-module-is-not-duplicated')
152+
).toBe('from-absolute-path, from-relative-path')
153+
})
154+
149155
test.runIf(isServe)('error on builtin modules usage', () => {
150156
expect(browserLogs).toEqual(
151157
expect.arrayContaining([
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Scheme check that imports from different paths are resolved to the same module
2+
const messages = []
3+
export const add = (message) => {
4+
messages.push(message)
5+
}
6+
export const get = () => messages
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "dep-non-optimized",
3+
"private": true,
4+
"version": "1.0.0",
5+
"type": "module"
6+
}

playground/optimize-deps/index.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ <h2>Flatten Id</h2>
8787
<div class="clonedeep-slash"></div>
8888
<div class="clonedeep-dot"></div>
8989

90+
<h2>Non Optimized Module isn't duplicated</h2>
91+
<div class="non-optimized-module-is-not-duplicated"></div>
92+
9093
<script>
9194
function text(el, text) {
9295
document.querySelector(el).textContent = text
@@ -145,6 +148,18 @@ <h2>Flatten Id</h2>
145148
text('.url', parse('https://vitejs.dev').hostname)
146149

147150
import './index.astro'
151+
152+
// All these imports should end up resolved to the same URL (same ?v= injected on them)
153+
import { add as addFromDirectAbsolutePath } from '/node_modules/dep-non-optimized/index.js'
154+
import { add as addFromDirectRelativePath } from './node_modules/dep-non-optimized/index.js'
155+
import { get as getFromBareImport } from 'dep-non-optimized'
156+
157+
addFromDirectAbsolutePath('from-absolute-path')
158+
addFromDirectRelativePath('from-relative-path')
159+
text(
160+
'.non-optimized-module-is-not-duplicated',
161+
getFromBareImport().join(', ')
162+
)
148163
</script>
149164

150165
<script type="module">

playground/optimize-deps/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"dep-with-builtin-module-esm": "file:./dep-with-builtin-module-esm",
2525
"dep-with-dynamic-import": "file:./dep-with-dynamic-import",
2626
"dep-with-optional-peer-dep": "file:./dep-with-optional-peer-dep",
27+
"dep-non-optimized": "file:./dep-non-optimized",
2728
"added-in-entries": "file:./added-in-entries",
2829
"lodash-es": "^4.17.21",
2930
"nested-exclude": "file:./nested-exclude",

playground/optimize-deps/vite.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ module.exports = {
2222
// will throw if optimized (should log warning instead)
2323
'non-optimizable-include'
2424
],
25-
exclude: ['nested-exclude'],
25+
exclude: ['nested-exclude', 'dep-non-optimized'],
2626
esbuildOptions: {
2727
plugins: [
2828
{

pnpm-lock.yaml

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)