Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 3 additions & 3 deletions docs/config/ssr-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ Unless noted, the options in this section are applied to both dev and build.

## ssr.external

- **Type:** `string[] | true`
- **Type:** `string | RegExp | (string | RegExp)[] | true`
- **Related:** [SSR Externals](/guide/ssr#ssr-externals)

Externalize the given dependencies and their transitive dependencies for SSR. By default, all dependencies are externalized except for linked dependencies (for HMR). If you prefer to externalize the linked dependency, you can pass its name to this option.

If `true`, all dependencies including linked dependencies are externalized.

Note that the explicitly listed dependencies (using `string[]` type) will always take priority if they're also listed in `ssr.noExternal` (using any type).
Note that explicitly listed dependencies (strings or regular expressions) will always take priority if they're also listed in `ssr.noExternal` (using any type).

## ssr.noExternal

Expand All @@ -20,7 +20,7 @@ Note that the explicitly listed dependencies (using `string[]` type) will always

Prevent listed dependencies from being externalized for SSR, which they will get bundled in build. By default, only linked dependencies are not externalized (for HMR). If you prefer to externalize the linked dependency, you can pass its name to the `ssr.external` option.

If `true`, no dependencies are externalized. However, dependencies explicitly listed in `ssr.external` (using `string[]` type) can take priority and still be externalized. If `ssr.target: 'node'` is set, Node.js built-ins will also be externalized by default.
If `true`, no dependencies are externalized. However, dependencies explicitly listed in `ssr.external` (strings or regular expressions) can take priority and still be externalized. If `ssr.target: 'node'` is set, Node.js built-ins will also be externalized by default.

Note that if both `ssr.noExternal: true` and `ssr.external: true` are configured, `ssr.noExternal` takes priority and no dependencies are externalized.

Expand Down
78 changes: 46 additions & 32 deletions packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import {
type EnvironmentResolveOptions,
type InternalResolveOptions,
type ResolveOptions,
normalizeExternalOption,
tryNodeResolve,
} from './plugins/resolve'
import type { LogLevel, Logger } from './logger'
Expand Down Expand Up @@ -247,7 +248,13 @@ type AllResolveOptions = ResolveOptions & {
alias?: AliasOptions
}

type ResolvedAllResolveOptions = Required<ResolveOptions> & { alias: Alias[] }
export type ResolvedResolveOptions = Required<
Omit<ResolveOptions, 'external'>
> & {
external: true | (string | RegExp)[]
}

type ResolvedAllResolveOptions = ResolvedResolveOptions & { alias: Alias[] }

export interface SharedEnvironmentOptions {
/**
Expand Down Expand Up @@ -286,8 +293,6 @@ export interface EnvironmentOptions extends SharedEnvironmentOptions {
build?: BuildEnvironmentOptions
}

export type ResolvedResolveOptions = Required<ResolveOptions>

export type ResolvedEnvironmentOptions = {
define?: Record<string, any>
resolve: ResolvedResolveOptions
Expand Down Expand Up @@ -585,7 +590,7 @@ export interface ResolvedConfig
isProduction: boolean
envDir: string | false
env: Record<string, any>
resolve: Required<ResolveOptions> & {
resolve: ResolvedResolveOptions & {
alias: Alias[]
}
plugins: readonly Plugin[]
Expand Down Expand Up @@ -662,7 +667,7 @@ export const configDefaults = Object.freeze({
dedupe: [],
/** @experimental */
noExternal: [],
external: [],
external: [] as (string | RegExp)[],
preserveSymlinks: false,
alias: [],
},
Expand Down Expand Up @@ -940,33 +945,42 @@ function resolveEnvironmentResolveOptions(
// Backward compatibility
isSsrTargetWebworkerEnvironment?: boolean,
): ResolvedAllResolveOptions {
const resolvedResolve: ResolvedAllResolveOptions = mergeWithDefaults(
{
...configDefaults.resolve,
mainFields:
consumer === undefined ||
consumer === 'client' ||
isSsrTargetWebworkerEnvironment
? DEFAULT_CLIENT_MAIN_FIELDS
: DEFAULT_SERVER_MAIN_FIELDS,
conditions:
consumer === undefined ||
consumer === 'client' ||
isSsrTargetWebworkerEnvironment
? DEFAULT_CLIENT_CONDITIONS
: DEFAULT_SERVER_CONDITIONS.filter((c) => c !== 'browser'),
builtins:
resolve?.builtins ??
(consumer === 'server'
? isSsrTargetWebworkerEnvironment && resolve?.noExternal === true
? []
: nodeLikeBuiltins
: []),
},
resolve ?? {},
)
resolvedResolve.preserveSymlinks = preserveSymlinks
resolvedResolve.alias = alias
const defaults: ResolvedResolveOptions = {
...configDefaults.resolve,
mainFields:
consumer === undefined ||
consumer === 'client' ||
isSsrTargetWebworkerEnvironment
? Array.from(DEFAULT_CLIENT_MAIN_FIELDS)
: Array.from(DEFAULT_SERVER_MAIN_FIELDS),
conditions:
consumer === undefined ||
consumer === 'client' ||
isSsrTargetWebworkerEnvironment
? Array.from(DEFAULT_CLIENT_CONDITIONS)
: DEFAULT_SERVER_CONDITIONS.filter((c) => c !== 'browser'),
builtins:
resolve?.builtins ??
(consumer === 'server'
? isSsrTargetWebworkerEnvironment && resolve?.noExternal === true
? []
: nodeLikeBuiltins
: []),
preserveSymlinks,
external: normalizeExternalOption(configDefaults.resolve.external),
}

const mergedResolve = mergeWithDefaults<
ResolvedResolveOptions,
EnvironmentResolveOptions
>(defaults, resolve ?? {})

const resolvedResolve: ResolvedAllResolveOptions = {
...mergedResolve,
external: normalizeExternalOption(mergedResolve.external),
preserveSymlinks,
alias,
}

if (
// @ts-expect-error removed field
Expand Down
68 changes: 55 additions & 13 deletions packages/vite/src/node/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,33 @@ const isExternalCache = new WeakMap<
(id: string, importer?: string) => boolean
>()

type ExternalList = Exclude<InternalResolveOptions['external'], true>

function resetAndTestRegExp(regexp: RegExp, value: string): boolean {
regexp.lastIndex = 0
return regexp.test(value)
}

function matchesExternalList(list: ExternalList, value: string): boolean {
for (const pattern of list) {
if (typeof pattern === 'string') {
if (pattern === value) {
return true
}
} else if (resetAndTestRegExp(pattern, value)) {
return true
}
}
return false
}

export function isIdExplicitlyExternal(
external: InternalResolveOptions['external'],
id: string,
): boolean {
return external === true ? true : matchesExternalList(external, id)
}

export function shouldExternalize(
environment: Environment,
id: string,
Expand All @@ -38,6 +65,8 @@ export function createIsConfiguredAsExternal(
const { config } = environment
const { root, resolve } = config
const { external, noExternal } = resolve
const externalList: ExternalList | undefined =
external === true ? undefined : external
const noExternalFilter =
typeof noExternal !== 'boolean' &&
!(Array.isArray(noExternal) && noExternal.length === 0) &&
Expand Down Expand Up @@ -92,25 +121,38 @@ export function createIsConfiguredAsExternal(
// Returns true if it is configured as external, false if it is filtered
// by noExternal and undefined if it isn't affected by the explicit config
return (id: string, importer?: string) => {
if (
// If this id is defined as external, force it as external
// Note that individual package entries are allowed in `external`
external !== true &&
external.includes(id)
) {
const explicitIdMatch =
externalList && matchesExternalList(externalList, id)
if (explicitIdMatch) {
const canExternalize = isExternalizable(id, importer, true)
if (!canExternalize) {
debug?.(
`Configured ${JSON.stringify(
id,
)} as external but failed to statically resolve it. ` +
`Falling back to honoring the explicit configuration.`,
)
}
return true
}
const pkgName = getNpmPackageName(id)
if (!pkgName) {
return isExternalizable(id, importer, false)
}
if (
// A package name in ssr.external externalizes every
// externalizable package entry
external !== true &&
external.includes(pkgName)
) {
return isExternalizable(id, importer, true)
const explicitPackageMatch =
externalList && matchesExternalList(externalList, pkgName)
if (explicitPackageMatch) {
const canExternalize = isExternalizable(id, importer, true)
if (!canExternalize) {
debug?.(
`Configured package ${JSON.stringify(
pkgName,
)} as external but failed to statically resolve ${JSON.stringify(
id,
)}. Falling back to honoring the explicit configuration.`,
)
}
return true
}
if (typeof noExternal === 'boolean') {
return !noExternal
Expand Down
49 changes: 42 additions & 7 deletions packages/vite/src/node/plugins/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ import {
import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer'
import type { DepsOptimizer } from '../optimizer'
import type { PackageCache, PackageData } from '../packages'
import { canExternalizeFile, shouldExternalize } from '../external'
import {
canExternalizeFile,
isIdExplicitlyExternal,
shouldExternalize,
} from '../external'
import {
findNearestMainPackageData,
findNearestPackageData,
Expand Down Expand Up @@ -72,6 +76,18 @@ const debug = createDebugger('vite:resolve-details', {
onlyWhenFocused: true,
})

export function normalizeExternalOption(
external: string | RegExp | (string | RegExp)[] | true | undefined,
): true | (string | RegExp)[] {
if (external === true) {
return true
}
if (!external) {
return []
}
return Array.isArray(external) ? external : [external]
}

export interface EnvironmentResolveOptions {
/**
* @default ['browser', 'module', 'jsnext:main', 'jsnext']
Expand All @@ -96,7 +112,7 @@ export interface EnvironmentResolveOptions {
* Only works in server environments for now. Previously this was `ssr.external`.
* @experimental
*/
external?: string[] | true
external?: string | RegExp | (string | RegExp)[] | true
/**
* Array of strings or regular expressions that indicate what modules are builtin for the environment.
*/
Expand Down Expand Up @@ -150,8 +166,10 @@ interface ResolvePluginOptions {
}

export interface InternalResolveOptions
extends Required<ResolveOptions>,
ResolvePluginOptions {}
extends Required<Omit<ResolveOptions, 'external'>>,
ResolvePluginOptions {
external: true | (string | RegExp)[]
}

// Defined ResolveOptions are used to overwrite the values for all environments
// It is used when creating custom resolvers (for CSS, scanning, etc)
Expand All @@ -162,6 +180,8 @@ export interface ResolvePluginOptionsWithOverrides
export function resolvePlugin(
resolveOptions: ResolvePluginOptionsWithOverrides,
): Plugin {
const { external: pluginExternal, ...resolveOptionsWithoutExternal } =
resolveOptions
const { root, isProduction, asSrc, preferRelative = false } = resolveOptions

// In unix systems, absolute paths inside root first needs to be checked as an
Expand Down Expand Up @@ -201,8 +221,12 @@ export function resolvePlugin(
const options: InternalResolveOptions = {
isRequire,
...currentEnvironmentOptions.resolve,
...resolveOptions, // plugin options + resolve options overrides
...resolveOptionsWithoutExternal, // plugin options + resolve options overrides
scan: resolveOpts.scan ?? resolveOptions.scan,
external:
pluginExternal === undefined
? currentEnvironmentOptions.resolve.external
: normalizeExternalOption(pluginExternal),
}

const resolvedImports = resolveSubpathImports(id, importer, options)
Expand Down Expand Up @@ -390,6 +414,17 @@ export function resolvePlugin(
return res
}

// For modules that should be externalized but couldn't be resolved by tryNodeResolve,
// we externalize them directly. However, we need to let built-ins go through their
// dedicated handling below to ensure proper moduleSideEffects setting.
if (
external &&
!isBuiltin(options.builtins, id) &&
!isNodeLikeBuiltin(id)
) {
return options.idOnly ? id : { id, external: true }
}

// built-ins
// externalize if building for a server environment, otherwise redirect to an empty module
if (
Expand All @@ -403,7 +438,7 @@ export function resolvePlugin(
currentEnvironmentOptions.consumer === 'server' &&
isNodeLikeBuiltin(id)
) {
if (!(options.external === true || options.external.includes(id))) {
if (!isIdExplicitlyExternal(options.external, id)) {
let message = `Automatically externalized node built-in module "${id}"`
if (importer) {
message += ` imported from "${path.relative(
Expand All @@ -426,7 +461,7 @@ export function resolvePlugin(
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))
!isIdExplicitlyExternal(options.external, id)
) {
let message = `Cannot bundle built-in module "${id}"`
if (importer) {
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/ssr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type SsrDepOptimizationConfig = DepOptimizationConfig

export interface SSROptions {
noExternal?: string | RegExp | (string | RegExp)[] | true
external?: string[] | true
external?: string | RegExp | (string | RegExp)[] | true

/**
* Define the target for the ssr build. The browser field in package.json
Expand Down