diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index a83ddae2b2bf2a..6dfa6a1437ee94 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -14,6 +14,7 @@ import { modulePreloadPolyfillPlugin } from './modulePreloadPolyfill' import { webWorkerPlugin } from './worker' import { preAliasPlugin } from './preAlias' import { definePlugin } from './define' +import { ssrRequireHookPlugin } from './ssrRequireHook' export async function resolvePlugins( config: ResolvedConfig, @@ -42,6 +43,7 @@ export async function resolvePlugins( ssrTarget: config.ssr?.target, asSrc: true }), + config.build.ssr ? ssrRequireHookPlugin(config) : null, htmlInlineScriptProxyPlugin(), cssPlugin(config), config.esbuild !== false ? esbuildPlugin(config.esbuild) : null, diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 15974ecfef7eef..9673c73b93bc09 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -20,7 +20,6 @@ import { normalizePath, fsPathFromId, ensureVolumeInPath, - resolveFrom, isDataUrl, cleanUrl, slash @@ -29,6 +28,7 @@ import { ViteDevServer, SSRTarget } from '..' import { createFilter } from '@rollup/pluginutils' import { PartialResolvedId } from 'rollup' import { resolve as _resolveExports } from 'resolve.exports' +import resolve from 'resolve' // special id for paths marked with browser: false // https://github.com/defunctzombie/package-browser-field-spec#ignore-a-module @@ -61,6 +61,7 @@ export interface InternalResolveOptions extends ResolveOptions { tryPrefix?: string preferRelative?: boolean isRequire?: boolean + preserveSymlinks?: boolean } export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { @@ -361,7 +362,7 @@ export const idToPkgMap = new Map() export function tryNodeResolve( id: string, - importer: string | undefined, + importer: string | null | undefined, options: InternalResolveOptions, targetWeb: boolean, server?: ViteDevServer, @@ -379,12 +380,12 @@ export function tryNodeResolve( path.isAbsolute(importer) && fs.existsSync(cleanUrl(importer)) ) { - basedir = path.dirname(importer) + basedir = fs.realpathSync.native(path.dirname(importer)) } else { basedir = root } - const pkg = resolvePackageData(pkgId, basedir) + const pkg = resolvePackageData(pkgId, basedir, options.preserveSymlinks) if (!pkg) { return @@ -483,14 +484,18 @@ const packageCache = new Map() export function resolvePackageData( id: string, - basedir: string + basedir: string, + preserveSymlinks = false ): PackageData | undefined { const cacheKey = id + basedir if (packageCache.has(cacheKey)) { return packageCache.get(cacheKey) } try { - const pkgPath = resolveFrom(`${id}/package.json`, basedir) + const pkgPath = resolve.sync(`${id}/package.json`, { + basedir, + preserveSymlinks + }) return loadPackageData(pkgPath, cacheKey) } catch (e) { isDebug && debug(`${chalk.red(`[failed loading package.json]`)} ${id}`) diff --git a/packages/vite/src/node/plugins/ssrRequireHook.ts b/packages/vite/src/node/plugins/ssrRequireHook.ts new file mode 100644 index 00000000000000..a8c263bd0cfecf --- /dev/null +++ b/packages/vite/src/node/plugins/ssrRequireHook.ts @@ -0,0 +1,64 @@ +import MagicString from 'magic-string' +import { ResolvedConfig } from '..' +import { Plugin } from '../plugin' + +export function ssrRequireHookPlugin(config: ResolvedConfig): Plugin | null { + if (config.command !== 'build' || !config.resolve.dedupe?.length) { + return null + } + return { + name: 'vite:ssr-require-hook', + transform(code, id) { + const moduleInfo = this.getModuleInfo(id) + if (moduleInfo?.isEntry) { + const s = new MagicString(code) + s.prepend( + `;(${dedupeRequire.toString()})(${JSON.stringify( + config.resolve.dedupe + )});\n` + ) + return { + code: s.toString(), + map: s.generateMap({ + source: id + }) + } + } + } + } +} + +type NodeResolveFilename = ( + request: string, + parent: NodeModule, + isMain: boolean, + options?: Record +) => string + +/** Respect the `resolve.dedupe` option in production SSR. */ +function dedupeRequire(dedupe: string[]) { + const Module = require('module') as { _resolveFilename: NodeResolveFilename } + const resolveFilename = Module._resolveFilename + Module._resolveFilename = function (request, parent, isMain, options) { + if (request[0] !== '.' && request[0] !== '/') { + const parts = request.split('/') + const pkgName = parts[0][0] === '@' ? parts[0] + '/' + parts[1] : parts[0] + if (dedupe.includes(pkgName)) { + // Use this module as the parent. + parent = module + } + } + return resolveFilename!(request, parent, isMain, options) + } +} + +export function hookNodeResolve( + getResolver: (resolveFilename: NodeResolveFilename) => NodeResolveFilename +): () => void { + const Module = require('module') as { _resolveFilename: NodeResolveFilename } + const prevResolver = Module._resolveFilename + Module._resolveFilename = getResolver(prevResolver) + return () => { + Module._resolveFilename = prevResolver + } +} diff --git a/packages/vite/src/node/ssr/ssrExternal.ts b/packages/vite/src/node/ssr/ssrExternal.ts index a3481283038789..8c8de96c01609f 100644 --- a/packages/vite/src/node/ssr/ssrExternal.ts +++ b/packages/vite/src/node/ssr/ssrExternal.ts @@ -35,6 +35,7 @@ export function resolveSSRExternal( const resolveOptions: InternalResolveOptions = { root, + preserveSymlinks: true, isProduction: false, isBuild: true } diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index 2eef895c627e76..7fe8d2207571e0 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -1,7 +1,7 @@ -import fs from 'fs' import path from 'path' +import { Module } from 'module' import { ViteDevServer } from '..' -import { cleanUrl, resolveFrom, unwrapId } from '../utils' +import { unwrapId } from '../utils' import { rebindErrorStacktrace, ssrRewriteStacktrace } from './ssrStacktrace' import { ssrExportAllKey, @@ -11,6 +11,8 @@ import { ssrDynamicImportKey } from './ssrTransform' import { transformRequest } from '../server/transformRequest' +import { InternalResolveOptions, tryNodeResolve } from '../plugins/resolve' +import { hookNodeResolve } from '../plugins/ssrRequireHook' interface SSRContext { global: NodeJS.Global @@ -80,7 +82,24 @@ async function instantiateModule( // referenced before it's been instantiated. mod.ssrModule = ssrModule - const ssrImportMeta = { url } + const { + isProduction, + resolve: { dedupe }, + root + } = server.config + + const resolveOptions: InternalResolveOptions = { + conditions: ['node'], + dedupe, + // Prefer CommonJS modules. + extensions: ['.js', '.mjs', '.ts', '.jsx', '.tsx', '.json'], + isBuild: true, + isProduction, + // Disable "module" condition. + isRequire: true, + mainFields: ['main'], + root + } urlStack = urlStack.concat(url) const isCircular = (url: string) => urlStack.includes(url) @@ -91,7 +110,7 @@ async function instantiateModule( const ssrImport = async (dep: string) => { if (dep[0] !== '.' && dep[0] !== '/') { - return nodeRequire(dep, mod.file, server.config.root) + return nodeRequire(dep, mod.file, resolveOptions) } dep = unwrapId(dep) if (!isCircular(dep) && !pendingImports.get(dep)?.some(isCircular)) { @@ -132,6 +151,7 @@ async function instantiateModule( } } + const ssrImportMeta = { url } try { // eslint-disable-next-line @typescript-eslint/no-empty-function const AsyncFunction = async function () {}.constructor as typeof Function @@ -168,10 +188,34 @@ async function instantiateModule( return Object.freeze(ssrModule) } -function nodeRequire(id: string, importer: string | null, root: string) { - const mod = require(resolve(id, importer, root)) - const defaultExport = mod.__esModule ? mod.default : mod +function nodeRequire( + id: string, + importer: string | null, + resolveOptions: InternalResolveOptions +) { + const loadModule = Module.createRequire(importer || resolveOptions.root + '/') + const unhookNodeResolve = hookNodeResolve( + (nodeResolve) => (id, parent, isMain, options) => { + if (id[0] === '.' || Module.builtinModules.includes(id)) { + return nodeResolve(id, parent, isMain, options) + } + const resolved = tryNodeResolve(id, parent.id, resolveOptions, false) + if (!resolved) { + throw Error(`Cannot find module '${id}' imported from '${parent.id}'`) + } + return resolved.id + } + ) + + let mod: any + try { + mod = loadModule(id) + } finally { + unhookNodeResolve() + } + // rollup-style default import interop for cjs + const defaultExport = mod.__esModule ? mod.default : mod return new Proxy(mod, { get(mod, prop) { if (prop === 'default') return defaultExport @@ -179,20 +223,3 @@ function nodeRequire(id: string, importer: string | null, root: string) { } }) } - -const resolveCache = new Map() - -function resolve(id: string, importer: string | null, root: string) { - const key = id + importer + root - const cached = resolveCache.get(key) - if (cached) { - return cached - } - const resolveDir = - importer && fs.existsSync(cleanUrl(importer)) - ? path.dirname(importer) - : root - const resolved = resolveFrom(id, resolveDir, true) - resolveCache.set(key, resolved) - return resolved -}