Skip to content

Commit 87c0050

Browse files
authored
fix(ssr): use tryNodeResolve instead of resolveFrom (#3951)
1 parent 9d50df8 commit 87c0050

File tree

5 files changed

+145
-45
lines changed

5 files changed

+145
-45
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { modulePreloadPolyfillPlugin } from './modulePreloadPolyfill'
1414
import { webWorkerPlugin } from './worker'
1515
import { preAliasPlugin } from './preAlias'
1616
import { definePlugin } from './define'
17+
import { ssrRequireHookPlugin } from './ssrRequireHook'
1718

1819
export async function resolvePlugins(
1920
config: ResolvedConfig,
@@ -42,6 +43,7 @@ export async function resolvePlugins(
4243
ssrConfig: config.ssr,
4344
asSrc: true
4445
}),
46+
config.build.ssr ? ssrRequireHookPlugin(config) : null,
4547
htmlInlineScriptProxyPlugin(config),
4648
cssPlugin(config),
4749
config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,

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

+9-10
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export interface InternalResolveOptions extends ResolveOptions {
6767
tryPrefix?: string
6868
skipPackageJson?: boolean
6969
preferRelative?: boolean
70+
preserveSymlinks?: boolean
7071
isRequire?: boolean
7172
// #3040
7273
// when the importer is a ts module,
@@ -305,7 +306,7 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
305306
function tryFsResolve(
306307
fsPath: string,
307308
options: InternalResolveOptions,
308-
preserveSymlinks: boolean,
309+
preserveSymlinks?: boolean,
309310
tryIndex = true,
310311
targetWeb = true
311312
): string | undefined {
@@ -426,7 +427,7 @@ function tryResolveFile(
426427
options: InternalResolveOptions,
427428
tryIndex: boolean,
428429
targetWeb: boolean,
429-
preserveSymlinks: boolean,
430+
preserveSymlinks?: boolean,
430431
tryPrefix?: string,
431432
skipPackageJson?: boolean
432433
): string | undefined {
@@ -489,7 +490,7 @@ export const idToPkgMap = new Map<string, PackageData>()
489490

490491
export function tryNodeResolve(
491492
id: string,
492-
importer: string | undefined,
493+
importer: string | null | undefined,
493494
options: InternalResolveOptions,
494495
targetWeb: boolean,
495496
server?: ViteDevServer,
@@ -522,14 +523,12 @@ export function tryNodeResolve(
522523
basedir = root
523524
}
524525

525-
const preserveSymlinks = !!server?.config.resolve.preserveSymlinks
526-
527526
// nested node module, step-by-step resolve to the basedir of the nestedPath
528527
if (nestedRoot) {
529-
basedir = nestedResolveFrom(nestedRoot, basedir, preserveSymlinks)
528+
basedir = nestedResolveFrom(nestedRoot, basedir, options.preserveSymlinks)
530529
}
531530

532-
const pkg = resolvePackageData(pkgId, basedir, preserveSymlinks)
531+
const pkg = resolvePackageData(pkgId, basedir, options.preserveSymlinks)
533532

534533
if (!pkg) {
535534
return
@@ -541,9 +540,9 @@ export function tryNodeResolve(
541540
pkg,
542541
options,
543542
targetWeb,
544-
preserveSymlinks
543+
options.preserveSymlinks
545544
)
546-
: resolvePackageEntry(id, pkg, options, targetWeb, preserveSymlinks)
545+
: resolvePackageEntry(id, pkg, options, targetWeb, options.preserveSymlinks)
547546
if (!resolved) {
548547
return
549548
}
@@ -876,7 +875,7 @@ function resolveDeepImport(
876875
}: PackageData,
877876
options: InternalResolveOptions,
878877
targetWeb: boolean,
879-
preserveSymlinks: boolean
878+
preserveSymlinks?: boolean
880879
): string | undefined {
881880
const cache = getResolvedCache(id, targetWeb)
882881
if (cache) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import MagicString from 'magic-string'
2+
import { ResolvedConfig } from '..'
3+
import { Plugin } from '../plugin'
4+
5+
/**
6+
* This plugin hooks into Node's module resolution algorithm at runtime,
7+
* so that SSR builds can benefit from `resolve.dedupe` like they do
8+
* in development.
9+
*/
10+
export function ssrRequireHookPlugin(config: ResolvedConfig): Plugin | null {
11+
if (config.command !== 'build' || !config.resolve.dedupe?.length) {
12+
return null
13+
}
14+
return {
15+
name: 'vite:ssr-require-hook',
16+
transform(code, id) {
17+
const moduleInfo = this.getModuleInfo(id)
18+
if (moduleInfo?.isEntry) {
19+
const s = new MagicString(code)
20+
s.prepend(
21+
`;(${dedupeRequire.toString()})(${JSON.stringify(
22+
config.resolve.dedupe
23+
)});\n`
24+
)
25+
return {
26+
code: s.toString(),
27+
map: s.generateMap({
28+
source: id
29+
})
30+
}
31+
}
32+
}
33+
}
34+
}
35+
36+
type NodeResolveFilename = (
37+
request: string,
38+
parent: NodeModule,
39+
isMain: boolean,
40+
options?: Record<string, any>
41+
) => string
42+
43+
/** Respect the `resolve.dedupe` option in production SSR. */
44+
function dedupeRequire(dedupe: string[]) {
45+
const Module = require('module') as { _resolveFilename: NodeResolveFilename }
46+
const resolveFilename = Module._resolveFilename
47+
Module._resolveFilename = function (request, parent, isMain, options) {
48+
if (request[0] !== '.' && request[0] !== '/') {
49+
const parts = request.split('/')
50+
const pkgName = parts[0][0] === '@' ? parts[0] + '/' + parts[1] : parts[0]
51+
if (dedupe.includes(pkgName)) {
52+
// Use this module as the parent.
53+
parent = module
54+
}
55+
}
56+
return resolveFilename!(request, parent, isMain, options)
57+
}
58+
}
59+
60+
export function hookNodeResolve(
61+
getResolver: (resolveFilename: NodeResolveFilename) => NodeResolveFilename
62+
): () => void {
63+
const Module = require('module') as { _resolveFilename: NodeResolveFilename }
64+
const prevResolver = Module._resolveFilename
65+
Module._resolveFilename = getResolver(prevResolver)
66+
return () => {
67+
Module._resolveFilename = prevResolver
68+
}
69+
}

packages/vite/src/node/ssr/ssrExternal.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ export function resolveSSRExternal(
3232
seen.add(id)
3333
})
3434

35-
collectExternals(config.root, ssrExternals, seen)
35+
collectExternals(
36+
config.root,
37+
config.resolve.preserveSymlinks,
38+
ssrExternals,
39+
seen
40+
)
3641

3742
for (const dep of knownImports) {
3843
// Assume external if not yet seen
@@ -59,6 +64,7 @@ export function resolveSSRExternal(
5964
// do we need to do this ahead of time or could we do it lazily?
6065
function collectExternals(
6166
root: string,
67+
preserveSymlinks: boolean | undefined,
6268
ssrExternals: Set<string>,
6369
seen: Set<string>
6470
) {
@@ -75,6 +81,7 @@ function collectExternals(
7581

7682
const resolveOptions: InternalResolveOptions = {
7783
root,
84+
preserveSymlinks,
7885
isProduction: false,
7986
isBuild: true
8087
}
@@ -132,7 +139,7 @@ function collectExternals(
132139
// or are there others like SystemJS / AMD that we'd need to handle?
133140
// for now, we'll just leave this as is
134141
else if (/\.m?js$/.test(esmEntry)) {
135-
if (pkg.type === "module" || esmEntry.endsWith('.mjs')) {
142+
if (pkg.type === 'module' || esmEntry.endsWith('.mjs')) {
136143
ssrExternals.add(id)
137144
continue
138145
}
@@ -145,7 +152,7 @@ function collectExternals(
145152
}
146153

147154
for (const depRoot of depsToTrace) {
148-
collectExternals(depRoot, ssrExternals, seen)
155+
collectExternals(depRoot, preserveSymlinks, ssrExternals, seen)
149156
}
150157
}
151158

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

+55-32
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import fs from 'fs'
21
import path from 'path'
32
import { pathToFileURL } from 'url'
4-
import { ViteDevServer } from '..'
3+
import { ViteDevServer } from '../server'
54
import {
65
dynamicImport,
7-
cleanUrl,
86
isBuiltin,
9-
resolveFrom,
107
unwrapId,
118
usingDynamicImport
129
} from '../utils'
@@ -19,6 +16,8 @@ import {
1916
ssrDynamicImportKey
2017
} from './ssrTransform'
2118
import { transformRequest } from '../server/transformRequest'
19+
import { InternalResolveOptions, tryNodeResolve } from '../plugins/resolve'
20+
import { hookNodeResolve } from '../plugins/ssrRequireHook'
2221

2322
interface SSRContext {
2423
global: typeof globalThis
@@ -96,13 +95,32 @@ async function instantiateModule(
9695
urlStack = urlStack.concat(url)
9796
const isCircular = (url: string) => urlStack.includes(url)
9897

98+
const {
99+
isProduction,
100+
resolve: { dedupe },
101+
root
102+
} = server.config
103+
104+
const resolveOptions: InternalResolveOptions = {
105+
conditions: ['node'],
106+
dedupe,
107+
// Prefer CommonJS modules.
108+
extensions: ['.js', '.mjs', '.ts', '.jsx', '.tsx', '.json'],
109+
isBuild: true,
110+
isProduction,
111+
// Disable "module" condition.
112+
isRequire: true,
113+
mainFields: ['main'],
114+
root
115+
}
116+
99117
// Since dynamic imports can happen in parallel, we need to
100118
// account for multiple pending deps and duplicate imports.
101119
const pendingDeps: string[] = []
102120

103121
const ssrImport = async (dep: string) => {
104122
if (dep[0] !== '.' && dep[0] !== '/') {
105-
return nodeImport(dep, mod.file, server.config)
123+
return nodeImport(dep, mod.file!, resolveOptions)
106124
}
107125
dep = unwrapId(dep)
108126
if (!isCircular(dep) && !pendingImports.get(dep)?.some(isCircular)) {
@@ -185,21 +203,48 @@ async function instantiateModule(
185203
// In node@12+ we can use dynamic import to load CJS and ESM
186204
async function nodeImport(
187205
id: string,
188-
importer: string | null,
189-
config: ViteDevServer['config']
206+
importer: string,
207+
resolveOptions: InternalResolveOptions
190208
) {
209+
// Node's module resolution is hi-jacked so Vite can ensure the
210+
// configured `resolve.dedupe` and `mode` options are respected.
211+
const viteResolve = (id: string, importer: string) => {
212+
const resolved = tryNodeResolve(id, importer, resolveOptions, false)
213+
if (!resolved) {
214+
const err: any = new Error(
215+
`Cannot find module '${id}' imported from '${importer}'`
216+
)
217+
err.code = 'ERR_MODULE_NOT_FOUND'
218+
throw err
219+
}
220+
return resolved.id
221+
}
222+
223+
// When an ESM module imports an ESM dependency, this hook is *not* used.
224+
const unhookNodeResolve = hookNodeResolve(
225+
(nodeResolve) => (id, parent, isMain, options) =>
226+
id[0] === '.' || isBuiltin(id)
227+
? nodeResolve(id, parent, isMain, options)
228+
: viteResolve(id, parent.id)
229+
)
230+
191231
let url: string
192232
// `resolve` doesn't handle `node:` builtins, so handle them directly
193233
if (id.startsWith('node:') || isBuiltin(id)) {
194234
url = id
195235
} else {
196-
url = resolve(id, importer, config.root, !!config.resolve.preserveSymlinks)
236+
url = viteResolve(id, importer)
197237
if (usingDynamicImport) {
198238
url = pathToFileURL(url).toString()
199239
}
200240
}
201-
const mod = await dynamicImport(url)
202-
return proxyESM(id, mod)
241+
242+
try {
243+
const mod = await dynamicImport(url)
244+
return proxyESM(id, mod)
245+
} finally {
246+
unhookNodeResolve()
247+
}
203248
}
204249

205250
// rollup-style default import interop for cjs
@@ -216,25 +261,3 @@ function proxyESM(id: string, mod: any) {
216261
}
217262
})
218263
}
219-
220-
const resolveCache = new Map<string, string>()
221-
222-
function resolve(
223-
id: string,
224-
importer: string | null,
225-
root: string,
226-
preserveSymlinks: boolean
227-
) {
228-
const key = id + importer + root
229-
const cached = resolveCache.get(key)
230-
if (cached) {
231-
return cached
232-
}
233-
const resolveDir =
234-
importer && fs.existsSync(cleanUrl(importer))
235-
? path.dirname(importer)
236-
: root
237-
const resolved = resolveFrom(id, resolveDir, preserveSymlinks, true)
238-
resolveCache.set(key, resolved)
239-
return resolved
240-
}

0 commit comments

Comments
 (0)