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

feat: add the builtins environment resolve #18584

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
13 changes: 11 additions & 2 deletions packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,16 @@ import {
asyncFlatten,
createDebugger,
createFilter,
isBuiltin,
isExternalUrl,
isFilePathESM,
isInNodeModules,
isNodeBuiltin,
isNodeLikeBuiltin,
isObject,
isParentDirectory,
mergeAlias,
mergeConfig,
nodeLikeBuiltins,
normalizeAlias,
normalizePath,
} from './utils'
Expand Down Expand Up @@ -781,6 +782,13 @@ function resolveEnvironmentResolveOptions(
dedupe: resolve?.dedupe ?? [],
preserveSymlinks,
alias,
builtins:
resolve?.builtins ??
(consumer === 'server'
? nodeLikeBuiltins
: [
// there are not built-in modules in the browser
]),
}

if (
Expand Down Expand Up @@ -1632,6 +1640,7 @@ async function bundleConfigFile(
preserveSymlinks: false,
packageCache,
isRequire,
builtins: nodeLikeBuiltins,
})?.id
}

Expand All @@ -1650,7 +1659,7 @@ async function bundleConfigFile(
// With the `isNodeBuiltin` check above, this check captures if the builtin is a
// non-node built-in, which esbuild doesn't know how to handle. In that case, we
// externalize it so the non-node runtime handles it instead.
if (isBuiltin(id)) {
if (isNodeLikeBuiltin(id)) {
return { external: true }
}

Expand Down
5 changes: 4 additions & 1 deletion packages/vite/src/node/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export function createIsConfiguredAsExternal(
// Allow linked packages to be externalized if they are explicitly
// configured as external
!!configuredAsExternal,
environment.config,
)?.external
} catch {
debug?.(
Expand Down Expand Up @@ -150,7 +151,9 @@ function createIsExternal(
}
let isExternal = false
if (id[0] !== '.' && !path.isAbsolute(id)) {
isExternal = isBuiltin(id) || isConfiguredAsExternal(id, importer)
isExternal =
isBuiltin(environment.config.resolve.builtins, id) ||
isConfiguredAsExternal(id, importer)
}
processedIds.set(id, isExternal)
return isExternal
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/optimizer/esbuildDepPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function esbuildDepPlugin(
namespace: 'optional-peer-dep',
}
}
if (environment.config.consumer === 'server' && isBuiltin(resolved)) {
if (isBuiltin(environment.config.resolve.builtins, resolved)) {
return
}
Copy link
Member

Choose a reason for hiding this comment

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

I guess this does not work. cloudflare:* will be processed by esbuild and IIRC esbuild does not externalize them automatically and then it throws Could not resolve "cloudflare:*" error. I guess it needs to be return { path: resolved, external: true }.
But I wonder if we should set external: true for anything that was externalized by rollup plugins or resolve.external instead of checking isBuiltin here.

if (isExternalUrl(resolved)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
if (shouldExternalize(environment, specifier, importer)) {
return
}
if (isBuiltin(specifier)) {
if (isBuiltin(environment.config.resolve.builtins, specifier)) {
return
}
}
Expand Down
104 changes: 63 additions & 41 deletions packages/vite/src/node/plugins/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,19 @@ import {
isDataUrl,
isExternalUrl,
isInNodeModules,
isNodeLikeBuiltin,
isNonDriveRelativeAbsolutePath,
isObject,
isOptimizable,
isTsRequest,
nodeLikeBuiltins,
normalizePath,
safeRealpathSync,
tryStatSync,
} from '../utils'
import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer'
import type { DepsOptimizer } from '../optimizer'
import type { SSROptions } from '..'
import type { EnvironmentOptions, SSROptions } from '..'
import type { PackageCache, PackageData } from '../packages'
import { shouldExternalize } from '../external'
import {
Expand Down Expand Up @@ -96,6 +98,10 @@ export interface EnvironmentResolveOptions {
* @experimental
*/
external?: string[] | true
/**
* Array of strings or regular expressions that indicate what modules are builtin for the environment.
*/
builtins?: (string | RegExp)[]
}

export interface ResolveOptions extends EnvironmentResolveOptions {
Expand Down Expand Up @@ -313,7 +319,13 @@ export function resolvePlugin(

if (
options.mainFields.includes('browser') &&
(res = tryResolveBrowserMapping(fsPath, importer, options, true))
(res = tryResolveBrowserMapping(
fsPath,
importer,
options,
true,
this.environment.config,
))
) {
return res
}
Expand Down Expand Up @@ -403,6 +415,7 @@ export function resolvePlugin(
importer,
options,
false,
this.environment.config,
external,
))
) {
Expand All @@ -417,51 +430,54 @@ export function resolvePlugin(
depsOptimizer,
external,
undefined,
this.environment.config,
))
) {
return res
}

// node built-ins.
// externalize if building for a node compatible environment, otherwise redirect to empty module
if (isBuiltin(id)) {
if (currentEnvironmentOptions.consumer === 'server') {
if (
options.noExternal === true &&
// if both noExternal and external are true, noExternal will take the higher priority and bundle it.
// only if the id is explicitly listed in external, we will externalize it and skip this error.
(options.external === true || !options.external.includes(id))
) {
let message = `Cannot bundle Node.js built-in "${id}"`
if (importer) {
message += ` imported from "${path.relative(
process.cwd(),
importer,
)}"`
}
message += `. Consider disabling environments.${this.environment.name}.noExternal or remove the built-in dependency.`
this.error(message)
// built-ins
// externalize if building for a server environment, otherwise redirect to an empty module
if (
currentEnvironmentOptions.consumer === 'server' &&
isBuiltin(this.environment.config.resolve.builtins, id)
) {
if (
options.noExternal === true &&
// if both noExternal and external are true, noExternal will take the higher priority and bundle it.
// only if the id is explicitly listed in external, we will externalize it and skip this error.
(options.external === true || !options.external.includes(id))
) {
let message = `Cannot bundle built-in module "${id}"`
if (importer) {
message += ` imported from "${path.relative(
process.cwd(),
importer,
)}"`
}
message += `. Consider disabling environments.${this.environment.name}.noExternal or remove the built-in dependency.`
this.error(message)
}

return options.idOnly
? id
: { id, external: true, moduleSideEffects: false }
} else {
if (!asSrc) {
debug?.(
`externalized node built-in "${id}" to empty module. ` +
`(imported by: ${colors.white(colors.dim(importer))})`,
)
} else if (isProduction) {
this.warn(
`Module "${id}" has been externalized for browser compatibility, imported by "${importer}". ` +
`See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.`,
)
}
return isProduction
? browserExternalId
: `${browserExternalId}:${id}`
return options.idOnly
? id
: { id, external: true, moduleSideEffects: false }
} else if (
currentEnvironmentOptions.consumer === 'client' &&
isNodeLikeBuiltin(id)
) {
if (!asSrc) {
debug?.(
`externalized node built-in "${id}" to empty module. ` +
`(imported by: ${colors.white(colors.dim(importer))})`,
)
} else if (isProduction) {
this.warn(
`Module "${id}" has been externalized for browser compatibility, imported by "${importer}". ` +
`See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.`,
)
}
return isProduction ? browserExternalId : `${browserExternalId}:${id}`
}
}

Expand Down Expand Up @@ -697,6 +713,7 @@ export function tryNodeResolve(
depsOptimizer?: DepsOptimizer,
externalize?: boolean,
allowLinkedExternal: boolean = true,
environmentOptions?: EnvironmentOptions,
): PartialResolvedId | undefined {
const { root, dedupe, isBuild, preserveSymlinks, packageCache } = options

Expand All @@ -720,8 +737,11 @@ export function tryNodeResolve(
basedir = root
}

const isModuleBuiltin = (id: string) =>
isBuiltin(environmentOptions?.resolve?.builtins ?? nodeLikeBuiltins, id)

let selfPkg = null
if (!isBuiltin(id) && !id.includes('\0') && bareImportRE.test(id)) {
if (!isModuleBuiltin(id) && !id.includes('\0') && bareImportRE.test(id)) {
// check if it's a self reference dep.
const selfPackageData = findNearestPackageData(basedir, packageCache)
selfPkg =
Expand All @@ -738,7 +758,7 @@ export function tryNodeResolve(
// if so, we can resolve to a special id that errors only when imported.
if (
basedir !== root && // root has no peer dep
!isBuiltin(id) &&
!isModuleBuiltin(id) &&
!id.includes('\0') &&
bareImportRE.test(id)
) {
Expand Down Expand Up @@ -1088,6 +1108,7 @@ function tryResolveBrowserMapping(
importer: string | undefined,
options: InternalResolveOptions,
isFilePath: boolean,
environmentOptions: EnvironmentOptions,
externalize?: boolean,
) {
let res: string | undefined
Expand All @@ -1107,6 +1128,7 @@ function tryResolveBrowserMapping(
undefined,
undefined,
undefined,
environmentOptions,
)?.id
: tryFsResolve(path.join(pkg.dir, browserMappedPath), options))
) {
Expand Down
7 changes: 5 additions & 2 deletions packages/vite/src/node/ssr/fetchModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ export async function fetchModule(
importer?: string,
options: FetchModuleOptions = {},
): Promise<FetchResult> {
// builtins should always be externalized
if (url.startsWith('data:') || isBuiltin(url)) {
if (
url.startsWith('data:') ||
isBuiltin(environment.config.resolve.builtins, url)
) {
return { externalize: url, type: 'builtin' }
}

Expand Down Expand Up @@ -57,6 +59,7 @@ export async function fetchModule(
isProduction,
root,
packageCache: environment.config.packageCache,
builtins: environment.config.resolve.builtins,
})
if (!resolved) {
const err: any = new Error(
Expand Down
42 changes: 37 additions & 5 deletions packages/vite/src/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,43 @@ const BUN_BUILTIN_NAMESPACE = 'bun:'
// Some runtimes like Bun injects namespaced modules here, which is not a node builtin
const nodeBuiltins = builtinModules.filter((id) => !id.includes(':'))

// TODO: Use `isBuiltin` from `node:module`, but Deno doesn't support it
export function isBuiltin(id: string): boolean {
if (process.versions.deno && id.startsWith(NPM_BUILTIN_NAMESPACE)) return true
if (process.versions.bun && id.startsWith(BUN_BUILTIN_NAMESPACE)) return true
return isNodeBuiltin(id)
const isBuiltinCache = new WeakMap<
(string | RegExp)[],
(id: string, importer?: string) => boolean
>()

export function isBuiltin(builtins: (string | RegExp)[], id: string): boolean {
let isBuiltin = isBuiltinCache.get(builtins)
if (!isBuiltin) {
isBuiltin = createIsBuiltin(builtins)
isBuiltinCache.set(builtins, isBuiltin)
}
return isBuiltin(id)
}

export function createIsBuiltin(
builtins: (string | RegExp)[],
): (id: string) => boolean {
const plainBuiltinsSet = new Set(
builtins.filter((builtin) => typeof builtin === 'string'),
)
const regexBuiltins = builtins.filter(
(builtin) => typeof builtin !== 'string',
)

return (id) =>
plainBuiltinsSet.has(id) || regexBuiltins.some((regexp) => regexp.test(id))
}

export const nodeLikeBuiltins = [
...nodeBuiltins,
new RegExp(`^${NODE_BUILTIN_NAMESPACE}`),
new RegExp(`^${NPM_BUILTIN_NAMESPACE}`),
new RegExp(`^${BUN_BUILTIN_NAMESPACE}`),
]

export function isNodeLikeBuiltin(id: string): boolean {
return isBuiltin(nodeLikeBuiltins, id)
}

export function isNodeBuiltin(id: string): boolean {
Expand Down
Loading