Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ssr): use tryNodeResolve instead of resolveFrom #3951

Merged
merged 2 commits into from
Nov 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To further elaborate Ben's comment about nestedResolveFrom, which would be on this line in the main branch. It's currently used to resolve optimiseDeps.include with syntax like my-lib > some-other-lib. It's probably fine to not update nestedResolveFrom as it's currently used to resolve CJS dependencies only, but yeah ideally we want to move away from it too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarifying. The change to nestedResolveFrom can be done in a future PR 👍

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
}