Skip to content

Commit a6e3078

Browse files
committed
fix(ssr): use tryNodeResolve instead of resolveFrom (vitejs#3951)
1 parent 7ef25e7 commit a6e3078

File tree

5 files changed

+129
-30
lines changed

5 files changed

+129
-30
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
ssrTarget: config.ssr?.target,
4344
asSrc: true
4445
}),
46+
config.build.ssr ? ssrRequireHookPlugin(config) : null,
4547
htmlInlineScriptProxyPlugin(),
4648
cssPlugin(config),
4749
config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,

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

+11-6
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
normalizePath,
2121
fsPathFromId,
2222
ensureVolumeInPath,
23-
resolveFrom,
2423
isDataUrl,
2524
cleanUrl,
2625
slash
@@ -29,6 +28,7 @@ import { ViteDevServer, SSRTarget } from '..'
2928
import { createFilter } from '@rollup/pluginutils'
3029
import { PartialResolvedId } from 'rollup'
3130
import { resolve as _resolveExports } from 'resolve.exports'
31+
import resolve from 'resolve'
3232

3333
// special id for paths marked with browser: false
3434
// https://github.com/defunctzombie/package-browser-field-spec#ignore-a-module
@@ -61,6 +61,7 @@ export interface InternalResolveOptions extends ResolveOptions {
6161
tryPrefix?: string
6262
preferRelative?: boolean
6363
isRequire?: boolean
64+
preserveSymlinks?: boolean
6465
}
6566

6667
export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
@@ -361,7 +362,7 @@ export const idToPkgMap = new Map<string, PackageData>()
361362

362363
export function tryNodeResolve(
363364
id: string,
364-
importer: string | undefined,
365+
importer: string | null | undefined,
365366
options: InternalResolveOptions,
366367
targetWeb: boolean,
367368
server?: ViteDevServer,
@@ -379,12 +380,12 @@ export function tryNodeResolve(
379380
path.isAbsolute(importer) &&
380381
fs.existsSync(cleanUrl(importer))
381382
) {
382-
basedir = path.dirname(importer)
383+
basedir = fs.realpathSync.native(path.dirname(importer))
383384
} else {
384385
basedir = root
385386
}
386387

387-
const pkg = resolvePackageData(pkgId, basedir)
388+
const pkg = resolvePackageData(pkgId, basedir, options.preserveSymlinks)
388389

389390
if (!pkg) {
390391
return
@@ -483,14 +484,18 @@ const packageCache = new Map<string, PackageData>()
483484

484485
export function resolvePackageData(
485486
id: string,
486-
basedir: string
487+
basedir: string,
488+
preserveSymlinks = false
487489
): PackageData | undefined {
488490
const cacheKey = id + basedir
489491
if (packageCache.has(cacheKey)) {
490492
return packageCache.get(cacheKey)
491493
}
492494
try {
493-
const pkgPath = resolveFrom(`${id}/package.json`, basedir)
495+
const pkgPath = resolve.sync(`${id}/package.json`, {
496+
basedir,
497+
preserveSymlinks
498+
})
494499
return loadPackageData(pkgPath, cacheKey)
495500
} catch (e) {
496501
isDebug && debug(`${chalk.red(`[failed loading package.json]`)} ${id}`)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import MagicString from 'magic-string'
2+
import { ResolvedConfig } from '..'
3+
import { Plugin } from '../plugin'
4+
5+
export function ssrRequireHookPlugin(config: ResolvedConfig): Plugin | null {
6+
if (config.command !== 'build' || !config.resolve.dedupe?.length) {
7+
return null
8+
}
9+
return {
10+
name: 'vite:ssr-require-hook',
11+
transform(code, id) {
12+
const moduleInfo = this.getModuleInfo(id)
13+
if (moduleInfo?.isEntry) {
14+
const s = new MagicString(code)
15+
s.prepend(
16+
`;(${dedupeRequire.toString()})(${JSON.stringify(
17+
config.resolve.dedupe
18+
)});\n`
19+
)
20+
return {
21+
code: s.toString(),
22+
map: s.generateMap({
23+
source: id
24+
})
25+
}
26+
}
27+
}
28+
}
29+
}
30+
31+
type NodeResolveFilename = (
32+
request: string,
33+
parent: NodeModule,
34+
isMain: boolean,
35+
options?: Record<string, any>
36+
) => string
37+
38+
/** Respect the `resolve.dedupe` option in production SSR. */
39+
function dedupeRequire(dedupe: string[]) {
40+
const Module = require('module') as { _resolveFilename: NodeResolveFilename }
41+
const resolveFilename = Module._resolveFilename
42+
Module._resolveFilename = function (request, parent, isMain, options) {
43+
if (request[0] !== '.' && request[0] !== '/') {
44+
const parts = request.split('/')
45+
const pkgName = parts[0][0] === '@' ? parts[0] + '/' + parts[1] : parts[0]
46+
if (dedupe.includes(pkgName)) {
47+
// Use this module as the parent.
48+
parent = module
49+
}
50+
}
51+
return resolveFilename!(request, parent, isMain, options)
52+
}
53+
}
54+
55+
export function hookNodeResolve(
56+
getResolver: (resolveFilename: NodeResolveFilename) => NodeResolveFilename
57+
): () => void {
58+
const Module = require('module') as { _resolveFilename: NodeResolveFilename }
59+
const prevResolver = Module._resolveFilename
60+
Module._resolveFilename = getResolver(prevResolver)
61+
return () => {
62+
Module._resolveFilename = prevResolver
63+
}
64+
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function resolveSSRExternal(
3535

3636
const resolveOptions: InternalResolveOptions = {
3737
root,
38+
preserveSymlinks: true,
3839
isProduction: false,
3940
isBuild: true
4041
}

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

+51-24
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 { ViteDevServer } from '..'
4-
import { cleanUrl, resolveFrom, unwrapId } from '../utils'
4+
import { unwrapId } from '../utils'
55
import { rebindErrorStacktrace, ssrRewriteStacktrace } from './ssrStacktrace'
66
import {
77
ssrExportAllKey,
@@ -11,6 +11,8 @@ import {
1111
ssrDynamicImportKey
1212
} from './ssrTransform'
1313
import { transformRequest } from '../server/transformRequest'
14+
import { InternalResolveOptions, tryNodeResolve } from '../plugins/resolve'
15+
import { hookNodeResolve } from '../plugins/ssrRequireHook'
1416

1517
interface SSRContext {
1618
global: NodeJS.Global
@@ -80,7 +82,24 @@ async function instantiateModule(
8082
// referenced before it's been instantiated.
8183
mod.ssrModule = ssrModule
8284

83-
const ssrImportMeta = { url }
85+
const {
86+
isProduction,
87+
resolve: { dedupe },
88+
root
89+
} = server.config
90+
91+
const resolveOptions: InternalResolveOptions = {
92+
conditions: ['node'],
93+
dedupe,
94+
// Prefer CommonJS modules.
95+
extensions: ['.js', '.mjs', '.ts', '.jsx', '.tsx', '.json'],
96+
isBuild: true,
97+
isProduction,
98+
// Disable "module" condition.
99+
isRequire: true,
100+
mainFields: ['main'],
101+
root
102+
}
84103

85104
urlStack = urlStack.concat(url)
86105
const isCircular = (url: string) => urlStack.includes(url)
@@ -91,7 +110,7 @@ async function instantiateModule(
91110

92111
const ssrImport = async (dep: string) => {
93112
if (dep[0] !== '.' && dep[0] !== '/') {
94-
return nodeRequire(dep, mod.file, server.config.root)
113+
return nodeRequire(dep, mod.file, resolveOptions)
95114
}
96115
dep = unwrapId(dep)
97116
if (!isCircular(dep) && !pendingImports.get(dep)?.some(isCircular)) {
@@ -132,6 +151,7 @@ async function instantiateModule(
132151
}
133152
}
134153

154+
const ssrImportMeta = { url }
135155
try {
136156
// eslint-disable-next-line @typescript-eslint/no-empty-function
137157
const AsyncFunction = async function () {}.constructor as typeof Function
@@ -168,31 +188,38 @@ async function instantiateModule(
168188
return Object.freeze(ssrModule)
169189
}
170190

171-
function nodeRequire(id: string, importer: string | null, root: string) {
172-
const mod = require(resolve(id, importer, root))
173-
const defaultExport = mod.__esModule ? mod.default : mod
191+
function nodeRequire(
192+
id: string,
193+
importer: string | null,
194+
resolveOptions: InternalResolveOptions
195+
) {
196+
const loadModule = Module.createRequire(importer || resolveOptions.root + '/')
197+
const unhookNodeResolve = hookNodeResolve(
198+
(nodeResolve) => (id, parent, isMain, options) => {
199+
if (id[0] === '.' || Module.builtinModules.includes(id)) {
200+
return nodeResolve(id, parent, isMain, options)
201+
}
202+
const resolved = tryNodeResolve(id, parent.id, resolveOptions, false)
203+
if (!resolved) {
204+
throw Error(`Cannot find module '${id}' imported from '${parent.id}'`)
205+
}
206+
return resolved.id
207+
}
208+
)
209+
210+
let mod: any
211+
try {
212+
mod = loadModule(id)
213+
} finally {
214+
unhookNodeResolve()
215+
}
216+
174217
// rollup-style default import interop for cjs
218+
const defaultExport = mod.__esModule ? mod.default : mod
175219
return new Proxy(mod, {
176220
get(mod, prop) {
177221
if (prop === 'default') return defaultExport
178222
return mod[prop]
179223
}
180224
})
181225
}
182-
183-
const resolveCache = new Map<string, string>()
184-
185-
function resolve(id: string, importer: string | null, root: string) {
186-
const key = id + importer + root
187-
const cached = resolveCache.get(key)
188-
if (cached) {
189-
return cached
190-
}
191-
const resolveDir =
192-
importer && fs.existsSync(cleanUrl(importer))
193-
? path.dirname(importer)
194-
: root
195-
const resolved = resolveFrom(id, resolveDir, true)
196-
resolveCache.set(key, resolved)
197-
return resolved
198-
}

0 commit comments

Comments
 (0)