Skip to content

Commit

Permalink
fix(ssr): use tryNodeResolve instead of resolveFrom (#3951)
Browse files Browse the repository at this point in the history
  • Loading branch information
aleclarson authored Nov 11, 2021
1 parent 9d50df8 commit 87c0050
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 45 deletions.
2 changes: 2 additions & 0 deletions packages/vite/src/node/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -42,6 +43,7 @@ export async function resolvePlugins(
ssrConfig: config.ssr,
asSrc: true
}),
config.build.ssr ? ssrRequireHookPlugin(config) : null,
htmlInlineScriptProxyPlugin(config),
cssPlugin(config),
config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,
Expand Down
19 changes: 9 additions & 10 deletions packages/vite/src/node/plugins/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export interface InternalResolveOptions extends ResolveOptions {
tryPrefix?: string
skipPackageJson?: boolean
preferRelative?: boolean
preserveSymlinks?: boolean
isRequire?: boolean
// #3040
// when the importer is a ts module,
Expand Down Expand Up @@ -305,7 +306,7 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
function tryFsResolve(
fsPath: string,
options: InternalResolveOptions,
preserveSymlinks: boolean,
preserveSymlinks?: boolean,
tryIndex = true,
targetWeb = true
): string | undefined {
Expand Down Expand Up @@ -426,7 +427,7 @@ function tryResolveFile(
options: InternalResolveOptions,
tryIndex: boolean,
targetWeb: boolean,
preserveSymlinks: boolean,
preserveSymlinks?: boolean,
tryPrefix?: string,
skipPackageJson?: boolean
): string | undefined {
Expand Down Expand Up @@ -489,7 +490,7 @@ export const idToPkgMap = new Map<string, PackageData>()

export function tryNodeResolve(
id: string,
importer: string | undefined,
importer: string | null | undefined,
options: InternalResolveOptions,
targetWeb: boolean,
server?: ViteDevServer,
Expand Down Expand Up @@ -522,14 +523,12 @@ export function tryNodeResolve(
basedir = root
}

const preserveSymlinks = !!server?.config.resolve.preserveSymlinks

// nested node module, step-by-step resolve to the basedir of the nestedPath
if (nestedRoot) {
basedir = nestedResolveFrom(nestedRoot, basedir, preserveSymlinks)
basedir = nestedResolveFrom(nestedRoot, basedir, options.preserveSymlinks)
}

const pkg = resolvePackageData(pkgId, basedir, preserveSymlinks)
const pkg = resolvePackageData(pkgId, basedir, options.preserveSymlinks)

if (!pkg) {
return
Expand All @@ -541,9 +540,9 @@ export function tryNodeResolve(
pkg,
options,
targetWeb,
preserveSymlinks
options.preserveSymlinks
)
: resolvePackageEntry(id, pkg, options, targetWeb, preserveSymlinks)
: resolvePackageEntry(id, pkg, options, targetWeb, options.preserveSymlinks)
if (!resolved) {
return
}
Expand Down Expand Up @@ -876,7 +875,7 @@ function resolveDeepImport(
}: PackageData,
options: InternalResolveOptions,
targetWeb: boolean,
preserveSymlinks: boolean
preserveSymlinks?: boolean
): string | undefined {
const cache = getResolvedCache(id, targetWeb)
if (cache) {
Expand Down
69 changes: 69 additions & 0 deletions packages/vite/src/node/plugins/ssrRequireHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import MagicString from 'magic-string'
import { ResolvedConfig } from '..'
import { Plugin } from '../plugin'

/**
* This plugin hooks into Node's module resolution algorithm at runtime,
* so that SSR builds can benefit from `resolve.dedupe` like they do
* in development.
*/
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, any>
) => 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
}
}
13 changes: 10 additions & 3 deletions packages/vite/src/node/ssr/ssrExternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ export function resolveSSRExternal(
seen.add(id)
})

collectExternals(config.root, ssrExternals, seen)
collectExternals(
config.root,
config.resolve.preserveSymlinks,
ssrExternals,
seen
)

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

const resolveOptions: InternalResolveOptions = {
root,
preserveSymlinks,
isProduction: false,
isBuild: true
}
Expand Down Expand Up @@ -132,7 +139,7 @@ function collectExternals(
// or are there others like SystemJS / AMD that we'd need to handle?
// for now, we'll just leave this as is
else if (/\.m?js$/.test(esmEntry)) {
if (pkg.type === "module" || esmEntry.endsWith('.mjs')) {
if (pkg.type === 'module' || esmEntry.endsWith('.mjs')) {
ssrExternals.add(id)
continue
}
Expand All @@ -145,7 +152,7 @@ function collectExternals(
}

for (const depRoot of depsToTrace) {
collectExternals(depRoot, ssrExternals, seen)
collectExternals(depRoot, preserveSymlinks, ssrExternals, seen)
}
}

Expand Down
87 changes: 55 additions & 32 deletions packages/vite/src/node/ssr/ssrModuleLoader.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import fs from 'fs'
import path from 'path'
import { pathToFileURL } from 'url'
import { ViteDevServer } from '..'
import { ViteDevServer } from '../server'
import {
dynamicImport,
cleanUrl,
isBuiltin,
resolveFrom,
unwrapId,
usingDynamicImport
} from '../utils'
Expand All @@ -19,6 +16,8 @@ import {
ssrDynamicImportKey
} from './ssrTransform'
import { transformRequest } from '../server/transformRequest'
import { InternalResolveOptions, tryNodeResolve } from '../plugins/resolve'
import { hookNodeResolve } from '../plugins/ssrRequireHook'

interface SSRContext {
global: typeof globalThis
Expand Down Expand Up @@ -96,13 +95,32 @@ async function instantiateModule(
urlStack = urlStack.concat(url)
const isCircular = (url: string) => urlStack.includes(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
}

// Since dynamic imports can happen in parallel, we need to
// account for multiple pending deps and duplicate imports.
const pendingDeps: string[] = []

const ssrImport = async (dep: string) => {
if (dep[0] !== '.' && dep[0] !== '/') {
return nodeImport(dep, mod.file, server.config)
return nodeImport(dep, mod.file!, resolveOptions)
}
dep = unwrapId(dep)
if (!isCircular(dep) && !pendingImports.get(dep)?.some(isCircular)) {
Expand Down Expand Up @@ -185,21 +203,48 @@ async function instantiateModule(
// In node@12+ we can use dynamic import to load CJS and ESM
async function nodeImport(
id: string,
importer: string | null,
config: ViteDevServer['config']
importer: string,
resolveOptions: InternalResolveOptions
) {
// Node's module resolution is hi-jacked so Vite can ensure the
// configured `resolve.dedupe` and `mode` options are respected.
const viteResolve = (id: string, importer: string) => {
const resolved = tryNodeResolve(id, importer, resolveOptions, false)
if (!resolved) {
const err: any = new Error(
`Cannot find module '${id}' imported from '${importer}'`
)
err.code = 'ERR_MODULE_NOT_FOUND'
throw err
}
return resolved.id
}

// When an ESM module imports an ESM dependency, this hook is *not* used.
const unhookNodeResolve = hookNodeResolve(
(nodeResolve) => (id, parent, isMain, options) =>
id[0] === '.' || isBuiltin(id)
? nodeResolve(id, parent, isMain, options)
: viteResolve(id, parent.id)
)

let url: string
// `resolve` doesn't handle `node:` builtins, so handle them directly
if (id.startsWith('node:') || isBuiltin(id)) {
url = id
} else {
url = resolve(id, importer, config.root, !!config.resolve.preserveSymlinks)
url = viteResolve(id, importer)
if (usingDynamicImport) {
url = pathToFileURL(url).toString()
}
}
const mod = await dynamicImport(url)
return proxyESM(id, mod)

try {
const mod = await dynamicImport(url)
return proxyESM(id, mod)
} finally {
unhookNodeResolve()
}
}

// rollup-style default import interop for cjs
Expand All @@ -216,25 +261,3 @@ function proxyESM(id: string, mod: any) {
}
})
}

const resolveCache = new Map<string, string>()

function resolve(
id: string,
importer: string | null,
root: string,
preserveSymlinks: boolean
) {
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, preserveSymlinks, true)
resolveCache.set(key, resolved)
return resolved
}

0 comments on commit 87c0050

Please sign in to comment.