Skip to content

Commit 2c2d521

Browse files
feat: add the builtins environment resolve (#18584)
Co-authored-by: 翠 / green <green@sapphi.red>
1 parent b84498b commit 2c2d521

File tree

8 files changed

+254
-60
lines changed

8 files changed

+254
-60
lines changed

packages/vite/src/node/__tests__/resolve.spec.ts

+132-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { join } from 'node:path'
22
import { describe, expect, onTestFinished, test } from 'vitest'
33
import { createServer } from '../server'
44
import { createServerModuleRunner } from '../ssr/runtime/serverModuleRunner'
5-
import type { InlineConfig } from '../config'
5+
import type { EnvironmentOptions, InlineConfig } from '../config'
66
import { build } from '../build'
77

88
describe('import and resolveId', () => {
@@ -116,6 +116,137 @@ describe('file url', () => {
116116
expect(mod4.default).toBe(mod)
117117
})
118118

119+
describe('environment builtins', () => {
120+
function getConfig(
121+
targetEnv: 'client' | 'ssr' | string,
122+
builtins: NonNullable<EnvironmentOptions['resolve']>['builtins'],
123+
): InlineConfig {
124+
return {
125+
configFile: false,
126+
root: join(import.meta.dirname, 'fixtures/file-url'),
127+
logLevel: 'error',
128+
server: {
129+
middlewareMode: true,
130+
},
131+
environments: {
132+
[targetEnv]: {
133+
resolve: {
134+
builtins,
135+
},
136+
},
137+
},
138+
}
139+
}
140+
141+
async function run({
142+
builtins,
143+
targetEnv = 'custom',
144+
testEnv = 'custom',
145+
idToResolve,
146+
}: {
147+
builtins?: NonNullable<EnvironmentOptions['resolve']>['builtins']
148+
targetEnv?: 'client' | 'ssr' | string
149+
testEnv?: 'client' | 'ssr' | string
150+
idToResolve: string
151+
}) {
152+
const server = await createServer(getConfig(targetEnv, builtins))
153+
onTestFinished(() => server.close())
154+
155+
return server.environments[testEnv]?.pluginContainer.resolveId(
156+
idToResolve,
157+
)
158+
}
159+
160+
test('declared builtin string', async () => {
161+
const resolved = await run({
162+
builtins: ['my-env:custom-builtin'],
163+
idToResolve: 'my-env:custom-builtin',
164+
})
165+
expect(resolved?.external).toBe(true)
166+
})
167+
168+
test('declared builtin regexp', async () => {
169+
const resolved = await run({
170+
builtins: [/^my-env:\w/],
171+
idToResolve: 'my-env:custom-builtin',
172+
})
173+
expect(resolved?.external).toBe(true)
174+
})
175+
176+
test('non declared builtin', async () => {
177+
const resolved = await run({
178+
builtins: [
179+
/* empty */
180+
],
181+
idToResolve: 'my-env:custom-builtin',
182+
})
183+
expect(resolved).toBeNull()
184+
})
185+
186+
test('non declared node builtin', async () => {
187+
await expect(
188+
run({
189+
builtins: [
190+
/* empty */
191+
],
192+
idToResolve: 'node:fs',
193+
}),
194+
).rejects.toThrowError(
195+
/Automatically externalized node built-in module "node:fs"/,
196+
)
197+
})
198+
199+
test('default to node-like builtins', async () => {
200+
const resolved = await run({
201+
idToResolve: 'node:fs',
202+
})
203+
expect(resolved?.external).toBe(true)
204+
})
205+
206+
test('default to node-like builtins for ssr environment', async () => {
207+
const resolved = await run({
208+
idToResolve: 'node:fs',
209+
testEnv: 'ssr',
210+
})
211+
expect(resolved?.external).toBe(true)
212+
})
213+
214+
test('no default to node-like builtins for client environment', async () => {
215+
const resolved = await run({
216+
idToResolve: 'node:fs',
217+
testEnv: 'client',
218+
})
219+
expect(resolved?.id).toEqual('__vite-browser-external:node:fs')
220+
})
221+
222+
test('no builtins overriding for client environment', async () => {
223+
const resolved = await run({
224+
idToResolve: 'node:fs',
225+
testEnv: 'client',
226+
targetEnv: 'client',
227+
})
228+
expect(resolved?.id).toEqual('__vite-browser-external:node:fs')
229+
})
230+
231+
test('declared node builtin', async () => {
232+
const resolved = await run({
233+
builtins: [/^node:/],
234+
idToResolve: 'node:fs',
235+
})
236+
expect(resolved?.external).toBe(true)
237+
})
238+
239+
test('declared builtin string in different environment', async () => {
240+
const resolved = await run({
241+
builtins: ['my-env:custom-builtin'],
242+
idToResolve: 'my-env:custom-builtin',
243+
targetEnv: 'custom',
244+
testEnv: 'ssr',
245+
})
246+
expect(resolved).toBe(null)
247+
})
248+
})
249+
119250
test('build', async () => {
120251
await build({
121252
...getConfig(),

packages/vite/src/node/config.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,17 @@ import {
6363
asyncFlatten,
6464
createDebugger,
6565
createFilter,
66-
isBuiltin,
6766
isExternalUrl,
6867
isFilePathESM,
6968
isInNodeModules,
7069
isNodeBuiltin,
70+
isNodeLikeBuiltin,
7171
isObject,
7272
isParentDirectory,
7373
mergeAlias,
7474
mergeConfig,
7575
mergeWithDefaults,
76+
nodeLikeBuiltins,
7677
normalizeAlias,
7778
normalizePath,
7879
} from './utils'
@@ -919,7 +920,11 @@ function resolveEnvironmentResolveOptions(
919920
isSsrTargetWebworkerEnvironment
920921
? DEFAULT_CLIENT_CONDITIONS
921922
: DEFAULT_SERVER_CONDITIONS.filter((c) => c !== 'browser'),
922-
enableBuiltinNoExternalCheck: !!isSsrTargetWebworkerEnvironment,
923+
builtins:
924+
resolve?.builtins ??
925+
(consumer === 'server' && !isSsrTargetWebworkerEnvironment
926+
? nodeLikeBuiltins
927+
: []),
923928
},
924929
resolve ?? {},
925930
)
@@ -1837,6 +1842,7 @@ async function bundleConfigFile(
18371842
preserveSymlinks: false,
18381843
packageCache,
18391844
isRequire,
1845+
builtins: nodeLikeBuiltins,
18401846
})?.id
18411847
}
18421848

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

packages/vite/src/node/external.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ function createIsExternal(
155155
}
156156
let isExternal = false
157157
if (id[0] !== '.' && !path.isAbsolute(id)) {
158-
isExternal = isBuiltin(id) || isConfiguredAsExternal(id, importer)
158+
isExternal =
159+
isBuiltin(environment.config.resolve.builtins, id) ||
160+
isConfiguredAsExternal(id, importer)
159161
}
160162
processedIds.set(id, isExternal)
161163
return isExternal

packages/vite/src/node/optimizer/esbuildDepPlugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export function esbuildDepPlugin(
115115
namespace: 'optional-peer-dep',
116116
}
117117
}
118-
if (environment.config.consumer === 'server' && isBuiltin(resolved)) {
118+
if (isBuiltin(environment.config.resolve.builtins, resolved)) {
119119
return
120120
}
121121
if (isExternalUrl(resolved)) {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
520520
if (shouldExternalize(environment, specifier, importer)) {
521521
return
522522
}
523-
if (isBuiltin(specifier)) {
523+
if (isBuiltin(environment.config.resolve.builtins, specifier)) {
524524
return
525525
}
526526
}

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

+66-46
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
isDataUrl,
2626
isExternalUrl,
2727
isInNodeModules,
28+
isNodeLikeBuiltin,
2829
isNonDriveRelativeAbsolutePath,
2930
isObject,
3031
isOptimizable,
@@ -97,9 +98,9 @@ export interface EnvironmentResolveOptions {
9798
*/
9899
external?: string[] | true
99100
/**
100-
* @internal
101+
* Array of strings or regular expressions that indicate what modules are builtin for the environment.
101102
*/
102-
enableBuiltinNoExternalCheck?: boolean
103+
builtins?: (string | RegExp)[]
103104
}
104105

105106
export interface ResolveOptions extends EnvironmentResolveOptions {
@@ -173,11 +174,8 @@ interface ResolvePluginOptions {
173174
}
174175

175176
export interface InternalResolveOptions
176-
extends Required<Omit<ResolveOptions, 'enableBuiltinNoExternalCheck'>>,
177-
ResolvePluginOptions {
178-
/** @internal this is always optional for backward compat */
179-
enableBuiltinNoExternalCheck?: boolean
180-
}
177+
extends Required<ResolveOptions>,
178+
ResolvePluginOptions {}
181179

182180
// Defined ResolveOptions are used to overwrite the values for all environments
183181
// It is used when creating custom resolvers (for CSS, scanning, etc)
@@ -422,47 +420,67 @@ export function resolvePlugin(
422420
return res
423421
}
424422

425-
// node built-ins.
426-
// externalize if building for a node compatible environment, otherwise redirect to empty module
427-
if (isBuiltin(id)) {
428-
if (currentEnvironmentOptions.consumer === 'server') {
429-
if (
430-
options.enableBuiltinNoExternalCheck &&
431-
options.noExternal === true &&
432-
// if both noExternal and external are true, noExternal will take the higher priority and bundle it.
433-
// only if the id is explicitly listed in external, we will externalize it and skip this error.
434-
(options.external === true || !options.external.includes(id))
435-
) {
436-
let message = `Cannot bundle Node.js built-in "${id}"`
437-
if (importer) {
438-
message += ` imported from "${path.relative(
439-
process.cwd(),
440-
importer,
441-
)}"`
442-
}
443-
message += `. Consider disabling environments.${this.environment.name}.noExternal or remove the built-in dependency.`
444-
this.error(message)
423+
// built-ins
424+
// externalize if building for a server environment, otherwise redirect to an empty module
425+
if (
426+
currentEnvironmentOptions.consumer === 'server' &&
427+
isBuiltin(options.builtins, id)
428+
) {
429+
return options.idOnly
430+
? id
431+
: { id, external: true, moduleSideEffects: false }
432+
} else if (
433+
currentEnvironmentOptions.consumer === 'server' &&
434+
isNodeLikeBuiltin(id)
435+
) {
436+
if (!(options.external === true || options.external.includes(id))) {
437+
let message = `Automatically externalized node built-in module "${id}"`
438+
if (importer) {
439+
message += ` imported from "${path.relative(
440+
process.cwd(),
441+
importer,
442+
)}"`
445443
}
444+
message += `. Consider adding it to environments.${this.environment.name}.external if it is intended.`
445+
this.error(message)
446+
}
446447

447-
return options.idOnly
448-
? id
449-
: { id, external: true, moduleSideEffects: false }
450-
} else {
451-
if (!asSrc) {
452-
debug?.(
453-
`externalized node built-in "${id}" to empty module. ` +
454-
`(imported by: ${colors.white(colors.dim(importer))})`,
455-
)
456-
} else if (isProduction) {
457-
this.warn(
458-
`Module "${id}" has been externalized for browser compatibility, imported by "${importer}". ` +
459-
`See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.`,
460-
)
448+
return options.idOnly
449+
? id
450+
: { id, external: true, moduleSideEffects: false }
451+
} else if (
452+
currentEnvironmentOptions.consumer === 'client' &&
453+
isNodeLikeBuiltin(id)
454+
) {
455+
if (
456+
options.noExternal === true &&
457+
// if both noExternal and external are true, noExternal will take the higher priority and bundle it.
458+
// only if the id is explicitly listed in external, we will externalize it and skip this error.
459+
(options.external === true || !options.external.includes(id))
460+
) {
461+
let message = `Cannot bundle built-in module "${id}"`
462+
if (importer) {
463+
message += ` imported from "${path.relative(
464+
process.cwd(),
465+
importer,
466+
)}"`
461467
}
462-
return isProduction
463-
? browserExternalId
464-
: `${browserExternalId}:${id}`
468+
message += `. Consider disabling environments.${this.environment.name}.noExternal or remove the built-in dependency.`
469+
this.error(message)
470+
}
471+
472+
if (!asSrc) {
473+
debug?.(
474+
`externalized node built-in "${id}" to empty module. ` +
475+
`(imported by: ${colors.white(colors.dim(importer))})`,
476+
)
477+
} else if (isProduction) {
478+
this.warn(
479+
`Module "${id}" has been externalized for browser compatibility, imported by "${importer}". ` +
480+
`See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.`,
481+
)
465482
}
483+
return isProduction ? browserExternalId : `${browserExternalId}:${id}`
466484
}
467485
}
468486

@@ -720,8 +738,10 @@ export function tryNodeResolve(
720738
basedir = root
721739
}
722740

741+
const isModuleBuiltin = (id: string) => isBuiltin(options.builtins, id)
742+
723743
let selfPkg = null
724-
if (!isBuiltin(id) && !id.includes('\0') && bareImportRE.test(id)) {
744+
if (!isModuleBuiltin(id) && !id.includes('\0') && bareImportRE.test(id)) {
725745
// check if it's a self reference dep.
726746
const selfPackageData = findNearestPackageData(basedir, packageCache)
727747
selfPkg =
@@ -738,7 +758,7 @@ export function tryNodeResolve(
738758
// if so, we can resolve to a special id that errors only when imported.
739759
if (
740760
basedir !== root && // root has no peer dep
741-
!isBuiltin(id) &&
761+
!isModuleBuiltin(id) &&
742762
!id.includes('\0') &&
743763
bareImportRE.test(id)
744764
) {

0 commit comments

Comments
 (0)