From 0e1f437d53683b57f0157ce3ff0b0f02acabb408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=A0=20/=20green?= Date: Wed, 13 Nov 2024 07:55:24 +0900 Subject: [PATCH] refactor: introduce `mergeWithDefaults` and organize how default values for config options are set (#18550) --- .../vite/src/node/__tests__/utils.spec.ts | 50 ++++ packages/vite/src/node/__tests_dts__/utils.ts | 58 ++++ packages/vite/src/node/build.ts | 155 +++++----- packages/vite/src/node/config.ts | 266 +++++++++++++----- packages/vite/src/node/constants.ts | 31 +- packages/vite/src/node/plugins/css.ts | 39 ++- packages/vite/src/node/plugins/index.ts | 9 +- packages/vite/src/node/plugins/json.ts | 2 +- packages/vite/src/node/preview.ts | 5 +- packages/vite/src/node/server/index.ts | 72 +++-- packages/vite/src/node/ssr/index.ts | 27 +- packages/vite/src/node/utils.ts | 83 ++++++ 12 files changed, 571 insertions(+), 226 deletions(-) create mode 100644 packages/vite/src/node/__tests_dts__/utils.ts diff --git a/packages/vite/src/node/__tests__/utils.spec.ts b/packages/vite/src/node/__tests__/utils.spec.ts index a4586c12df1f70..1e7ca99d88d653 100644 --- a/packages/vite/src/node/__tests__/utils.spec.ts +++ b/packages/vite/src/node/__tests__/utils.spec.ts @@ -10,6 +10,7 @@ import { getLocalhostAddressIfDiffersFromDNS, injectQuery, isFileReadable, + mergeWithDefaults, posToNumber, processSrcSetSync, resolveHostname, @@ -449,3 +450,52 @@ describe('flattenId', () => { expect(result2).toHaveLength(170) }) }) + +describe('mergeWithDefaults', () => { + test('merges with defaults', () => { + const actual = mergeWithDefaults( + { + useDefault: 1, + useValueIfNull: 2, + replaceArray: [0, 1], + nested: { + foo: 'bar', + }, + }, + { + useDefault: undefined, + useValueIfNull: null, + useValueIfNoDefault: 'foo', + replaceArray: [2, 3], + nested: { + foo2: 'bar2', + }, + }, + ) + expect(actual).toStrictEqual({ + useDefault: 1, + useValueIfNull: null, + useValueIfNoDefault: 'foo', + replaceArray: [2, 3], + nested: { + foo: 'bar', + foo2: 'bar2', + }, + }) + + const defaults = { + object: {}, + array: [], + regex: /foo/, + function: () => {}, + } + const actual2 = mergeWithDefaults(defaults, {}) + expect(actual2.object).toStrictEqual({}) + expect(actual2.array).toStrictEqual([]) + expect(actual2.regex).toStrictEqual(/foo/) + expect(actual2.function).toStrictEqual(expect.any(Function)) + // cloned + expect(actual2.object).not.toBe(defaults.object) + expect(actual2.array).not.toBe(defaults.array) + }) +}) diff --git a/packages/vite/src/node/__tests_dts__/utils.ts b/packages/vite/src/node/__tests_dts__/utils.ts new file mode 100644 index 00000000000000..44a28194e708e7 --- /dev/null +++ b/packages/vite/src/node/__tests_dts__/utils.ts @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { Equal, ExpectTrue } from '@type-challenges/utils' +import { mergeWithDefaults } from '../utils' + +const useDefaultTypeForUndefined1 = mergeWithDefaults( + { + foo: 1, + }, + {}, +) + +const useDefaultTypeForUndefined2 = mergeWithDefaults( + { + foo: 1, + }, + { + foo: 2 as number | undefined, + }, +) + +const includeKeyNotIncludedInDefault1 = mergeWithDefaults( + {}, + { + foo: 2, + }, +) + +const extendTypeWithValueType = mergeWithDefaults( + { + foo: 1, + }, + { + foo: 'string' as string | number, + }, +) + +const plainObject = mergeWithDefaults({ foo: { bar: 1 } }, { foo: { baz: 2 } }) + +const nonPlainObject = mergeWithDefaults( + { foo: ['foo'] }, + { foo: [0] as number[] | undefined }, +) + +const optionalNested = mergeWithDefaults({ foo: { bar: true } }, { + foo: { bar: false }, +} as { foo?: { bar?: boolean } }) + +export type cases1 = [ + ExpectTrue>, + ExpectTrue>, + ExpectTrue>, + ExpectTrue>, + ExpectTrue>, + ExpectTrue>, + ExpectTrue< + Equal + >, +] diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 0d0c645da4fb69..bb8e79a5fc6fb6 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -46,6 +46,7 @@ import { emptyDir, getPkgName, joinUrlSegments, + mergeWithDefaults, normalizePath, partialEncodeURIPath, } from './utils' @@ -347,6 +348,44 @@ export interface ResolvedBuildOptions modulePreload: false | ResolvedModulePreloadOptions } +export const buildEnvironmentOptionsDefaults = Object.freeze({ + target: 'modules', + /** @deprecated */ + polyfillModulePreload: true, + modulePreload: true, + outDir: 'dist', + assetsDir: 'assets', + assetsInlineLimit: DEFAULT_ASSETS_INLINE_LIMIT, + // cssCodeSplit + // cssTarget + // cssMinify + sourcemap: false, + // minify + terserOptions: {}, + rollupOptions: {}, + commonjsOptions: { + include: [/node_modules/], + extensions: ['.js', '.cjs'], + }, + dynamicImportVarsOptions: { + warnOnError: true, + exclude: [/node_modules/], + }, + write: true, + emptyOutDir: null, + copyPublicDir: true, + manifest: false, + lib: false, + // ssr + ssrManifest: false, + ssrEmitAssets: false, + // emitAssets + reportCompressedSize: true, + chunkSizeWarningLimit: 500, + watch: null, + // createEnvironment +}) + export function resolveBuildEnvironmentOptions( raw: BuildEnvironmentOptions, logger: Logger, @@ -369,84 +408,49 @@ export function resolveBuildEnvironmentOptions( raw.modulePreload = { polyfill: false } } - const modulePreload = raw.modulePreload - const defaultModulePreload = { - polyfill: true, + const merged = mergeWithDefaults( + { + ...buildEnvironmentOptionsDefaults, + cssCodeSplit: !raw.lib, + minify: consumer === 'server' ? false : 'esbuild', + ssr: consumer === 'server', + emitAssets: consumer === 'client', + createEnvironment: (name, config) => new BuildEnvironment(name, config), + } satisfies BuildEnvironmentOptions, + raw, + ) + + // handle special build targets + if (merged.target === 'modules') { + merged.target = ESBUILD_MODULES_TARGET } - const defaultBuildEnvironmentOptions: BuildEnvironmentOptions = { - outDir: 'dist', - assetsDir: 'assets', - assetsInlineLimit: DEFAULT_ASSETS_INLINE_LIMIT, - cssCodeSplit: !raw.lib, - sourcemap: false, - rollupOptions: {}, - minify: consumer === 'server' ? false : 'esbuild', - terserOptions: {}, - write: true, - emptyOutDir: null, - copyPublicDir: true, - manifest: false, - lib: false, - ssr: consumer === 'server', - ssrManifest: false, - ssrEmitAssets: false, - emitAssets: consumer === 'client', - reportCompressedSize: true, - chunkSizeWarningLimit: 500, - watch: null, - createEnvironment: (name, config) => new BuildEnvironment(name, config), + // normalize false string into actual false + if ((merged.minify as string) === 'false') { + merged.minify = false + } else if (merged.minify === true) { + merged.minify = 'esbuild' } - const userBuildEnvironmentOptions = raw - ? mergeConfig(defaultBuildEnvironmentOptions, raw) - : defaultBuildEnvironmentOptions + const defaultModulePreload = { + polyfill: true, + } - // @ts-expect-error Fallback options instead of merging const resolved: ResolvedBuildEnvironmentOptions = { - target: 'modules', - cssTarget: false, - ...userBuildEnvironmentOptions, - commonjsOptions: { - include: [/node_modules/], - extensions: ['.js', '.cjs'], - ...userBuildEnvironmentOptions.commonjsOptions, - }, - dynamicImportVarsOptions: { - warnOnError: true, - exclude: [/node_modules/], - ...userBuildEnvironmentOptions.dynamicImportVarsOptions, - }, + ...merged, + cssTarget: merged.cssTarget ?? merged.target, + cssMinify: + merged.cssMinify ?? (consumer === 'server' ? 'esbuild' : !!merged.minify), // Resolve to false | object modulePreload: - modulePreload === false + merged.modulePreload === false ? false - : typeof modulePreload === 'object' - ? { + : merged.modulePreload === true + ? defaultModulePreload + : { ...defaultModulePreload, - ...modulePreload, - } - : defaultModulePreload, - } - - // handle special build targets - if (resolved.target === 'modules') { - resolved.target = ESBUILD_MODULES_TARGET - } - - if (!resolved.cssTarget) { - resolved.cssTarget = resolved.target - } - - // normalize false string into actual false - if ((resolved.minify as string) === 'false') { - resolved.minify = false - } else if (resolved.minify === true) { - resolved.minify = 'esbuild' - } - - if (resolved.cssMinify == null) { - resolved.cssMinify = consumer === 'server' ? 'esbuild' : !!resolved.minify + ...merged.modulePreload, + }, } if (isSsrTargetWebworkerEnvironment) { @@ -1503,15 +1507,20 @@ async function defaultBuildApp(builder: ViteBuilder): Promise { } } +export const builderOptionsDefaults = Object.freeze({ + sharedConfigBuild: false, + sharedPlugins: false, + // buildApp +}) + export function resolveBuilderOptions( options: BuilderOptions | undefined, ): ResolvedBuilderOptions | undefined { if (!options) return - return { - sharedConfigBuild: options.sharedConfigBuild ?? false, - sharedPlugins: options.sharedPlugins ?? false, - buildApp: options.buildApp ?? defaultBuildApp, - } + return mergeWithDefaults( + { ...builderOptionsDefaults, buildApp: defaultBuildApp }, + options, + ) } export type ResolvedBuilderOptions = Required diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index e0ff775db7aeb1..ca3fa85f0ad62a 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -15,11 +15,12 @@ import { withTrailingSlash } from '../shared/utils' import { CLIENT_ENTRY, DEFAULT_ASSETS_RE, - DEFAULT_CONDITIONS, + DEFAULT_CLIENT_CONDITIONS, + DEFAULT_CLIENT_MAIN_FIELDS, DEFAULT_CONFIG_FILES, - DEFAULT_EXTENSIONS, - DEFAULT_EXTERNAL_CONDITIONS, - DEFAULT_MAIN_FIELDS, + DEFAULT_PREVIEW_PORT, + DEFAULT_SERVER_CONDITIONS, + DEFAULT_SERVER_MAIN_FIELDS, ENV_ENTRY, FS_PREFIX, } from './constants' @@ -37,9 +38,14 @@ import type { ResolvedBuildOptions, ResolvedBuilderOptions, } from './build' -import { resolveBuildEnvironmentOptions, resolveBuilderOptions } from './build' +import { + buildEnvironmentOptionsDefaults, + builderOptionsDefaults, + resolveBuildEnvironmentOptions, + resolveBuilderOptions, +} from './build' import type { ResolvedServerOptions, ServerOptions } from './server' -import { resolveServerOptions } from './server' +import { resolveServerOptions, serverConfigDefaults } from './server' import { DevEnvironment } from './server/environment' import { createRunnableDevEnvironment } from './server/environments/runnableEnvironment' import type { WebSocketServer } from './server/ws' @@ -48,6 +54,7 @@ import { resolvePreviewOptions } from './preview' import { type CSSOptions, type ResolvedCSSOptions, + cssConfigDefaults, resolveCSSOptions, } from './plugins/css' import { @@ -63,6 +70,7 @@ import { isParentDirectory, mergeAlias, mergeConfig, + mergeWithDefaults, normalizeAlias, normalizePath, } from './utils' @@ -87,7 +95,7 @@ import type { PackageCache } from './packages' import { findNearestNodeModules, findNearestPackageData } from './packages' import { loadEnv, resolveEnvPrefix } from './env' import type { ResolvedSSROptions, SSROptions } from './ssr' -import { resolveSSROptions } from './ssr' +import { resolveSSROptions, ssrConfigDefaults } from './ssr' import { PartialEnvironment } from './baseEnvironment' import { createIdResolver } from './idResolver' @@ -212,7 +220,15 @@ function defaultCreateDevEnvironment(name: string, config: ResolvedConfig) { return createRunnableDevEnvironment(name, config) } -export type ResolvedDevEnvironmentOptions = Required +export type ResolvedDevEnvironmentOptions = Omit< + Required, + 'sourcemapIgnoreList' +> & { + sourcemapIgnoreList: Exclude< + DevEnvironmentOptions['sourcemapIgnoreList'], + false | undefined + > +} type AllResolveOptions = ResolveOptions & { alias?: AliasOptions @@ -522,12 +538,15 @@ export type ResolvedConfig = Readonly< UserConfig, | 'plugins' | 'css' + | 'json' | 'assetsInclude' | 'optimizeDeps' | 'worker' | 'build' | 'dev' | 'environments' + | 'server' + | 'preview' > & { configFile: string | undefined configFileDependencies: string[] @@ -556,6 +575,7 @@ export type ResolvedConfig = Readonly< } plugins: readonly Plugin[] css: ResolvedCSSOptions + json: Required esbuild: ESBuildOptions | false server: ResolvedServerOptions dev: ResolvedDevEnvironmentOptions @@ -581,6 +601,111 @@ export type ResolvedConfig = Readonly< } & PluginHookUtils > +// inferred ones are omitted +export const configDefaults = Object.freeze({ + define: {}, + dev: { + warmup: [], + // preTransformRequests + /** @experimental */ + sourcemap: { js: true }, + sourcemapIgnoreList: undefined, + // createEnvironment + // recoverable + // moduleRunnerTransform + }, + build: buildEnvironmentOptionsDefaults, + resolve: { + // mainFields + // conditions + externalConditions: ['node'], + extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'], + dedupe: [], + /** @experimental */ + noExternal: [], + // external + preserveSymlinks: false, + alias: [], + }, + + // root + base: '/', + publicDir: 'public', + // cacheDir + // mode + plugins: [], + html: { + cspNonce: undefined, + }, + css: cssConfigDefaults, + json: { + namedExports: true, + stringify: 'auto', + }, + // esbuild + assetsInclude: undefined, + /** @experimental */ + builder: builderOptionsDefaults, + server: serverConfigDefaults, + preview: { + port: DEFAULT_PREVIEW_PORT, + // strictPort + // host + // https + // open + // proxy + // cors + // headers + }, + /** @experimental */ + experimental: { + importGlobRestoreExtension: false, + renderBuiltUrl: undefined, + hmrPartialAccept: false, + skipSsrTransform: false, + }, + future: { + removePluginHookHandleHotUpdate: undefined, + removePluginHookSsrArgument: undefined, + removeServerModuleGraph: undefined, + removeServerHot: undefined, + removeServerTransformRequest: undefined, + removeSsrLoadModule: undefined, + }, + legacy: { + proxySsrExternalModules: false, + }, + logLevel: 'info', + customLogger: undefined, + clearScreen: true, + envDir: undefined, + envPrefix: 'VITE_', + worker: { + format: 'iife', + plugins: () => [], + // rollupOptions + }, + optimizeDeps: { + include: [], + exclude: [], + needsInterop: [], + // esbuildOptions + /** @experimental */ + extensions: [], + /** @deprecated @experimental */ + disabled: 'build', + // noDiscovery + /** @experimental */ + holdUntilCrawlEnd: true, + // entries + /** @experimental */ + force: false, + }, + ssr: ssrConfigDefaults, + environments: {}, + appType: 'spa', +} satisfies UserConfig) + export function resolveDevEnvironmentOptions( dev: DevEnvironmentOptions | undefined, environmentName: string | undefined, @@ -588,25 +713,29 @@ export function resolveDevEnvironmentOptions( // Backward compatibility skipSsrTransform?: boolean, ): ResolvedDevEnvironmentOptions { + const resolved = mergeWithDefaults( + { + ...configDefaults.dev, + sourcemapIgnoreList: isInNodeModules, + preTransformRequests: consumer === 'client', + createEnvironment: + environmentName === 'client' + ? defaultCreateClientDevEnvironment + : defaultCreateDevEnvironment, + recoverable: consumer === 'client', + moduleRunnerTransform: + skipSsrTransform !== undefined && consumer === 'server' + ? skipSsrTransform + : consumer === 'server', + }, + dev ?? {}, + ) return { - sourcemap: dev?.sourcemap ?? { js: true }, + ...resolved, sourcemapIgnoreList: - dev?.sourcemapIgnoreList === false + resolved.sourcemapIgnoreList === false ? () => false - : dev?.sourcemapIgnoreList || isInNodeModules, - preTransformRequests: dev?.preTransformRequests ?? consumer === 'client', - warmup: dev?.warmup ?? [], - createEnvironment: - dev?.createEnvironment ?? - (environmentName === 'client' - ? defaultCreateClientDevEnvironment - : defaultCreateDevEnvironment), - recoverable: dev?.recoverable ?? consumer === 'client', - moduleRunnerTransform: - dev?.moduleRunnerTransform ?? - (skipSsrTransform !== undefined && consumer === 'server' - ? skipSsrTransform - : consumer === 'server'), + : resolved.sourcemapIgnoreList, } } @@ -744,34 +873,26 @@ function resolveEnvironmentResolveOptions( // Backward compatibility isSsrTargetWebworkerEnvironment?: boolean, ): ResolvedAllResolveOptions { - let mainFields = resolve?.mainFields - mainFields ??= - consumer === 'client' || isSsrTargetWebworkerEnvironment - ? DEFAULT_MAIN_FIELDS - : DEFAULT_MAIN_FIELDS.filter((f) => f !== 'browser') - - let conditions = resolve?.conditions - conditions ??= - consumer === 'client' || isSsrTargetWebworkerEnvironment - ? DEFAULT_CONDITIONS.filter((c) => c !== 'node') - : DEFAULT_CONDITIONS.filter((c) => c !== 'browser') - - const resolvedResolve: ResolvedAllResolveOptions = { - mainFields, - conditions, - externalConditions: - resolve?.externalConditions ?? DEFAULT_EXTERNAL_CONDITIONS, - external: - resolve?.external ?? - (consumer === 'server' && !isSsrTargetWebworkerEnvironment - ? builtinModules - : []), - noExternal: resolve?.noExternal ?? [], - extensions: resolve?.extensions ?? DEFAULT_EXTENSIONS, - dedupe: resolve?.dedupe ?? [], - preserveSymlinks, - alias, - } + const resolvedResolve: ResolvedAllResolveOptions = mergeWithDefaults( + { + ...configDefaults.resolve, + mainFields: + consumer === 'client' || isSsrTargetWebworkerEnvironment + ? DEFAULT_CLIENT_MAIN_FIELDS + : DEFAULT_SERVER_MAIN_FIELDS, + conditions: + consumer === 'client' || isSsrTargetWebworkerEnvironment + ? DEFAULT_CLIENT_CONDITIONS + : DEFAULT_SERVER_CONDITIONS.filter((c) => c !== 'browser'), + external: + consumer === 'server' && !isSsrTargetWebworkerEnvironment + ? builtinModules + : [], + }, + resolve ?? {}, + ) + resolvedResolve.preserveSymlinks = preserveSymlinks + resolvedResolve.alias = alias if ( // @ts-expect-error removed field @@ -794,8 +915,11 @@ function resolveResolveOptions( logger: Logger, ): ResolvedAllResolveOptions { // resolve alias with internal client alias - const alias = normalizeAlias(mergeAlias(clientAlias, resolve?.alias || [])) - const preserveSymlinks = resolve?.preserveSymlinks ?? false + const alias = normalizeAlias( + mergeAlias(clientAlias, resolve?.alias || configDefaults.resolve.alias), + ) + const preserveSymlinks = + resolve?.preserveSymlinks ?? configDefaults.resolve.preserveSymlinks if (alias.some((a) => a.find === '/')) { logger.warn( @@ -821,22 +945,17 @@ function resolveDepOptimizationOptions( preserveSymlinks: boolean, consumer: 'client' | 'server' | undefined, ): DepOptimizationOptions { - optimizeDeps ??= {} - return { - include: optimizeDeps.include ?? [], - exclude: optimizeDeps.exclude ?? [], - needsInterop: optimizeDeps.needsInterop ?? [], - extensions: optimizeDeps.extensions ?? [], - noDiscovery: optimizeDeps.noDiscovery ?? consumer !== 'client', - holdUntilCrawlEnd: optimizeDeps.holdUntilCrawlEnd ?? true, - esbuildOptions: { - preserveSymlinks, - ...optimizeDeps.esbuildOptions, + return mergeWithDefaults( + { + ...configDefaults.optimizeDeps, + disabled: undefined, // do not set here to avoid deprecation warning + noDiscovery: consumer !== 'client', + esbuildOptions: { + preserveSymlinks, + }, }, - disabled: optimizeDeps.disabled, - entries: optimizeDeps.entries, - force: optimizeDeps.force ?? false, - } + optimizeDeps ?? {}, + ) } export async function resolveConfig( @@ -1117,7 +1236,7 @@ export async function resolveConfig( ? !isBuild || config.build?.ssr ? '/' : './' - : (resolveBaseUrl(config.base, isBuild, logger) ?? '/') + : (resolveBaseUrl(config.base, isBuild, logger) ?? configDefaults.base) // resolve cache directory const pkgDir = findNearestPackageData(resolvedRoot, packageCache)?.dir @@ -1141,7 +1260,9 @@ export async function resolveConfig( ? normalizePath( path.resolve( resolvedRoot, - typeof publicDir === 'string' ? publicDir : 'public', + typeof publicDir === 'string' + ? publicDir + : configDefaults.publicDir, ), ) : '' @@ -1244,6 +1365,7 @@ export async function resolveConfig( isProduction, plugins: userPlugins, // placeholder to be replaced css: resolveCSSOptions(config.css), + json: mergeWithDefaults(configDefaults.json, config.json ?? {}), esbuild: config.esbuild === false ? false @@ -1618,7 +1740,7 @@ async function bundleConfigFile( external: [], noExternal: [], dedupe: [], - extensions: DEFAULT_EXTENSIONS, + extensions: configDefaults.resolve.extensions, preserveSymlinks: false, packageCache, isRequire, diff --git a/packages/vite/src/node/constants.ts b/packages/vite/src/node/constants.ts index 270dbef7b7ac91..0b10a990350817 100644 --- a/packages/vite/src/node/constants.ts +++ b/packages/vite/src/node/constants.ts @@ -39,12 +39,16 @@ export const ROLLUP_HOOKS = [ export const VERSION = version as string -export const DEFAULT_MAIN_FIELDS = [ +const DEFAULT_MAIN_FIELDS = [ 'browser', 'module', 'jsnext:main', // moment still uses this... 'jsnext', ] +export const DEFAULT_CLIENT_MAIN_FIELDS = DEFAULT_MAIN_FIELDS +export const DEFAULT_SERVER_MAIN_FIELDS = DEFAULT_MAIN_FIELDS.filter( + (f) => f !== 'browser', +) /** * A special condition that would be replaced with production or development @@ -52,14 +56,13 @@ export const DEFAULT_MAIN_FIELDS = [ */ export const DEV_PROD_CONDITION = `development|production` as const -export const DEFAULT_CONDITIONS = [ - 'module', - 'browser', - 'node', - DEV_PROD_CONDITION, -] - -export const DEFAULT_EXTERNAL_CONDITIONS = ['node'] +const DEFAULT_CONDITIONS = ['module', 'browser', 'node', DEV_PROD_CONDITION] +export const DEFAULT_CLIENT_CONDITIONS = DEFAULT_CONDITIONS.filter( + (c) => c !== 'node', +) +export const DEFAULT_SERVER_CONDITIONS = DEFAULT_CONDITIONS.filter( + (c) => c !== 'browser', +) // Baseline support browserslist // "defaults and supports es6-module and supports es6-module-dynamic-import" @@ -72,16 +75,6 @@ export const ESBUILD_MODULES_TARGET = [ 'safari14', ] -export const DEFAULT_EXTENSIONS = [ - '.mjs', - '.js', - '.mts', - '.ts', - '.jsx', - '.tsx', - '.json', -] - export const DEFAULT_CONFIG_FILES = [ 'vite.config.js', 'vite.config.mjs', diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 346ae94b2843c6..3f3e35fb962f49 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -68,6 +68,7 @@ import { isExternalUrl, isObject, joinUrlSegments, + mergeWithDefaults, normalizePath, processSrcSet, removeDirectQuery, @@ -185,25 +186,35 @@ export interface CSSModulesOptions { ) => string) } -export type ResolvedCSSOptions = Omit & { - lightningcss?: LightningCSSOptions -} +export const cssConfigDefaults = Object.freeze({ + /** @experimental */ + transformer: 'postcss', + // modules + // preprocessorOptions + /** @experimental */ + preprocessorMaxWorkers: 0, + // postcss + /** @experimental */ + devSourcemap: false, + // lightningcss +} satisfies CSSOptions) + +export type ResolvedCSSOptions = Omit & + Required> & { + lightningcss?: LightningCSSOptions + } export function resolveCSSOptions( options: CSSOptions | undefined, ): ResolvedCSSOptions { - if (options?.transformer === 'lightningcss') { - return { - ...options, - lightningcss: { - ...options.lightningcss, - targets: - options.lightningcss?.targets ?? - convertTargets(ESBUILD_MODULES_TARGET), - }, - } + const resolved = mergeWithDefaults(cssConfigDefaults, options ?? {}) + if (resolved.transformer === 'lightningcss') { + resolved.lightningcss ??= {} + resolved.lightningcss.targets ??= convertTargets(ESBUILD_MODULES_TARGET) + } else { + resolved.lightningcss = undefined } - return { ...options, lightningcss: undefined } + return resolved } const cssModuleRE = new RegExp(`\\.module${CSS_LANGS_RE.source}`) diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 4685f8ea86b783..159a457a76d721 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -69,14 +69,7 @@ export async function resolvePlugins( htmlInlineProxyPlugin(config), cssPlugin(config), config.esbuild !== false ? esbuildPlugin(config) : null, - jsonPlugin( - { - namedExports: true, - stringify: 'auto', - ...config.json, - }, - isBuild, - ), + jsonPlugin(config.json, isBuild), wasmHelperPlugin(), webWorkerPlugin(config), assetPlugin(config), diff --git a/packages/vite/src/node/plugins/json.ts b/packages/vite/src/node/plugins/json.ts index 3ba18f5bb60b29..d531ee80efac7f 100644 --- a/packages/vite/src/node/plugins/json.ts +++ b/packages/vite/src/node/plugins/json.ts @@ -37,7 +37,7 @@ export const isJSONRequest = (request: string): boolean => jsonLangRE.test(request) export function jsonPlugin( - options: JsonOptions = {}, + options: Required, isBuild: boolean, ): Plugin { return { diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index a341f5948b72ec..f149fac30d53d5 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -5,7 +5,6 @@ import compression from '@polka/compression' import connect from 'connect' import type { Connect } from 'dep-types/connect' import corsMiddleware from 'cors' -import { DEFAULT_PREVIEW_PORT } from './constants' import type { HttpServer, ResolvedServerOptions, @@ -35,7 +34,7 @@ import { import { printServerUrls } from './logger' import { bindCLIShortcuts } from './shortcuts' import type { BindCLIShortcutsOptions } from './shortcuts' -import { resolveConfig } from './config' +import { configDefaults, resolveConfig } from './config' import type { InlineConfig, ResolvedConfig } from './config' export interface PreviewOptions extends CommonServerOptions {} @@ -243,7 +242,7 @@ export async function preview( } const hostname = await resolveHostname(options.host) - const port = options.port ?? DEFAULT_PREVIEW_PORT + const port = options.port ?? configDefaults.preview.port await httpServerStart(httpServer, { port, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index ffc8577120a03d..b6bfdf5a211228 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -30,6 +30,7 @@ import { isObject, isParentDirectory, mergeConfig, + mergeWithDefaults, normalizePath, resolveHostname, resolveServerUrls, @@ -1016,33 +1017,64 @@ function resolvedAllowDir(root: string, dir: string): string { return normalizePath(path.resolve(root, dir)) } +export const serverConfigDefaults = Object.freeze({ + port: DEFAULT_DEV_PORT, + strictPort: false, + host: 'localhost', + https: undefined, + open: false, + proxy: {}, + cors: true, + headers: {}, + // hmr + // ws + warmup: { + clientFiles: [], + ssrFiles: [], + }, + // watch + middlewareMode: false, + fs: { + strict: true, + // allow + deny: ['.env', '.env.*', '*.{crt,pem}', '**/.git/**'], + }, + // origin + preTransformRequests: true, + // sourcemapIgnoreList + perEnvironmentStartEndDuringDev: false, + // hotUpdateEnvironments +} satisfies ServerOptions) + export function resolveServerOptions( root: string, raw: ServerOptions | undefined, logger: Logger, ): ResolvedServerOptions { + const _server = mergeWithDefaults( + { + ...serverConfigDefaults, + host: undefined, // do not set here to detect whether host is set or not + sourcemapIgnoreList: isInNodeModules, + }, + raw ?? {}, + ) + const server: ResolvedServerOptions = { - preTransformRequests: true, - perEnvironmentStartEndDuringDev: false, - ...(raw as Omit), + ..._server, + fs: { + ..._server.fs, + // run searchForWorkspaceRoot only if needed + allow: raw?.fs?.allow ?? [searchForWorkspaceRoot(root)], + }, sourcemapIgnoreList: - raw?.sourcemapIgnoreList === false + _server.sourcemapIgnoreList === false ? () => false - : raw?.sourcemapIgnoreList || isInNodeModules, - middlewareMode: raw?.middlewareMode || false, - } - let allowDirs = server.fs?.allow - const deny = server.fs?.deny || [ - '.env', - '.env.*', - '*.{crt,pem}', - '**/.git/**', - ] - - if (!allowDirs) { - allowDirs = [searchForWorkspaceRoot(root)] + : _server.sourcemapIgnoreList, } + let allowDirs = server.fs.allow + if (process.versions.pnp) { // running a command fails if cwd doesn't exist and root may not exist // search for package root to find a path that exists @@ -1074,11 +1106,7 @@ export function resolveServerOptions( allowDirs.push(resolvedClientDir) } - server.fs = { - strict: server.fs?.strict ?? true, - allow: allowDirs, - deny, - } + server.fs.allow = allowDirs if (server.origin?.endsWith('/')) { server.origin = server.origin.slice(0, -1) diff --git a/packages/vite/src/node/ssr/index.ts b/packages/vite/src/node/ssr/index.ts index 34d4dbb9683f3c..45901f069b565c 100644 --- a/packages/vite/src/node/ssr/index.ts +++ b/packages/vite/src/node/ssr/index.ts @@ -1,4 +1,5 @@ import type { DepOptimizationConfig } from '../optimizer' +import { mergeWithDefaults } from '../utils' export type SSRTarget = 'node' | 'webworker' @@ -50,22 +51,20 @@ export interface ResolvedSSROptions extends SSROptions { optimizeDeps: SsrDepOptimizationConfig } +export const ssrConfigDefaults = Object.freeze({ + // noExternal + // external + target: 'node', + optimizeDeps: {}, + // resolve +} satisfies SSROptions) + export function resolveSSROptions( ssr: SSROptions | undefined, preserveSymlinks: boolean, ): ResolvedSSROptions { - ssr ??= {} - const optimizeDeps = ssr.optimizeDeps ?? {} - const target: SSRTarget = 'node' - return { - target, - ...ssr, - optimizeDeps: { - ...optimizeDeps, - esbuildOptions: { - preserveSymlinks, - ...optimizeDeps.esbuildOptions, - }, - }, - } + const defaults = mergeWithDefaults(ssrConfigDefaults, { + optimizeDeps: { esbuildOptions: { preserveSymlinks } }, + } satisfies SSROptions) + return mergeWithDefaults(defaults, ssr ?? {}) } diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 876544cd2e6991..60b78f869cfffa 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -16,6 +16,7 @@ import colors from 'picocolors' import debug from 'debug' import type { Alias, AliasOptions } from 'dep-types/alias' import type MagicString from 'magic-string' +import type { Equal } from '@type-challenges/utils' import type { TransformResult } from 'rollup' import { createFilter as _createFilter } from '@rollup/pluginutils' @@ -1069,6 +1070,88 @@ function backwardCompatibleWorkerPlugins(plugins: any) { return [] } +function deepClone(value: T): T { + if (Array.isArray(value)) { + return value.map((v) => deepClone(v)) as T + } + if (isObject(value)) { + const cloned: Record = {} + for (const key in value) { + cloned[key] = deepClone(value[key]) + } + return cloned as T + } + if (typeof value === 'function') { + return value as T + } + if (value instanceof RegExp) { + return structuredClone(value) + } + if (typeof value === 'object' && value != null) { + throw new Error('Cannot deep clone non-plain object') + } + return value +} + +type MaybeFallback = undefined extends V ? Exclude | D : V + +type MergeWithDefaultsResult = + Equal extends true + ? V + : D extends Function | Array + ? MaybeFallback + : V extends Function | Array + ? MaybeFallback + : D extends Record + ? V extends Record + ? { + [K in keyof D | keyof V]: K extends keyof D + ? K extends keyof V + ? MergeWithDefaultsResult + : D[K] + : K extends keyof V + ? V[K] + : never + } + : MaybeFallback + : MaybeFallback + +function mergeWithDefaultsRecursively< + D extends Record, + V extends Record, +>(defaults: D, values: V): MergeWithDefaultsResult { + const merged: Record = defaults + for (const key in values) { + const value = values[key] + // let null to set the value (e.g. `server.watch: null`) + if (value === undefined) continue + + const existing = merged[key] + if (existing === undefined) { + merged[key] = value + continue + } + + if (isObject(existing) && isObject(value)) { + merged[key] = mergeWithDefaultsRecursively(existing, value) + continue + } + + // use replace even for arrays + merged[key] = value + } + return merged as MergeWithDefaultsResult +} + +export function mergeWithDefaults< + D extends Record, + V extends Record, +>(defaults: D, values: V): MergeWithDefaultsResult { + // NOTE: we need to clone the value here to avoid mutating the defaults + const clonedDefaults = deepClone(defaults) + return mergeWithDefaultsRecursively(clonedDefaults, values) +} + function mergeConfigRecursively( defaults: Record, overrides: Record,