Skip to content

Commit 404f9d3

Browse files
committed
fix(ssr): use tryNodeResolve instead of resolveFrom
1 parent a44f480 commit 404f9d3

File tree

5 files changed

+146
-43
lines changed

5 files changed

+146
-43
lines changed

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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 9 additions & 10 deletions
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) {
Lines changed: 69 additions & 0 deletions
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

Lines changed: 10 additions & 3 deletions
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

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import fs from 'fs'
21
import path from 'path'
2+
import { Module } from 'module'
33
import { pathToFileURL } from 'url'
4-
import { ViteDevServer } from '..'
4+
import { ViteDevServer } from '../server'
55
import {
66
dynamicImport,
77
cleanUrl,
@@ -19,6 +19,8 @@ import {
1919
ssrDynamicImportKey
2020
} from './ssrTransform'
2121
import { transformRequest } from '../server/transformRequest'
22+
import { InternalResolveOptions, tryNodeResolve } from '../plugins/resolve'
23+
import { hookNodeResolve } from '../plugins/ssrRequireHook'
2224

2325
interface SSRContext {
2426
global: typeof globalThis
@@ -96,13 +98,32 @@ async function instantiateModule(
9698
urlStack = urlStack.concat(url)
9799
const isCircular = (url: string) => urlStack.includes(url)
98100

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

103124
const ssrImport = async (dep: string) => {
104125
if (dep[0] !== '.' && dep[0] !== '/') {
105-
return nodeImport(dep, mod.file, server.config)
126+
return nodeImport(dep, mod.file!, resolveOptions)
106127
}
107128
dep = unwrapId(dep)
108129
if (!isCircular(dep) && !pendingImports.get(dep)?.some(isCircular)) {
@@ -185,21 +206,48 @@ async function instantiateModule(
185206
// In node@12+ we can use dynamic import to load CJS and ESM
186207
async function nodeImport(
187208
id: string,
188-
importer: string | null,
189-
config: ViteDevServer['config']
209+
importer: string,
210+
resolveOptions: InternalResolveOptions
190211
) {
212+
// Node's module resolution is hi-jacked so Vite can ensure the
213+
// configured `resolve.dedupe` and `mode` options are respected.
214+
const viteResolve = (id: string, importer: string) => {
215+
const resolved = tryNodeResolve(id, importer, resolveOptions, false)
216+
if (!resolved) {
217+
const err: any = new Error(
218+
`Cannot find module '${id}' imported from '${importer}'`
219+
)
220+
err.code = 'ERR_MODULE_NOT_FOUND'
221+
throw err
222+
}
223+
return resolved.id
224+
}
225+
226+
// When an ESM module imports an ESM dependency, this hook is *not* used.
227+
const unhookNodeResolve = hookNodeResolve(
228+
(nodeResolve) => (id, parent, isMain, options) =>
229+
id[0] === '.' || isBuiltin(id)
230+
? nodeResolve(id, parent, isMain, options)
231+
: viteResolve(id, parent.id)
232+
)
233+
191234
let url: string
192235
// `resolve` doesn't handle `node:` builtins, so handle them directly
193236
if (id.startsWith('node:') || isBuiltin(id)) {
194237
url = id
195238
} else {
196-
url = resolve(id, importer, config.root, !!config.resolve.preserveSymlinks)
239+
url = viteResolve(id, importer)
197240
if (usingDynamicImport) {
198241
url = pathToFileURL(url).toString()
199242
}
200243
}
201-
const mod = await dynamicImport(url)
202-
return proxyESM(id, mod)
244+
245+
try {
246+
const mod = await dynamicImport(url)
247+
return proxyESM(id, mod)
248+
} finally {
249+
unhookNodeResolve()
250+
}
203251
}
204252

205253
// rollup-style default import interop for cjs
@@ -216,25 +264,3 @@ function proxyESM(id: string, mod: any) {
216264
}
217265
})
218266
}
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)