diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 294ff48654d301..f9bfb31a0365b9 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -398,7 +398,7 @@ async function doBuild( external = await cjsSsrResolveExternal(config, userExternal) } - if (isDepsOptimizerEnabled(config)) { + if (isDepsOptimizerEnabled(config, ssr)) { await initDepsOptimizer(config) } @@ -739,7 +739,7 @@ async function cjsSsrResolveExternal( } catch (e) {} if (!knownImports) { // no dev deps optimization data, do a fresh scan - knownImports = await findKnownImports(config) + knownImports = await findKnownImports(config, false) // needs to use non-ssr } const ssrExternals = cjsSsrResolveExternals(config, knownImports) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 3c49c3a3d413ef..779a7cdeec9e7e 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -7,7 +7,6 @@ import colors from 'picocolors' import type { Alias, AliasOptions } from 'types/alias' import aliasPlugin from '@rollup/plugin-alias' import { build } from 'esbuild' -import type { Plugin as ESBuildPlugin } from 'esbuild' import type { RollupOptions } from 'rollup' import type { Plugin } from './plugin' import type { @@ -47,7 +46,7 @@ import type { InternalResolveOptions, ResolveOptions } from './plugins/resolve' import { resolvePlugin } from './plugins/resolve' import type { LogLevel, Logger } from './logger' import { createLogger } from './logger' -import type { DepOptimizationOptions } from './optimizer' +import type { DepOptimizationConfig, DepOptimizationOptions } from './optimizer' import type { JsonOptions } from './plugins/json' import type { PluginContainer } from './server/pluginContainer' import { createPluginContainer } from './server/pluginContainer' @@ -334,7 +333,7 @@ export type ResolvedConfig = Readonly< server: ResolvedServerOptions build: ResolvedBuildOptions preview: ResolvedPreviewOptions - ssr: ResolvedSSROptions | undefined + ssr: ResolvedSSROptions assetsInclude: (file: string) => boolean logger: Logger createResolver: (options?: Partial) => ResolveFn @@ -560,15 +559,14 @@ export async function resolveConfig( : '' const server = resolveServerOptions(resolvedRoot, config.server, logger) - let ssr = resolveSSROptions(config.ssr) - if (config.legacy?.buildSsrCjsExternalHeuristics) { - if (ssr) ssr.format = 'cjs' - else ssr = { target: 'node', format: 'cjs' } - } + const ssr = resolveSSROptions( + config.ssr, + config.legacy?.buildSsrCjsExternalHeuristics, + config.resolve?.preserveSymlinks + ) const middlewareMode = config?.server?.middlewareMode - config = mergeConfig(config, externalConfigCompat(config, configEnv)) const optimizeDeps = config.optimizeDeps || {} if (process.env.VITE_TEST_LEGACY_CJS_PLUGIN) { @@ -668,6 +666,12 @@ export async function resolveConfig( } else if (optimizerDisabled === 'dev') { resolved.optimizeDeps.disabled = true // Also disabled during build } + const ssrOptimizerDisabled = resolved.ssr.optimizeDeps.disabled + if (!ssrOptimizerDisabled) { + resolved.ssr.optimizeDeps.disabled = 'build' + } else if (ssrOptimizerDisabled === 'dev') { + resolved.ssr.optimizeDeps.disabled = true // Also disabled during build + } } // Some plugins that aren't intended to work in the bundling of workers (doing post-processing at build time for example). @@ -1004,92 +1008,21 @@ async function loadConfigFromBundledFile( return raw.__esModule ? raw.default : raw } -export function isDepsOptimizerEnabled(config: ResolvedConfig): boolean { - const { command, optimizeDeps } = config - const { disabled } = optimizeDeps +export function getDepOptimizationConfig( + config: ResolvedConfig, + ssr: boolean +): DepOptimizationConfig { + return ssr ? config.ssr.optimizeDeps : config.optimizeDeps +} +export function isDepsOptimizerEnabled( + config: ResolvedConfig, + ssr: boolean +): boolean { + const { command } = config + const { disabled } = getDepOptimizationConfig(config, ssr) return !( disabled === true || (command === 'build' && disabled === 'build') || - (command === 'serve' && optimizeDeps.disabled === 'dev') + (command === 'serve' && disabled === 'dev') ) } - -// esbuild doesn't transpile `require('foo')` into `import` statements if 'foo' is externalized -// https://github.com/evanw/esbuild/issues/566#issuecomment-735551834 -function esbuildCjsExternalPlugin(externals: string[]): ESBuildPlugin { - return { - name: 'cjs-external', - setup(build) { - const escape = (text: string) => - `^${text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}$` - const filter = new RegExp(externals.map(escape).join('|')) - - build.onResolve({ filter: /.*/, namespace: 'external' }, (args) => ({ - path: args.path, - external: true - })) - - build.onResolve({ filter }, (args) => ({ - path: args.path, - namespace: 'external' - })) - - build.onLoad({ filter: /.*/, namespace: 'external' }, (args) => ({ - contents: `export * from ${JSON.stringify(args.path)}` - })) - } - } -} - -// Support `rollupOptions.external` when `legacy.buildRollupPluginCommonjs` is disabled -function externalConfigCompat(config: UserConfig, { command }: ConfigEnv) { - // Only affects the build command - if (command !== 'build') { - return {} - } - - // Skip if using Rollup CommonJS plugin - if ( - config.legacy?.buildRollupPluginCommonjs || - config.optimizeDeps?.disabled === 'build' - ) { - return {} - } - - // Skip if no `external` configured - const external = config?.build?.rollupOptions?.external - if (!external) { - return {} - } - - let normalizedExternal = external - if (typeof external === 'string') { - normalizedExternal = [external] - } - - // TODO: decide whether to support RegExp and function options - // They're not supported yet because `optimizeDeps.exclude` currently only accepts strings - if ( - !Array.isArray(normalizedExternal) || - normalizedExternal.some((ext) => typeof ext !== 'string') - ) { - throw new Error( - `[vite] 'build.rollupOptions.external' can only be an array of strings or a string.\n` + - `You can turn on 'legacy.buildRollupPluginCommonjs' to support more advanced options.` - ) - } - - const additionalConfig: UserConfig = { - optimizeDeps: { - exclude: normalizedExternal as string[], - esbuildOptions: { - plugins: [ - // TODO: maybe it can be added globally/unconditionally? - esbuildCjsExternalPlugin(normalizedExternal as string[]) - ] - } - } - } - - return additionalConfig -} diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index e1a9de026ffc1e..bdac099dd214e6 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -35,6 +35,7 @@ export type { export type { DepOptimizationMetadata, DepOptimizationOptions, + DepOptimizationConfig, DepOptimizationResult, DepOptimizationProcessing, OptimizedDepInfo, @@ -43,6 +44,7 @@ export type { } from './optimizer' export type { ResolvedSSROptions, + SsrDepOptimizationOptions, SSROptions, SSRFormat, SSRTarget diff --git a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts index d41caca22d2d4a..20b808b1dc7ec7 100644 --- a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts +++ b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts @@ -2,6 +2,7 @@ import path from 'node:path' import { promises as fs } from 'node:fs' import type { ImportKind, Plugin } from 'esbuild' import { KNOWN_ASSET_TYPES } from '../constants' +import { getDepOptimizationConfig } from '..' import type { ResolvedConfig } from '..' import { flattenId, @@ -43,14 +44,15 @@ const externalTypes = [ export function esbuildDepPlugin( qualified: Record, exportsData: Record, + external: string[], config: ResolvedConfig, ssr: boolean ): Plugin { + const { extensions } = getDepOptimizationConfig(config, ssr) + // remove optimizable extensions from `externalTypes` list - const allExternalTypes = config.optimizeDeps.extensions - ? externalTypes.filter( - (type) => !config.optimizeDeps.extensions?.includes('.' + type) - ) + const allExternalTypes = extensions + ? externalTypes.filter((type) => !extensions?.includes('.' + type)) : externalTypes // default resolver which prefers ESM @@ -163,7 +165,7 @@ export function esbuildDepPlugin( build.onResolve( { filter: /^[\w@][^:]/ }, async ({ path: id, importer, kind }) => { - if (moduleListContains(config.optimizeDeps?.exclude, id)) { + if (moduleListContains(external, id)) { return { path: id, external: true @@ -301,3 +303,30 @@ module.exports = Object.create(new Proxy({}, { } } } + +// esbuild doesn't transpile `require('foo')` into `import` statements if 'foo' is externalized +// https://github.com/evanw/esbuild/issues/566#issuecomment-735551834 +export function esbuildCjsExternalPlugin(externals: string[]): Plugin { + return { + name: 'cjs-external', + setup(build) { + const escape = (text: string) => + `^${text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}$` + const filter = new RegExp(externals.map(escape).join('|')) + + build.onResolve({ filter: /.*/, namespace: 'external' }, (args) => ({ + path: args.path, + external: true + })) + + build.onResolve({ filter }, (args) => ({ + path: args.path, + namespace: 'external' + })) + + build.onLoad({ filter: /.*/, namespace: 'external' }, (args) => ({ + contents: `export * from ${JSON.stringify(args.path)}` + })) + } + } +} diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 142a9519bb4b59..d941b2fa6c7570 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -7,6 +7,7 @@ import type { BuildOptions as EsbuildBuildOptions } from 'esbuild' import { build } from 'esbuild' import { init, parse } from 'es-module-lexer' import { createFilter } from '@rollup/pluginutils' +import { getDepOptimizationConfig } from '../config' import type { ResolvedConfig } from '../config' import { arraify, @@ -24,7 +25,7 @@ import { } from '../utils' import { transformWithEsbuild } from '../plugins/esbuild' import { ESBUILD_MODULES_TARGET } from '../constants' -import { esbuildDepPlugin } from './esbuildDepPlugin' +import { esbuildCjsExternalPlugin, esbuildDepPlugin } from './esbuildDepPlugin' import { scanImports } from './scan' export { initDepsOptimizer, @@ -71,18 +72,7 @@ export interface DepsOptimizer { options: DepOptimizationOptions } -export interface DepOptimizationOptions { - /** - * By default, Vite will crawl your `index.html` to detect dependencies that - * need to be pre-bundled. If `build.rollupOptions.input` is specified, Vite - * will crawl those entry points instead. - * - * If neither of these fit your needs, you can specify custom entries using - * this option - the value should be a fast-glob pattern or array of patterns - * (https://github.com/mrmlnc/fast-glob#basic-syntax) that are relative from - * vite project root. This will overwrite default entries inference. - */ - entries?: string | string[] +export interface DepOptimizationConfig { /** * Force optimize listed dependencies (must be resolvable import paths, * cannot be globs). @@ -141,6 +131,20 @@ export interface DepOptimizationOptions { * @experimental */ disabled?: boolean | 'build' | 'dev' +} + +export type DepOptimizationOptions = DepOptimizationConfig & { + /** + * By default, Vite will crawl your `index.html` to detect dependencies that + * need to be pre-bundled. If `build.rollupOptions.input` is specified, Vite + * will crawl those entry points instead. + * + * If neither of these fit your needs, you can specify custom entries using + * this option - the value should be a fast-glob pattern or array of patterns + * (https://github.com/mrmlnc/fast-glob#basic-syntax) that are relative from + * vite project root. This will overwrite default entries inference. + */ + entries?: string | string[] /** * Force dep pre-optimization regardless of whether deps have changed. * @experimental @@ -223,22 +227,26 @@ export async function optimizeDeps( ): Promise { const log = asCommand ? config.logger.info : debug + const ssr = !!config.build.ssr + const cachedMetadata = loadCachedDepOptimizationMetadata( config, + ssr, force, asCommand ) if (cachedMetadata) { return cachedMetadata } + const deps = await discoverProjectDependencies(config) const depsString = depsLogString(Object.keys(deps)) log(colors.green(`Optimizing dependencies:\n ${depsString}`)) - await addManuallyIncludedOptimizeDeps(deps, config) + await addManuallyIncludedOptimizeDeps(deps, config, ssr) - const depsInfo = toDiscoveredDependencies(config, deps, !!config.build.ssr) + const depsInfo = toDiscoveredDependencies(config, deps, ssr) const result = await runOptimizeDeps(config, depsInfo) @@ -250,11 +258,12 @@ export async function optimizeDeps( export async function optimizeServerSsrDeps( config: ResolvedConfig ): Promise { + const ssr = true const cachedMetadata = loadCachedDepOptimizationMetadata( config, + ssr, config.optimizeDeps.force, - false, - true // ssr + false ) if (cachedMetadata) { return cachedMetadata @@ -263,6 +272,8 @@ export async function optimizeServerSsrDeps( let alsoInclude: string[] | undefined let noExternalFilter: ((id: unknown) => boolean) | undefined + const { exclude } = getDepOptimizationConfig(config, ssr) + const noExternal = config.ssr?.noExternal if (noExternal) { alsoInclude = arraify(noExternal).filter( @@ -271,7 +282,7 @@ export async function optimizeServerSsrDeps( noExternalFilter = noExternal === true ? (dep: unknown) => false - : createFilter(noExternal, config.optimizeDeps?.exclude, { + : createFilter(undefined, exclude, { resolve: false }) } @@ -281,6 +292,7 @@ export async function optimizeServerSsrDeps( await addManuallyIncludedOptimizeDeps( deps, config, + ssr, alsoInclude, noExternalFilter ) @@ -296,9 +308,10 @@ export async function optimizeServerSsrDeps( export function initDepsOptimizerMetadata( config: ResolvedConfig, + ssr: boolean, timestamp?: string ): DepOptimizationMetadata { - const hash = getDepHash(config) + const hash = getDepHash(config, ssr) return { hash, browserHash: getOptimizedBrowserHash(hash, {}, timestamp), @@ -325,9 +338,9 @@ export function addOptimizedDepInfo( */ export function loadCachedDepOptimizationMetadata( config: ResolvedConfig, + ssr: boolean, force = config.optimizeDeps.force, - asCommand = false, - ssr = !!config.build.ssr + asCommand = false ): DepOptimizationMetadata | undefined { const log = asCommand ? config.logger.info : debug @@ -349,7 +362,7 @@ export function loadCachedDepOptimizationMetadata( ) } catch (e) {} // hash is consistent, no need to re-bundle - if (cachedMetadata && cachedMetadata.hash === getDepHash(config)) { + if (cachedMetadata && cachedMetadata.hash === getDepHash(config, ssr)) { log('Hash is consistent. Skipping. Use --force to override.') // Nothing to commit or cancel as we are using the cache, we only // need to resolve the processing promise so requests can move on @@ -396,7 +409,7 @@ export function toDiscoveredDependencies( timestamp?: string ): Record { const browserHash = getOptimizedBrowserHash( - getDepHash(config), + getDepHash(config, ssr), deps, timestamp ) @@ -408,7 +421,7 @@ export function toDiscoveredDependencies( file: getOptimizedDepPath(id, config, ssr), src, browserHash: browserHash, - exportsData: extractExportsData(src, config) + exportsData: extractExportsData(src, config, ssr) } } return discovered @@ -463,7 +476,7 @@ export async function runOptimizeDeps( JSON.stringify({ type: 'module' }) ) - const metadata = initDepsOptimizerMetadata(config) + const metadata = initDepsOptimizerMetadata(config, ssr) metadata.browserHash = getOptimizedBrowserHash( metadata.hash, @@ -504,13 +517,15 @@ export async function runOptimizeDeps( const idToExports: Record = {} const flatIdToExports: Record = {} - const { plugins = [], ...esbuildOptions } = - config.optimizeDeps?.esbuildOptions ?? {} + const optimizeDeps = getDepOptimizationConfig(config, ssr) + + const { plugins: pluginsFromConfig = [], ...esbuildOptions } = + optimizeDeps?.esbuildOptions ?? {} for (const id in depsInfo) { const src = depsInfo[id].src! const exportsData = await (depsInfo[id].exportsData ?? - extractExportsData(src, config)) + extractExportsData(src, config, ssr)) if (exportsData.jsxLoader) { // Ensure that optimization won't fail by defaulting '.js' to the JSX parser. // This is useful for packages such as Gatsby. @@ -538,6 +553,37 @@ export async function runOptimizeDeps( const platform = ssr && config.ssr?.target !== 'webworker' ? 'node' : 'browser' + const external = [...(optimizeDeps?.exclude ?? [])] + + if (isBuild) { + let rollupOptionsExternal = config?.build?.rollupOptions?.external + if (rollupOptionsExternal) { + if (typeof rollupOptionsExternal === 'string') { + rollupOptionsExternal = [rollupOptionsExternal] + } + // TODO: decide whether to support RegExp and function options + // They're not supported yet because `optimizeDeps.exclude` currently only accepts strings + if ( + !Array.isArray(rollupOptionsExternal) || + rollupOptionsExternal.some((ext) => typeof ext !== 'string') + ) { + throw new Error( + `[vite] 'build.rollupOptions.external' can only be an array of strings or a string.\n` + + `You can turn on 'legacy.buildRollupPluginCommonjs' to support more advanced options.` + ) + } + external.push(...(rollupOptionsExternal as string[])) + } + } + + const plugins = [...pluginsFromConfig] + if (external.length) { + plugins.push(esbuildCjsExternalPlugin(external)) + } + plugins.push( + esbuildDepPlugin(flatIdDeps, flatIdToExports, external, config, ssr) + ) + const start = performance.now() const result = await build({ @@ -558,17 +604,14 @@ export async function runOptimizeDeps( } : undefined, target: isBuild ? config.build.target || undefined : ESBUILD_MODULES_TARGET, - external: config.optimizeDeps?.exclude, + external, logLevel: 'error', splitting: true, sourcemap: true, outdir: processingCacheDir, ignoreAnnotations: !isBuild, metafile: true, - plugins: [ - ...plugins, - esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr) - ], + plugins, ...esbuildOptions, supported: { 'dynamic-import': true, @@ -599,7 +642,7 @@ export async function runOptimizeDeps( browserHash: metadata.browserHash, // After bundling we have more information and can warn the user about legacy packages // that require manual configuration - needsInterop: needsInterop(config, id, idToExports[id], output) + needsInterop: needsInterop(config, ssr, id, idToExports[id], output) }) } @@ -634,21 +677,34 @@ export async function runOptimizeDeps( } export async function findKnownImports( - config: ResolvedConfig + config: ResolvedConfig, + ssr: boolean ): Promise { const deps = (await scanImports(config)).deps - await addManuallyIncludedOptimizeDeps(deps, config) + await addManuallyIncludedOptimizeDeps(deps, config, ssr) return Object.keys(deps) } export async function addManuallyIncludedOptimizeDeps( deps: Record, config: ResolvedConfig, + ssr: boolean, extra: string[] = [], filter?: (id: string) => boolean ): Promise { - const optimizeDepsInclude = config.optimizeDeps?.include ?? [] + const { logger } = config + const optimizeDeps = getDepOptimizationConfig(config, ssr) + const optimizeDepsInclude = optimizeDeps?.include ?? [] if (optimizeDepsInclude.length || extra.length) { + const unableToOptimize = (id: string, msg: string) => { + if (optimizeDepsInclude.includes(id)) { + logger.warn( + `${msg}: ${colors.cyan(id)}, present in '${ + ssr ? 'ssr.' : '' + }optimizeDeps.include'` + ) + } + } const resolve = config.createResolver({ asSrc: false, scan: true }) for (const id of [...optimizeDepsInclude, ...extra]) { // normalize 'foo >bar` as 'foo > bar' to prevent same id being added @@ -657,17 +713,13 @@ export async function addManuallyIncludedOptimizeDeps( if (!deps[normalizedId] && filter?.(normalizedId) !== false) { const entry = await resolve(id) if (entry) { - if (isOptimizable(entry, config.optimizeDeps)) { + if (isOptimizable(entry, optimizeDeps)) { deps[normalizedId] = entry - } else if (optimizeDepsInclude.includes(id)) { - config.logger.warn( - `Cannot optimize included dependency: ${colors.cyan(id)}` - ) + } else { + unableToOptimize(entry, 'Cannot optimize dependency') } } else { - throw new Error( - `Failed to resolve force included dependency: ${colors.cyan(id)}` - ) + unableToOptimize(id, 'Failed to resolve dependency') } } } @@ -865,12 +917,15 @@ function esbuildOutputFromId( export async function extractExportsData( filePath: string, - config: ResolvedConfig + config: ResolvedConfig, + ssr: boolean ): Promise { await init - const esbuildOptions = config.optimizeDeps?.esbuildOptions ?? {} - if (config.optimizeDeps.extensions?.some((ext) => filePath.endsWith(ext))) { + const optimizeDeps = getDepOptimizationConfig(config, ssr) + + const esbuildOptions = optimizeDeps?.esbuildOptions ?? {} + if (optimizeDeps.extensions?.some((ext) => filePath.endsWith(ext))) { // For custom supported extensions, build the entry file to transform it into JS, // and then parse with es-module-lexer. Note that the `bundle` option is not `true`, // so only the entry file is being transformed. @@ -933,12 +988,13 @@ const KNOWN_INTEROP_IDS = new Set(['moment']) function needsInterop( config: ResolvedConfig, + ssr: boolean, id: string, exportsData: ExportsData, output?: { exports: string[] } ): boolean { if ( - config.optimizeDeps?.needsInterop?.includes(id) || + getDepOptimizationConfig(config, ssr)?.needsInterop?.includes(id) || KNOWN_INTEROP_IDS.has(id) ) { return true @@ -972,10 +1028,11 @@ function isSingleDefaultExport(exports: readonly string[]) { const lockfileFormats = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'] -export function getDepHash(config: ResolvedConfig): string { +export function getDepHash(config: ResolvedConfig, ssr: boolean): string { let content = lookupFile(config.root, lockfileFormats) || '' // also take config into account // only a subset of config options that can affect dep optimization + const optimizeDeps = getDepOptimizationConfig(config, ssr) content += JSON.stringify( { mode: process.env.NODE_ENV || config.mode, @@ -985,13 +1042,11 @@ export function getDepHash(config: ResolvedConfig): string { assetsInclude: config.assetsInclude, plugins: config.plugins.map((p) => p.name), optimizeDeps: { - include: config.optimizeDeps?.include, - exclude: config.optimizeDeps?.exclude, + include: optimizeDeps?.include, + exclude: optimizeDeps?.exclude, esbuildOptions: { - ...config.optimizeDeps?.esbuildOptions, - plugins: config.optimizeDeps?.esbuildOptions?.plugins?.map( - (p) => p.name - ) + ...optimizeDeps?.esbuildOptions, + plugins: optimizeDeps?.esbuildOptions?.plugins?.map((p) => p.name) } } }, @@ -1044,13 +1099,15 @@ function findOptimizedDepInfoInRecord( export async function optimizedDepNeedsInterop( metadata: DepOptimizationMetadata, file: string, - config: ResolvedConfig + config: ResolvedConfig, + ssr: boolean ): Promise { const depInfo = optimizedDepInfoFromFile(metadata, file) if (depInfo?.src && depInfo.needsInterop === undefined) { - depInfo.exportsData ??= extractExportsData(depInfo.src, config) + depInfo.exportsData ??= extractExportsData(depInfo.src, config, ssr) depInfo.needsInterop = needsInterop( config, + ssr, depInfo.id, await depInfo.exportsData ) diff --git a/packages/vite/src/node/optimizer/optimizer.ts b/packages/vite/src/node/optimizer/optimizer.ts index 5432f9493a7348..4759edeb9d0b51 100644 --- a/packages/vite/src/node/optimizer/optimizer.ts +++ b/packages/vite/src/node/optimizer/optimizer.ts @@ -1,6 +1,7 @@ import colors from 'picocolors' import _debug from 'debug' import { getHash } from '../utils' +import { getDepOptimizationConfig } from '..' import type { ResolvedConfig, ViteDevServer } from '..' import { addManuallyIncludedOptimizeDeps, @@ -40,10 +41,10 @@ const devSsrDepsOptimizerMap = new WeakMap() export function getDepsOptimizer( config: ResolvedConfig, - type: { ssr?: boolean } + ssr?: boolean ): DepsOptimizer | undefined { // Workers compilation shares the DepsOptimizer from the main build - const isDevSsr = type.ssr && config.command !== 'build' + const isDevSsr = ssr && config.command !== 'build' return (isDevSsr ? devSsrDepsOptimizerMap : depsOptimizerMap).get( config.mainConfig || config ) @@ -53,7 +54,9 @@ export async function initDepsOptimizer( config: ResolvedConfig, server?: ViteDevServer ): Promise { - if (!getDepsOptimizer(config, { ssr: false })) { + // Non Dev SSR Optimizer + const ssr = !!config.build.ssr + if (!getDepsOptimizer(config, ssr)) { await createDepsOptimizer(config, server) } } @@ -63,7 +66,8 @@ export async function initDevSsrDepsOptimizer( config: ResolvedConfig, server: ViteDevServer ): Promise { - if (getDepsOptimizer(config, { ssr: true })) { + if (getDepsOptimizer(config, true)) { + // ssr return } if (creatingDevSsrOptimizer) { @@ -73,10 +77,11 @@ export async function initDevSsrDepsOptimizer( // Important: scanning needs to be done before starting the SSR dev optimizer // If ssrLoadModule is called before server.listen(), the main deps optimizer // will not be yet created - if (!getDepsOptimizer(config, { ssr: false })) { + const ssr = false + if (!getDepsOptimizer(config, ssr)) { await initDepsOptimizer(config, server) } - await getDepsOptimizer(config, { ssr: false })!.scanProcessing + await getDepsOptimizer(config, ssr)!.scanProcessing await createDevSsrDepsOptimizer(config) creatingDevSsrOptimizer = undefined @@ -90,15 +95,16 @@ async function createDepsOptimizer( ): Promise { const { logger } = config const isBuild = config.command === 'build' + const ssr = !!config.build.ssr // safe as Dev SSR don't use this optimizer const sessionTimestamp = Date.now().toString() - const cachedMetadata = loadCachedDepOptimizationMetadata(config) + const cachedMetadata = loadCachedDepOptimizationMetadata(config, ssr) let handle: NodeJS.Timeout | undefined let metadata = - cachedMetadata || initDepsOptimizerMetadata(config, sessionTimestamp) + cachedMetadata || initDepsOptimizerMetadata(config, ssr, sessionTimestamp) const depsOptimizer: DepsOptimizer = { metadata, @@ -112,7 +118,7 @@ async function createDepsOptimizer( delayDepsOptimizerUntil, resetRegisteredIds, ensureFirstRun, - options: config.optimizeDeps + options: getDepOptimizationConfig(config, ssr) } depsOptimizerMap.set(config, depsOptimizer) @@ -160,12 +166,12 @@ async function createDepsOptimizer( // Initialize discovered deps with manually added optimizeDeps.include info const deps: Record = {} - await addManuallyIncludedOptimizeDeps(deps, config) + await addManuallyIncludedOptimizeDeps(deps, config, ssr) const discovered = await toDiscoveredDependencies( config, deps, - !!config.build.ssr, + ssr, sessionTimestamp ) @@ -481,14 +487,8 @@ async function createDepsOptimizer( function registerMissingImport( id: string, - resolved: string, - ssr?: boolean + resolved: string ): OptimizedDepInfo { - if (!isBuild && ssr) { - config.logger.error( - 'Vite internal error: ssr dep registerd as a browser dep' - ) - } const optimized = metadata.optimized[id] if (optimized) { return optimized @@ -504,7 +504,7 @@ async function createDepsOptimizer( return missing } - missing = addMissingDep(id, resolved, ssr) + missing = addMissingDep(id, resolved) // Until the first optimize run is called, avoid triggering processing // We'll wait until the user codebase is eagerly processed by Vite so @@ -522,7 +522,7 @@ async function createDepsOptimizer( return missing } - function addMissingDep(id: string, resolved: string, ssr?: boolean) { + function addMissingDep(id: string, resolved: string) { newDepsDiscovered = true return addOptimizedDepInfo(metadata, 'discovered', { @@ -541,7 +541,7 @@ async function createDepsOptimizer( // loading of this pre-bundled dep needs to await for its processing // promise to be resolved processing: depOptimizationProcessing.promise, - exportsData: extractExportsData(resolved, config) + exportsData: extractExportsData(resolved, config, ssr) }) } @@ -748,7 +748,7 @@ async function createDevSsrDepsOptimizer( delayDepsOptimizerUntil: (id: string, done: () => Promise) => {}, resetRegisteredIds: () => {}, ensureFirstRun: () => {}, - options: config.optimizeDeps + options: config.ssr.optimizeDeps } devSsrDepsOptimizerMap.set(config, depsOptimizer) } diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index 5608472b4efe91..f4eae2bc2ed3e2 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -44,6 +44,8 @@ export async function scanImports(config: ResolvedConfig): Promise<{ deps: Record missing: Record }> { + // Only used to scan non-ssr code + const start = performance.now() let entries: string[] = [] diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 5af046908ff0fa..3458b9f3d51d8e 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -9,6 +9,7 @@ import { parse as parseJS } from 'acorn' import type { Node } from 'estree' import { findStaticImports, parseStaticImport } from 'mlly' import { makeLegalIdentifier } from '@rollup/pluginutils' +import { getDepOptimizationConfig } from '..' import type { ViteDevServer } from '..' import { CLIENT_DIR, @@ -214,7 +215,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { ) } - const depsOptimizer = getDepsOptimizer(config, { ssr }) + const depsOptimizer = getDepsOptimizer(config, ssr) const { moduleGraph } = server // since we are already in the transform phase of the importer, it must @@ -267,7 +268,9 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { } let importerFile = importer - if (moduleListContains(config.optimizeDeps?.exclude, url)) { + + const optimizeDeps = getDepOptimizationConfig(config, ssr) + if (moduleListContains(optimizeDeps?.exclude, url)) { if (depsOptimizer) { await depsOptimizer.scanProcessing @@ -485,7 +488,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const needsInterop = await optimizedDepNeedsInterop( depsOptimizer.metadata, file, - config + config, + ssr ) if (needsInterop === undefined) { diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index b83ae46451c8f7..18a33b6d61ce74 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -14,6 +14,7 @@ import { moduleListContains } from '../utils' import type { Plugin } from '../plugin' +import { getDepOptimizationConfig } from '../config' import type { ResolvedConfig } from '../config' import { genSourceMapUrl } from '../server/sourcemap' import { getDepsOptimizer, optimizedDepNeedsInterop } from '../optimizer' @@ -155,7 +156,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { } const { root } = config - const depsOptimizer = getDepsOptimizer(config, { ssr }) + const depsOptimizer = getDepsOptimizer(config, ssr) const normalizeUrl = async ( url: string, @@ -163,7 +164,8 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { ): Promise<[string, string]> => { let importerFile = importer - if (moduleListContains(config.optimizeDeps?.exclude, url)) { + const optimizeDeps = getDepOptimizationConfig(config, ssr) + if (moduleListContains(optimizeDeps?.exclude, url)) { if (depsOptimizer) { await depsOptimizer.scanProcessing @@ -260,7 +262,8 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { const needsInterop = await optimizedDepNeedsInterop( depsOptimizer.metadata, file, - config + config, + ssr ) let rewriteDone = false diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 0bab69f50eeb98..c58cf8f30a77e5 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -33,7 +33,6 @@ export async function resolvePlugins( ): Promise { const isBuild = config.command === 'build' const isWatch = isBuild && !!config.build.watch - const buildPlugins = isBuild ? (await import('../build')).resolveBuildPlugins(config) : { pre: [], post: [] } @@ -47,7 +46,8 @@ export async function resolvePlugins( config.build.polyfillModulePreload ? modulePreloadPolyfillPlugin(config) : null, - ...(isDepsOptimizerEnabled(config) + ...(isDepsOptimizerEnabled(config, false) || + isDepsOptimizerEnabled(config, true) ? [ isBuild ? optimizedDepsBuildPlugin(config) @@ -62,8 +62,7 @@ export async function resolvePlugins( packageCache: config.packageCache, ssrConfig: config.ssr, asSrc: true, - getDepsOptimizer: (type: { ssr?: boolean }) => - getDepsOptimizer(config, type), + getDepsOptimizer: (ssr: boolean) => getDepsOptimizer(config, ssr), shouldExternalize: isBuild && config.build.ssr && config.ssr?.format !== 'cjs' ? (id) => shouldExternalizeForSSR(id, config) diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts index dff944c3d224e2..98139519b88a47 100644 --- a/packages/vite/src/node/plugins/optimizedDeps.ts +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -18,7 +18,7 @@ export function optimizedDepsPlugin(config: ResolvedConfig): Plugin { name: 'vite:optimized-deps', async resolveId(id, source, { ssr }) { - if (getDepsOptimizer(config, { ssr })?.isOptimizedDepFile(id)) { + if (getDepsOptimizer(config, ssr)?.isOptimizedDepFile(id)) { return id } }, @@ -29,7 +29,7 @@ export function optimizedDepsPlugin(config: ResolvedConfig): Plugin { async load(id, options) { const ssr = options?.ssr === true - const depsOptimizer = getDepsOptimizer(config, { ssr }) + const depsOptimizer = getDepsOptimizer(config, ssr) if (depsOptimizer?.isOptimizedDepFile(id)) { const metadata = depsOptimizer.metadata const file = cleanUrl(id) @@ -85,29 +85,26 @@ export function optimizedDepsBuildPlugin(config: ResolvedConfig): Plugin { if (!config.isWorker) { // This will be run for the current active optimizer, during build // it will be the SSR optimizer if config.build.ssr is defined - getDepsOptimizer(config, { ssr: undefined })?.resetRegisteredIds() + getDepsOptimizer(config)?.resetRegisteredIds() } }, async resolveId(id, importer, { ssr }) { - if (getDepsOptimizer(config, { ssr })?.isOptimizedDepFile(id)) { + if (getDepsOptimizer(config, ssr)?.isOptimizedDepFile(id)) { return id } }, transform(_code, id, options) { const ssr = options?.ssr === true - getDepsOptimizer(config, { ssr })?.delayDepsOptimizerUntil( - id, - async () => { - await this.load({ id }) - } - ) + getDepsOptimizer(config, ssr)?.delayDepsOptimizerUntil(id, async () => { + await this.load({ id }) + }) }, async load(id, options) { const ssr = options?.ssr === true - const depsOptimizer = getDepsOptimizer(config, { ssr }) + const depsOptimizer = getDepsOptimizer(config, ssr) if (!depsOptimizer?.isOptimizedDepFile(id)) { return } diff --git a/packages/vite/src/node/plugins/preAlias.ts b/packages/vite/src/node/plugins/preAlias.ts index 2623a9d4ec97bc..d9cd9cdfb888d6 100644 --- a/packages/vite/src/node/plugins/preAlias.ts +++ b/packages/vite/src/node/plugins/preAlias.ts @@ -13,7 +13,7 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin { name: 'vite:pre-alias', async resolveId(id, importer, options) { const ssr = options?.ssr === true - const depsOptimizer = getDepsOptimizer(config, { ssr }) + const depsOptimizer = getDepsOptimizer(config, ssr) if ( importer && depsOptimizer && diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index a18a30d72ee073..cbad9fab449e0d 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -83,7 +83,7 @@ export interface InternalResolveOptions extends ResolveOptions { // True when resolving during the scan phase to discover dependencies scan?: boolean // Resolve using esbuild deps optimization - getDepsOptimizer?: (type: { ssr?: boolean }) => DepsOptimizer | undefined + getDepsOptimizer?: (ssr: boolean) => DepsOptimizer | undefined shouldExternalize?: (id: string) => boolean | undefined } @@ -106,7 +106,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { // We need to delay depsOptimizer until here instead of passing it as an option // the resolvePlugin because the optimizer is created on server listen during dev - const depsOptimizer = resolveOptions.getDepsOptimizer?.({ ssr }) + const depsOptimizer = resolveOptions.getDepsOptimizer?.(ssr) if (id.startsWith(browserExternalId)) { return id diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index cf62c91f0f24f9..29c05a880d998c 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -270,7 +270,7 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { : 'module' const workerOptions = workerType === 'classic' ? '' : ',{type: "module"}' if (isBuild) { - getDepsOptimizer(config, { ssr })?.registerWorkersSource(id) + getDepsOptimizer(config, ssr)?.registerWorkersSource(id) if (query.inline != null) { const chunk = await bundleWorkerEntry(config, id, query) // inline as blob data url diff --git a/packages/vite/src/node/plugins/workerImportMetaUrl.ts b/packages/vite/src/node/plugins/workerImportMetaUrl.ts index 86b8e377a40c71..5fbd7d4883382f 100644 --- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts @@ -118,7 +118,7 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { let url: string if (isBuild) { - getDepsOptimizer(config, { ssr })?.registerWorkersSource(id) + getDepsOptimizer(config, ssr)?.registerWorkersSource(id) url = await workerFileToUrl(config, file, query) } else { url = await fileToUrl(cleanUrl(file), config, this) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 9c722ed667a4e7..d270c92bfd0e74 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -329,7 +329,7 @@ export async function createServer( }, transformIndexHtml: null!, // to be immediately set async ssrLoadModule(url, opts?: { fixStacktrace?: boolean }) { - if (isDepsOptimizerEnabled(config)) { + if (isDepsOptimizerEnabled(config, true)) { await initDevSsrDepsOptimizer(config, server) } await updateCjsSsrExternals(server) @@ -539,7 +539,8 @@ export async function createServer( } initingServer = (async function () { await container.buildStart({}) - if (isDepsOptimizerEnabled(config)) { + if (isDepsOptimizerEnabled(config, false)) { + // non-ssr await initDepsOptimizer(config, server) } initingServer = undefined @@ -776,7 +777,7 @@ async function updateCjsSsrExternals(server: ViteDevServer) { // This is part of the v2 externalization heuristics and it is kept // for backwards compatibility in case user needs to fallback to the // legacy scheme. It may be removed in a future v3 minor. - const depsOptimizer = getDepsOptimizer(server.config, { ssr: false }) + const depsOptimizer = getDepsOptimizer(server.config, false) // non-ssr if (depsOptimizer) { await depsOptimizer.scanProcessing diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 15151b7a29e08a..8d240df9cda2f1 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -71,11 +71,8 @@ export function transformMiddleware( const isSourceMap = withoutQuery.endsWith('.map') // since we generate source map references, handle those requests here if (isSourceMap) { - if ( - getDepsOptimizer(server.config, { ssr: false })?.isOptimizedDepUrl( - url - ) - ) { + const depsOptimizer = getDepsOptimizer(server.config, false) // non-ssr + if (depsOptimizer?.isOptimizedDepUrl(url)) { // If the browser is requesting a source map for an optimized dep, it // means that the dependency has already been pre-bundled and loaded const mapFile = url.startsWith(FS_PREFIX) @@ -189,12 +186,10 @@ export function transformMiddleware( html: req.headers.accept?.includes('text/html') }) if (result) { + const depsOptimizer = getDepsOptimizer(server.config, false) // non-ssr const type = isDirectCSSRequest(url) ? 'css' : 'js' const isDep = - DEP_VERSION_RE.test(url) || - getDepsOptimizer(server.config, { ssr: false })?.isOptimizedDepUrl( - url - ) + DEP_VERSION_RE.test(url) || depsOptimizer?.isOptimizedDepUrl(url) return send(req, res, result.code, type, { etag: result.etag, // allow browser to cache npm deps! diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index 574e69b27524a5..7f74d5ee704544 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -145,7 +145,7 @@ async function doTransform( const result = loadAndTransform(id, url, server, options, timestamp) - getDepsOptimizer(config, { ssr })?.delayDepsOptimizerUntil(id, () => result) + getDepsOptimizer(config, ssr)?.delayDepsOptimizerUntil(id, () => result) return result } diff --git a/packages/vite/src/node/ssr/index.ts b/packages/vite/src/node/ssr/index.ts index 84f050ff8e41d4..0d70e53cccdb5e 100644 --- a/packages/vite/src/node/ssr/index.ts +++ b/packages/vite/src/node/ssr/index.ts @@ -1,9 +1,13 @@ +import type { DepOptimizationConfig } from '../optimizer' + export type SSRTarget = 'node' | 'webworker' export type SSRFormat = 'esm' | 'cjs' +export type SsrDepOptimizationOptions = DepOptimizationConfig + export interface SSROptions { - external?: string[] noExternal?: string | RegExp | (string | RegExp)[] | true + external?: string[] /** * Define the target for the ssr build. The browser field in package.json * is ignored for node but used if webworker is the target @@ -18,22 +22,50 @@ export interface SSROptions { * @experimental */ format?: SSRFormat + /** + * Control over which dependencies are optimized during SSR and esbuild options + * During build: + * no external CJS dependencies are optimized by default + * During dev: + * explicit no external CJS dependencies are optimized by default + * @experimental + */ + optimizeDeps?: SsrDepOptimizationOptions } export interface ResolvedSSROptions extends SSROptions { target: SSRTarget format: SSRFormat + optimizeDeps: SsrDepOptimizationOptions } export function resolveSSROptions( - ssr: SSROptions | undefined -): ResolvedSSROptions | undefined { - if (ssr === undefined) { - return undefined + ssr: SSROptions | undefined, + buildSsrCjsExternalHeuristics?: boolean, + preserveSymlinks?: boolean +): ResolvedSSROptions { + ssr ??= {} + const optimizeDeps = ssr.optimizeDeps ?? {} + let format: SSRFormat = 'esm' + let target: SSRTarget = 'node' + if (buildSsrCjsExternalHeuristics) { + if (ssr) { + format = 'cjs' + } else { + target = 'node' + format = 'cjs' + } } return { - format: 'esm', - target: 'node', - ...ssr + format, + target, + ...ssr, + optimizeDeps: { + ...optimizeDeps, + esbuildOptions: { + preserveSymlinks, + ...optimizeDeps.esbuildOptions + } + } } } diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 6f7bef05752003..0b0c205d80b9e4 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -28,6 +28,7 @@ import { VALID_ID_PREFIX, wildcardHosts } from './constants' +import type { DepOptimizationConfig } from './optimizer' import type { ResolvedConfig } from '.' /** @@ -94,11 +95,12 @@ export function moduleListContains( export function isOptimizable( id: string, - optimizeDepsConfig: ResolvedConfig['optimizeDeps'] + optimizeDeps: DepOptimizationConfig ): boolean { + const { extensions } = optimizeDeps return ( OPTIMIZABLE_ENTRY_RE.test(id) || - (optimizeDepsConfig.extensions?.some((ext) => id.endsWith(ext)) ?? false) + (extensions?.some((ext) => id.endsWith(ext)) ?? false) ) }