From 67d469138b03f691248abcb980c2b29ca97420e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 12:46:31 +0000 Subject: [PATCH 01/12] perf(webpack-cli): cache schema arguments and use map lookups for options Building CLI argument metadata from the webpack/dev-server schema walks a large JSON schema and was repeated within a single run (once per command and again in loadConfig). Memoize it per webpack module and schema. Replace the linear `.find()` scans over the full option list with map/set lookups when matching CLI options in loadConfig and the serve command. https://claude.ai/code/session_01PEtzv6Xqv2yXQaQsZaeoSF --- .changeset/fast-pumas-cache.md | 5 +++ packages/webpack-cli/src/webpack-cli.ts | 43 +++++++++++++++++++++---- 2 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 .changeset/fast-pumas-cache.md diff --git a/.changeset/fast-pumas-cache.md b/.changeset/fast-pumas-cache.md new file mode 100644 index 00000000000..5f17e038d02 --- /dev/null +++ b/.changeset/fast-pumas-cache.md @@ -0,0 +1,5 @@ +--- +"webpack-cli": patch +--- + +Cache CLI argument metadata built from the webpack/dev-server schema and use map lookups instead of linear scans when applying CLI options, reducing redundant work and memory allocations per run. diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index ecdd3196373..5444ae7164b 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -910,13 +910,39 @@ class WebpackCLI { return (error as Error).name === "ValidationError"; } + // Building arguments from the webpack/dev-server schema walks a large JSON + // schema and is repeated within a single run (e.g. once per command and again + // in `loadConfig`). Cache the result per webpack module and schema. + #argumentsCache = new WeakMap< + object, + Map> + >(); + + #getArguments(webpackMod: typeof webpack, schema: Schema) { + let perModuleCache = this.#argumentsCache.get(webpackMod); + + if (!perModuleCache) { + perModuleCache = new Map(); + this.#argumentsCache.set(webpackMod, perModuleCache); + } + + let args = perModuleCache.get(schema); + + if (!args) { + args = webpackMod.cli.getArguments(schema); + perModuleCache.set(schema, args); + } + + return args; + } + schemaToOptions( webpackMod: typeof webpack, schema: Schema = undefined, additionalOptions: CommandOption[] = [], override: Partial = {}, ): CommandOption[] { - const args = webpackMod.cli.getArguments(schema); + const args = this.#getArguments(webpackMod, schema); // Take memory const options: CommandOption[] = Array.from({ length: additionalOptions.length + Object.keys(args).length, @@ -1630,12 +1656,11 @@ class WebpackCLI { const { webpack, webpackOptions, devServerOptions } = cmd.context; const webpackCLIOptions: Options = { webpack, isWatchingLikeCommand: true }; const devServerCLIOptions: CommanderArgs = {}; + const webpackOptionNames = new Set(webpackOptions.map((option) => option.name)); for (const optionName in options) { const kebabedOption = this.toKebabCase(optionName); - const isBuiltInOption = webpackOptions.find( - (builtInOption) => builtInOption.name === kebabedOption, - ); + const isBuiltInOption = webpackOptionNames.has(kebabedOption); if (isBuiltInOption) { webpackCLIOptions[optionName as keyof Options] = options[optionName]; @@ -1678,6 +1703,9 @@ class WebpackCLI { const compilersForDevServer = possibleCompilers.length > 0 ? possibleCompilers : [compilers[0]]; const usedPorts: number[] = []; + const devServerOptionsByName = new Map( + devServerOptions.map((option) => [option.name, option]), + ); for (const compilerForDevServer of compilersForDevServer) { if (compilerForDevServer.options.devServer === false) { @@ -1694,7 +1722,7 @@ class WebpackCLI { if (name === "argv") continue; const kebabName = this.toKebabCase(name); - const arg = devServerOptions.find((item) => item.name === kebabName); + const arg = devServerOptionsByName.get(kebabName); if (arg) { args[name] = arg as unknown as WebpackArgument; @@ -1953,7 +1981,7 @@ class WebpackCLI { async run(args: readonly string[], parseOptions: ParseOptions) { // Default `--color` and `--no-color` options - // eslint-disable-next-line @typescript-eslint/no-this-alias + const self: WebpackCLI = this; // Register own exit @@ -2531,6 +2559,7 @@ class WebpackCLI { const { default: CLIPlugin } = (await import("./plugins/cli-plugin.js")).default; const builtInOptions = this.schemaToOptions(options.webpack); + const builtInOptionsByName = new Map(builtInOptions.map((option) => [option.name, option])); const internalBuildConfig = (configuration: Configuration) => { const originalWatchValue = configuration.watch; @@ -2542,7 +2571,7 @@ class WebpackCLI { if (name === "argv") continue; const kebabName = this.toKebabCase(name); - const arg = builtInOptions.find((item) => item.name === kebabName); + const arg = builtInOptionsByName.get(kebabName); if (arg) { args[name] = arg as unknown as WebpackArgument; From 01dd6ec761130616de852f3eca806aa1088e134b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 17:23:24 +0000 Subject: [PATCH 02/12] perf(webpack-cli): apply CLI options from cached arguments map in loadConfig `loadConfig` rebuilt a full `schemaToOptions` array (~850 objects) and a lookup map on every run just to match passed CLI options. The cached `getArguments()` result is already a name-keyed map of exactly the metadata `processArguments` consumes, so index it directly. Also hoist the per-call `flagsWithAlias` array in `makeOption` to a module-level Set. This roughly halves the time spent in `loadConfig` and removes ~850 object allocations plus a Map per build invocation. https://claude.ai/code/session_01PEtzv6Xqv2yXQaQsZaeoSF --- .changeset/fast-pumas-cache.md | 2 +- packages/webpack-cli/src/webpack-cli.ts | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.changeset/fast-pumas-cache.md b/.changeset/fast-pumas-cache.md index 5f17e038d02..ed4907cfd73 100644 --- a/.changeset/fast-pumas-cache.md +++ b/.changeset/fast-pumas-cache.md @@ -2,4 +2,4 @@ "webpack-cli": patch --- -Cache CLI argument metadata built from the webpack/dev-server schema and use map lookups instead of linear scans when applying CLI options, reducing redundant work and memory allocations per run. +Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. This reduces per-invocation CPU work and memory allocations, most noticeably in `loadConfig`. diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 5444ae7164b..87b6febea2a 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -245,6 +245,9 @@ type Options = const DEFAULT_WEBPACK_PACKAGES: string[] = ["webpack", "loader"]; +// Options that get a single-character alias derived from their name. +const FLAGS_WITH_ALIAS = new Set(["devtool", "output-path", "target", "watch", "extends"]); + class ConfigurationLoadingError extends Error { name = "ConfigurationLoadingError"; @@ -676,9 +679,8 @@ class WebpackCLI { }; let mainOption: MainOption; let negativeOption: NegativeOption | undefined; - const flagsWithAlias = ["devtool", "output-path", "target", "watch", "extends"]; - if (flagsWithAlias.includes(option.name)) { + if (FLAGS_WITH_ALIAS.has(option.name)) { [option.alias] = option.name; } @@ -2558,8 +2560,10 @@ class WebpackCLI { const { default: CLIPlugin } = (await import("./plugins/cli-plugin.js")).default; - const builtInOptions = this.schemaToOptions(options.webpack); - const builtInOptionsByName = new Map(builtInOptions.map((option) => [option.name, option])); + // `getArguments()` already returns a name-keyed map of exactly the argument + // metadata `processArguments` consumes, so use it directly (cached) instead + // of rebuilding a `schemaToOptions` array and a lookup map on every run. + const builtInArgs = this.#getArguments(options.webpack, undefined); const internalBuildConfig = (configuration: Configuration) => { const originalWatchValue = configuration.watch; @@ -2571,10 +2575,10 @@ class WebpackCLI { if (name === "argv") continue; const kebabName = this.toKebabCase(name); - const arg = builtInOptionsByName.get(kebabName); + const arg = builtInArgs[kebabName]; if (arg) { - args[name] = arg as unknown as WebpackArgument; + args[name] = arg; // We really don't know what the value is // eslint-disable-next-line @typescript-eslint/no-explicit-any values[name] = options[name as keyof Options] as any; From f4ae979d51528dbc323463aa1222022a1cb84876 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 17:53:24 +0000 Subject: [PATCH 03/12] perf(webpack-cli): read directories once for default config discovery When no `--config` is passed, config discovery probed every `` combination (3 base names x ~35 extensions = ~100 paths) with a separate sequential `fs.access` call. For config-less builds these all fail, costing ~5-6ms of serialized syscalls. Read each candidate directory once with `readdir`, match names in the same priority order in memory, and confirm the chosen file with a single `access` (preserving exact existence semantics for e.g. broken symlinks). This keeps the common "config exists" path fast while cutting the no-config case from ~100 syscalls to ~2. https://claude.ai/code/session_01PEtzv6Xqv2yXQaQsZaeoSF --- .changeset/fast-pumas-cache.md | 2 +- packages/webpack-cli/src/webpack-cli.ts | 70 ++++++++++++++++++++----- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/.changeset/fast-pumas-cache.md b/.changeset/fast-pumas-cache.md index ed4907cfd73..76903dc2d4e 100644 --- a/.changeset/fast-pumas-cache.md +++ b/.changeset/fast-pumas-cache.md @@ -2,4 +2,4 @@ "webpack-cli": patch --- -Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. This reduces per-invocation CPU work and memory allocations, most noticeably in `loadConfig`. +Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. Default-config discovery now reads each candidate directory once instead of probing every `` combination with a separate `fs.access` call (up to ~100 sequential syscalls when no config file exists). This reduces per-invocation CPU work, syscalls, and memory allocations. diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 87b6febea2a..92c9583a154 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -2356,7 +2356,10 @@ class WebpackCLI { } else { const interpret = await import("interpret"); // Prioritize popular extensions first to avoid unnecessary fs calls - const extensions = new Set([ + const seenExtensions = new Set(); + const orderedExtensions: string[] = []; + + for (const ext of [ ".js", ".mjs", ".cjs", @@ -2364,24 +2367,63 @@ class WebpackCLI { ".cts", ".mts", ...Object.keys(interpret.extensions), - ]); - // Order defines the priority, in decreasing order - const defaultConfigFiles = new Set( - DEFAULT_CONFIGURATION_FILES.flatMap((filename) => - [...extensions].map((ext) => path.resolve(filename + ext)), - ), - ); + ]) { + if (!seenExtensions.has(ext)) { + seenExtensions.add(ext); + orderedExtensions.push(ext); + } + } + + // Read each candidate directory once and match in-memory instead of + // probing every `` combination with a separate `fs.access` + // call (which is up to ~100 sequential syscalls when no config exists). + const directoryEntriesCache = new Map | null>(); + const readDirectoryEntries = async (directory: string) => { + let entries = directoryEntriesCache.get(directory); + + if (typeof entries === "undefined") { + try { + entries = new Set(await fs.promises.readdir(directory)); + } catch { + entries = null; + } + + directoryEntriesCache.set(directory, entries); + } + + return entries; + }; let foundDefaultConfigFile; - for (const defaultConfigFile of defaultConfigFiles) { - try { - await fs.promises.access(defaultConfigFile, fs.constants.F_OK); - foundDefaultConfigFile = defaultConfigFile; - break; - } catch { + // Order defines the priority, in decreasing order + configFileSearch: for (const filename of DEFAULT_CONFIGURATION_FILES) { + const resolvedBase = path.resolve(filename); + const entries = await readDirectoryEntries(path.dirname(resolvedBase)); + + if (!entries) { continue; } + + const basename = path.basename(resolvedBase); + + for (const ext of orderedExtensions) { + if (!entries.has(basename + ext)) { + continue; + } + + const candidate = resolvedBase + ext; + + // Confirm with `access` to preserve exact existence semantics (e.g. + // broken symlinks are listed by `readdir` but fail `access`). + try { + await fs.promises.access(candidate, fs.constants.F_OK); + foundDefaultConfigFile = candidate; + break configFileSearch; + } catch { + // Listed but not accessible, keep looking + } + } } if (foundDefaultConfigFile) { From 5b4e29ee263559733e12270e0c77b364da8a7d47 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 18:58:20 +0000 Subject: [PATCH 04/12] perf(webpack-cli): create colors lazily to avoid loading webpack when unused `#createColors` loads the webpack package to obtain color helpers, but the constructor ran it eagerly on every invocation. Commands like `version` and `info` never otherwise need webpack, so they paid for loading it. Make `colors` a lazy getter so the webpack require only happens when colored output is actually produced. https://claude.ai/code/session_01PEtzv6Xqv2yXQaQsZaeoSF --- .changeset/fast-pumas-cache.md | 2 +- packages/webpack-cli/src/webpack-cli.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.changeset/fast-pumas-cache.md b/.changeset/fast-pumas-cache.md index 76903dc2d4e..c1b4adb734d 100644 --- a/.changeset/fast-pumas-cache.md +++ b/.changeset/fast-pumas-cache.md @@ -2,4 +2,4 @@ "webpack-cli": patch --- -Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. Default-config discovery now reads each candidate directory once instead of probing every `` combination with a separate `fs.access` call (up to ~100 sequential syscalls when no config file exists). This reduces per-invocation CPU work, syscalls, and memory allocations. +Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. Default-config discovery now reads each candidate directory once instead of probing every `` combination with a separate `fs.access` call (up to ~100 sequential syscalls when no config file exists). Colors are also created lazily, so commands that don't need webpack (such as `version` and `info`) no longer load it. This reduces per-invocation CPU work, syscalls, and memory allocations. diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 92c9583a154..d7b33e80616 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -266,7 +266,17 @@ class ConfigurationLoadingError extends Error { } class WebpackCLI { - colors: Colors; + #colors: Colors | undefined; + + // Created lazily because `#createColors` loads the (large) webpack package, + // which commands like `version`/`info` don't otherwise need. + get colors(): Colors { + return (this.#colors ??= this.#createColors()); + } + + set colors(value: Colors) { + this.#colors = value; + } logger: Logger; @@ -275,7 +285,6 @@ class WebpackCLI { program: Command; constructor() { - this.colors = this.#createColors(); this.logger = this.getLogger(); // Initialize program From 10cdeeb087a6938acd69dbf458998a4ba9e8280e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 19:10:32 +0000 Subject: [PATCH 05/12] perf(webpack-cli): hold cached argument metadata via WeakRef The per-schema argument metadata cached for option parsing is large (~1MB for webpack, ~1.85MB including dev-server) but is only needed while setting up a command. Storing it via WeakRef lets the GC reclaim it once setup completes, which matters for long-running serve/watch sessions. A cache miss simply rebuilds it; in practice V8 keeps the target alive across the short setup window, so the dedup within a run (only one schema walk) is preserved. https://claude.ai/code/session_01PEtzv6Xqv2yXQaQsZaeoSF --- .changeset/fast-pumas-cache.md | 2 +- packages/webpack-cli/src/webpack-cli.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.changeset/fast-pumas-cache.md b/.changeset/fast-pumas-cache.md index c1b4adb734d..952283b3638 100644 --- a/.changeset/fast-pumas-cache.md +++ b/.changeset/fast-pumas-cache.md @@ -2,4 +2,4 @@ "webpack-cli": patch --- -Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. Default-config discovery now reads each candidate directory once instead of probing every `` combination with a separate `fs.access` call (up to ~100 sequential syscalls when no config file exists). Colors are also created lazily, so commands that don't need webpack (such as `version` and `info`) no longer load it. This reduces per-invocation CPU work, syscalls, and memory allocations. +Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. Default-config discovery now reads each candidate directory once instead of probing every `` combination with a separate `fs.access` call (up to ~100 sequential syscalls when no config file exists). Colors are also created lazily, so commands that don't need webpack (such as `version` and `info`) no longer load it. The cached argument metadata (~1MB per schema) is held via `WeakRef` so the garbage collector can reclaim it once command setup is done, which matters for long-running `serve`/`watch`. This reduces per-invocation CPU work, syscalls, and memory usage. diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index d7b33e80616..997b88829ae 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -923,10 +923,13 @@ class WebpackCLI { // Building arguments from the webpack/dev-server schema walks a large JSON // schema and is repeated within a single run (e.g. once per command and again - // in `loadConfig`). Cache the result per webpack module and schema. + // in `loadConfig`). Cache the result per webpack module and schema. The values + // are large (~1MB each) and only needed while setting up a command, so they are + // held via `WeakRef` to let the GC reclaim them afterwards (important for + // long-running `serve`/`watch`); a miss simply rebuilds them. #argumentsCache = new WeakMap< object, - Map> + Map>> >(); #getArguments(webpackMod: typeof webpack, schema: Schema) { @@ -937,11 +940,11 @@ class WebpackCLI { this.#argumentsCache.set(webpackMod, perModuleCache); } - let args = perModuleCache.get(schema); + let args = perModuleCache.get(schema)?.deref(); if (!args) { args = webpackMod.cli.getArguments(schema); - perModuleCache.set(schema, args); + perModuleCache.set(schema, new WeakRef(args)); } return args; From cb24a16f2610e88cda60060cdd3e097f10e62cd7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 19:24:11 +0000 Subject: [PATCH 06/12] perf(webpack-cli): stop retaining serve option arrays for the whole session The serve command stashed the full webpack and dev-server option arrays (~900KB) in its command context, which lives for the entire dev-server session even though the arrays are only needed during setup. Build the arrays transiently in the `options` callback for registration, and derive the action's lightweight lookups (built-in option name set, dev-server arg metadata) from the cached `getArguments` map instead. The large arrays are now reclaimable after startup. https://claude.ai/code/session_01PEtzv6Xqv2yXQaQsZaeoSF --- .changeset/fast-pumas-cache.md | 2 +- packages/webpack-cli/src/webpack-cli.ts | 44 ++++++++++--------------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/.changeset/fast-pumas-cache.md b/.changeset/fast-pumas-cache.md index 952283b3638..f17b3ddc064 100644 --- a/.changeset/fast-pumas-cache.md +++ b/.changeset/fast-pumas-cache.md @@ -2,4 +2,4 @@ "webpack-cli": patch --- -Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. Default-config discovery now reads each candidate directory once instead of probing every `` combination with a separate `fs.access` call (up to ~100 sequential syscalls when no config file exists). Colors are also created lazily, so commands that don't need webpack (such as `version` and `info`) no longer load it. The cached argument metadata (~1MB per schema) is held via `WeakRef` so the garbage collector can reclaim it once command setup is done, which matters for long-running `serve`/`watch`. This reduces per-invocation CPU work, syscalls, and memory usage. +Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. Default-config discovery now reads each candidate directory once instead of probing every `` combination with a separate `fs.access` call (up to ~100 sequential syscalls when no config file exists). Colors are also created lazily, so commands that don't need webpack (such as `version` and `info`) no longer load it. The cached argument metadata (~1MB per schema) is held via `WeakRef` so the garbage collector can reclaim it once command setup is done, which matters for long-running `serve`/`watch`. The `serve` command no longer retains the full option arrays (~900KB) in its context for the whole session, deriving the lookups it needs from the cached argument metadata instead. This reduces per-invocation CPU work, syscalls, and memory usage. diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 997b88829ae..0298c8585d8 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -114,28 +114,16 @@ interface WebpackContext { webpack: typeof webpack; } -interface WebpackOptionsContext { - webpackOptions: CommandOption[]; -} - interface WebpackDevServerContext { devServer: typeof import("webpack-dev-server"); } -interface WebpackDevServerOptionsContext { - devServerOptions: CommandOption[]; -} - interface KnownWebpackCLICommands { build: CommandOptions; serve: CommandOptions< string[], CommanderArgs, - WebpackContext & - WebpackOptionsContext & - WebpackDevServerContext & - WebpackDevServerOptionsContext & - Context + WebpackContext & WebpackDevServerContext & Context >; watch: CommandOptions; version: CommandOptions; @@ -1651,26 +1639,31 @@ class WebpackCLI { dependencies: [WEBPACK_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE], preload: async () => { const webpack = await this.loadWebpack(); - const webpackOptions = this.schemaToOptions(webpack, undefined, this.#CLIOptions); const devServer = await this.loadWebpackDevServer(); + + return { webpack, devServer }; + }, + options: (cmd) => { + const { webpack, devServer } = cmd.context; + const webpackOptions = this.schemaToOptions(webpack, undefined, this.#CLIOptions); // @ts-expect-error different versions of the `Schema` type const devServerOptions = this.schemaToOptions(webpack, devServer.schema, undefined, { hidden: false, negativeHidden: false, }); - return { webpack, webpackOptions, devServer, devServerOptions }; - }, - options: (cmd) => { - const { webpackOptions, devServerOptions } = cmd.context; - return [...webpackOptions, ...devServerOptions]; }, action: async (entries: string[], options: CommanderArgs, cmd) => { - const { webpack, webpackOptions, devServerOptions } = cmd.context; + const { webpack, devServer } = cmd.context; const webpackCLIOptions: Options = { webpack, isWatchingLikeCommand: true }; const devServerCLIOptions: CommanderArgs = {}; - const webpackOptionNames = new Set(webpackOptions.map((option) => option.name)); + // Derive the built-in option names from the cached argument metadata + // instead of retaining the full option arrays for the whole session. + const webpackOptionNames = new Set([ + ...this.#CLIOptions.map((option) => option.name), + ...Object.keys(this.#getArguments(webpack, undefined)), + ]); for (const optionName in options) { const kebabedOption = this.toKebabCase(optionName); @@ -1717,9 +1710,8 @@ class WebpackCLI { const compilersForDevServer = possibleCompilers.length > 0 ? possibleCompilers : [compilers[0]]; const usedPorts: number[] = []; - const devServerOptionsByName = new Map( - devServerOptions.map((option) => [option.name, option]), - ); + // @ts-expect-error different versions of the `Schema` type + const devServerArgs = this.#getArguments(webpack, devServer.schema); for (const compilerForDevServer of compilersForDevServer) { if (compilerForDevServer.options.devServer === false) { @@ -1736,10 +1728,10 @@ class WebpackCLI { if (name === "argv") continue; const kebabName = this.toKebabCase(name); - const arg = devServerOptionsByName.get(kebabName); + const arg = devServerArgs[kebabName]; if (arg) { - args[name] = arg as unknown as WebpackArgument; + args[name] = arg; // We really don't know what the value is // eslint-disable-next-line @typescript-eslint/no-explicit-any values[name] = options[name as keyof Options] as any; From 48e97b75218d3b57a548ec613b020d935db51c71 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 19:32:34 +0000 Subject: [PATCH 07/12] fix(webpack-cli): make default config discovery matching case-insensitive The readdir-based discovery matched entry names case-sensitively, which differed from the previous `fs.access` behavior on case-insensitive filesystems (a config named e.g. `Webpack.Config.js` would no longer be found). Lowercase the directory entries for the membership check and rely on the existing `access` confirm for exact existence semantics, so behavior matches the old `fs.access` on both case-sensitive and case-insensitive filesystems while keeping the fast no-config path. https://claude.ai/code/session_01PEtzv6Xqv2yXQaQsZaeoSF --- packages/webpack-cli/src/webpack-cli.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 0298c8585d8..d2b73513161 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -2381,13 +2381,18 @@ class WebpackCLI { // Read each candidate directory once and match in-memory instead of // probing every `` combination with a separate `fs.access` // call (which is up to ~100 sequential syscalls when no config exists). + // Entries are lowercased so the membership check is case-insensitive; the + // actual existence is then confirmed with `access`, which keeps exact + // filesystem semantics (case-sensitive or not) identical to before. const directoryEntriesCache = new Map | null>(); const readDirectoryEntries = async (directory: string) => { let entries = directoryEntriesCache.get(directory); if (typeof entries === "undefined") { try { - entries = new Set(await fs.promises.readdir(directory)); + entries = new Set( + (await fs.promises.readdir(directory)).map((entry) => entry.toLowerCase()), + ); } catch { entries = null; } @@ -2412,7 +2417,7 @@ class WebpackCLI { const basename = path.basename(resolvedBase); for (const ext of orderedExtensions) { - if (!entries.has(basename + ext)) { + if (!entries.has((basename + ext).toLowerCase())) { continue; } From 50a9e7c2593beb6a1a42068eb94f3dede5a681b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 19:46:08 +0000 Subject: [PATCH 08/12] perf(webpack-cli): register only the options present in argv Setting up a command registered all ~850 webpack/dev-server options on commander on every run, which dominated CLI setup time (~28ms) and retained ~1.4MB of Option objects. Register only the options actually present in the argument tokens instead; unrecognized flags still error, and "did you mean" suggestions use the full option-name list stashed on the command. Help still registers every option so it can list them all. Cuts command-setup time roughly in half and CLI overhead by ~35% for a typical build, and drops retained heap by ~1.4MB. https://claude.ai/code/session_01PEtzv6Xqv2yXQaQsZaeoSF --- .changeset/fast-pumas-cache.md | 2 +- packages/webpack-cli/src/webpack-cli.ts | 95 +++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/.changeset/fast-pumas-cache.md b/.changeset/fast-pumas-cache.md index f17b3ddc064..b341e267445 100644 --- a/.changeset/fast-pumas-cache.md +++ b/.changeset/fast-pumas-cache.md @@ -2,4 +2,4 @@ "webpack-cli": patch --- -Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. Default-config discovery now reads each candidate directory once instead of probing every `` combination with a separate `fs.access` call (up to ~100 sequential syscalls when no config file exists). Colors are also created lazily, so commands that don't need webpack (such as `version` and `info`) no longer load it. The cached argument metadata (~1MB per schema) is held via `WeakRef` so the garbage collector can reclaim it once command setup is done, which matters for long-running `serve`/`watch`. The `serve` command no longer retains the full option arrays (~900KB) in its context for the whole session, deriving the lookups it needs from the cached argument metadata instead. This reduces per-invocation CPU work, syscalls, and memory usage. +Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. Default-config discovery now reads each candidate directory once instead of probing every `` combination with a separate `fs.access` call (up to ~100 sequential syscalls when no config file exists). Colors are also created lazily, so commands that don't need webpack (such as `version` and `info`) no longer load it. The cached argument metadata (~1MB per schema) is held via `WeakRef` so the garbage collector can reclaim it once command setup is done, which matters for long-running `serve`/`watch`. The `serve` command no longer retains the full option arrays (~900KB) in its context for the whole session, deriving the lookups it needs from the cached argument metadata instead. Command setup now registers only the options actually present in the arguments instead of all ~850, which roughly halves command-setup time and avoids retaining ~1.4MB of option objects per run (help still lists every option). This reduces per-invocation CPU work, syscalls, and memory usage. diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index d2b73513161..66f4e8e8b85 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -270,6 +270,10 @@ class WebpackCLI { #isColorSupportChanged: boolean | undefined; + // Flag tokens of the current invocation, used to register only the options + // actually present (instead of all ~850) when setting up a command. + #argvForParsing: readonly string[] | undefined; + program: Command; constructor() { @@ -650,7 +654,22 @@ class WebpackCLI { commandOptions = options.options; } + // Keep all option names for "did you mean" suggestions on unknown options, + // since not every option is registered on commander below. + (command as Command & { allOptionNames?: string[] }).allOptionNames = commandOptions.map( + (option) => option.name, + ); + + // For help we register every option (help lists them all). Otherwise we + // register only the options actually present in argv, avoiding the cost of + // building ~850 commander Options per run. Unrecognized flags still error. + const neededOptions = forHelp ? undefined : this.#neededOptionNames(); + for (const option of commandOptions) { + if (neededOptions && !this.#isOptionNeeded(option, neededOptions)) { + continue; + } + this.makeOption(command, option); } } @@ -660,6 +679,61 @@ class WebpackCLI { return command; } + #neededOptionNames(): Set | undefined { + const argv = this.#argvForParsing; + + if (!argv) { + return undefined; + } + + const names = new Set(); + + for (const token of argv) { + // Must start with `-` to name an option. + if (token.length < 2 || token.charCodeAt(0) !== 45) { + continue; + } + + if (token.charCodeAt(1) === 45) { + // Long option: `--name` or `--name=value`. + let name = token.slice(2); + const equalsIndex = name.indexOf("="); + + if (equalsIndex !== -1) { + name = name.slice(0, equalsIndex); + } + + if (!name) { + continue; + } + + names.add(name); + + // `--no-x` must register the `x` option (which provides the negation). + if (name.startsWith("no-")) { + names.add(name.slice(3)); + } + } else { + // Short option: the alias is the first character; the rest (if any) is + // an attached value, e.g. `-d` means `-d `. + names.add(token[1]); + } + } + + return names; + } + + #isOptionNeeded(option: CommandOption, neededOptions: Set): boolean { + if (neededOptions.has(option.name)) { + return true; + } + + // `makeOption` derives a single-character alias for these from the name. + const alias = option.alias ?? (FLAGS_WITH_ALIAS.has(option.name) ? option.name[0] : undefined); + + return typeof alias === "string" && neededOptions.has(alias); + } + makeOption(command: Command, option: CommandOption) { type MainOption = Pick< CommandOption, @@ -2020,12 +2094,16 @@ class WebpackCLI { process.exit(2); } - for (const option of command.options) { - if ( - !(option as Option & { internal?: boolean }).internal && - distance(name, option.long?.slice(2) as string) < 3 - ) { - this.logger.error(`Did you mean '--${option.name()}'?`); + const { allOptionNames } = command as Command & { allOptionNames?: string[] }; + const candidateNames = + allOptionNames ?? + command.options + .filter((option) => !(option as Option & { internal?: boolean }).internal) + .map((option) => option.long?.slice(2) as string); + + for (const candidate of candidateNames) { + if (candidate && distance(name, candidate) < 3) { + this.logger.error(`Did you mean '--${candidate}'?`); } } } @@ -2073,6 +2151,11 @@ class WebpackCLI { this.program.allowExcessArguments(true); this.program.action(async (options) => { const { operands, unknown } = this.program.parseOptions(this.program.args); + + // Remember the flag tokens so command setup only registers options that + // are actually used (see `#neededOptionNames`). + this.#argvForParsing = unknown; + const defaultCommandNameToRun = this.#commands.build.rawName; const hasOperand = typeof operands[0] !== "undefined"; const operand = hasOperand ? operands[0] : defaultCommandNameToRun; From 42a88da89d7467351421eb00fa51f161aead624f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 12:37:59 +0000 Subject: [PATCH 09/12] fix(webpack-cli): address review feedback on lazy option registration - Register every letter of a combined short token (e.g. `-abc`), not just the first, so clustered boolean short flags are not dropped and reported as unknown options. - Include the `--no-` negated forms in the stashed option-name list so "did you mean" suggestions still work for mistyped negated flags. - Fall back to per-candidate `access` probing when a directory can't be listed (e.g. execute-only permissions), so default-config discovery keeps working in restricted-permission directories. https://claude.ai/code/session_01PEtzv6Xqv2yXQaQsZaeoSF --- packages/webpack-cli/src/webpack-cli.ts | 53 ++++++++++++++++++------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 66f4e8e8b85..68e9103cb4f 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -654,11 +654,20 @@ class WebpackCLI { commandOptions = options.options; } - // Keep all option names for "did you mean" suggestions on unknown options, - // since not every option is registered on commander below. - (command as Command & { allOptionNames?: string[] }).allOptionNames = commandOptions.map( - (option) => option.name, - ); + // Keep all option names (including the `no-` negated forms commander + // registers) for "did you mean" suggestions on unknown options, since not + // every option is registered on commander below. + const allOptionNames: string[] = []; + + for (const option of commandOptions) { + allOptionNames.push(option.name); + + if (this.#optionSupportsNegation(option)) { + allOptionNames.push(`no-${option.name}`); + } + } + + (command as Command & { allOptionNames?: string[] }).allOptionNames = allOptionNames; // For help we register every option (help lists them all). Otherwise we // register only the options actually present in argv, avoiding the cost of @@ -714,9 +723,13 @@ class WebpackCLI { names.add(name.slice(3)); } } else { - // Short option: the alias is the first character; the rest (if any) is - // an attached value, e.g. `-d` means `-d `. - names.add(token[1]); + // Short option(s): either a single option with an attached value + // (`-d`) or combined boolean flags (`-abc` => `-a -b -c`). Since + // we can't tell which without the option definitions, register every + // letter; over-registering an unused option is harmless. + for (const char of token.slice(1).split("=", 1)[0]) { + names.add(char); + } } } @@ -734,6 +747,19 @@ class WebpackCLI { return typeof alias === "string" && neededOptions.has(alias); } + // Mirrors when `makeOption` registers a `--no-` negated option. + #optionSupportsNegation(option: CommandOption): boolean { + if (option.configs) { + return option.configs.some( + (config) => + config.type === "boolean" || + (config.type === "enum" && (config.values || []).includes(false)), + ); + } + + return Boolean(option.negative); + } + makeOption(command: Command, option: CommandOption) { type MainOption = Pick< CommandOption, @@ -2492,15 +2518,14 @@ class WebpackCLI { configFileSearch: for (const filename of DEFAULT_CONFIGURATION_FILES) { const resolvedBase = path.resolve(filename); const entries = await readDirectoryEntries(path.dirname(resolvedBase)); - - if (!entries) { - continue; - } - const basename = path.basename(resolvedBase); for (const ext of orderedExtensions) { - if (!entries.has((basename + ext).toLowerCase())) { + // Fast path: skip candidates absent from the directory listing. When + // the directory can't be listed (e.g. execute-only permissions), + // `entries` is `null`, so probe every candidate directly with `access` + // to keep discovery working in restricted-permission directories. + if (entries && !entries.has((basename + ext).toLowerCase())) { continue; } From b60f44c0f0829462dc6d64a715fde2cf65d83216 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 12:42:59 +0000 Subject: [PATCH 10/12] docs(changeset): condense entry to a single sentence https://claude.ai/code/session_01PEtzv6Xqv2yXQaQsZaeoSF --- .changeset/fast-pumas-cache.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fast-pumas-cache.md b/.changeset/fast-pumas-cache.md index b341e267445..8f5eda5505b 100644 --- a/.changeset/fast-pumas-cache.md +++ b/.changeset/fast-pumas-cache.md @@ -2,4 +2,4 @@ "webpack-cli": patch --- -Cache CLI argument metadata built from the webpack/dev-server schema and apply CLI options using the cached name-keyed map directly, avoiding a redundant schema walk and the rebuild of a large options array and lookup map on every run. Default-config discovery now reads each candidate directory once instead of probing every `` combination with a separate `fs.access` call (up to ~100 sequential syscalls when no config file exists). Colors are also created lazily, so commands that don't need webpack (such as `version` and `info`) no longer load it. The cached argument metadata (~1MB per schema) is held via `WeakRef` so the garbage collector can reclaim it once command setup is done, which matters for long-running `serve`/`watch`. The `serve` command no longer retains the full option arrays (~900KB) in its context for the whole session, deriving the lookups it needs from the cached argument metadata instead. Command setup now registers only the options actually present in the arguments instead of all ~850, which roughly halves command-setup time and avoids retaining ~1.4MB of option objects per run (help still lists every option). This reduces per-invocation CPU work, syscalls, and memory usage. +Reduced CLI startup CPU and memory usage by caching schema-derived argument metadata, registering only the options present in the arguments, and reading config directories once during default-config discovery. From edef29d755ab63c30be54fdff3fd0b736c7e8610 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 12:47:38 +0000 Subject: [PATCH 11/12] refactor(webpack-cli): extract default-config discovery into a method Move the candidate-search logic out of `loadConfig` into a dedicated `#findDefaultConfigFile` private method for readability. No behavior change. https://claude.ai/code/session_01PEtzv6Xqv2yXQaQsZaeoSF --- packages/webpack-cli/src/webpack-cli.ts | 154 ++++++++++++------------ 1 file changed, 79 insertions(+), 75 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 68e9103cb4f..67a1895bd4e 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -2296,6 +2296,84 @@ class WebpackCLI { await this.program.parseAsync(args, parseOptions); } + // Finds the highest-priority default configuration file (used when no + // `--config` is passed). Reads each candidate directory once and matches + // in-memory instead of probing every `` combination with a + // separate `fs.access` call (up to ~100 sequential syscalls when no config + // exists). Entries are lowercased so the membership check is case-insensitive; + // the actual existence is confirmed with `access`, which keeps exact + // filesystem semantics (case-sensitive or not). When a directory can't be + // listed (e.g. execute-only permissions), every candidate is probed directly. + async #findDefaultConfigFile(): Promise { + const interpret = await import("interpret"); + // Prioritize popular extensions first to avoid unnecessary fs calls + const seenExtensions = new Set(); + const orderedExtensions: string[] = []; + + for (const ext of [ + ".js", + ".mjs", + ".cjs", + ".ts", + ".cts", + ".mts", + ...Object.keys(interpret.extensions), + ]) { + if (!seenExtensions.has(ext)) { + seenExtensions.add(ext); + orderedExtensions.push(ext); + } + } + + const directoryEntriesCache = new Map | null>(); + const readDirectoryEntries = async (directory: string) => { + let entries = directoryEntriesCache.get(directory); + + if (typeof entries === "undefined") { + try { + entries = new Set( + (await fs.promises.readdir(directory)).map((entry) => entry.toLowerCase()), + ); + } catch { + entries = null; + } + + directoryEntriesCache.set(directory, entries); + } + + return entries; + }; + + // Order defines the priority, in decreasing order + for (const filename of DEFAULT_CONFIGURATION_FILES) { + const resolvedBase = path.resolve(filename); + const entries = await readDirectoryEntries(path.dirname(resolvedBase)); + const basename = path.basename(resolvedBase); + + for (const ext of orderedExtensions) { + // Fast path: skip candidates absent from the directory listing. When the + // directory can't be listed, `entries` is `null`, so probe every + // candidate directly with `access`. + if (entries && !entries.has((basename + ext).toLowerCase())) { + continue; + } + + const candidate = resolvedBase + ext; + + // Confirm with `access` to preserve exact existence semantics (e.g. + // broken symlinks are listed by `readdir` but fail `access`). + try { + await fs.promises.access(candidate, fs.constants.F_OK); + return candidate; + } catch { + // Listed but not accessible, keep looking + } + } + } + + return undefined; + } + async loadConfig(options: Options) { const disableInterpret = typeof options.disableInterpret !== "undefined" && options.disableInterpret; @@ -2467,81 +2545,7 @@ class WebpackCLI { } } } else { - const interpret = await import("interpret"); - // Prioritize popular extensions first to avoid unnecessary fs calls - const seenExtensions = new Set(); - const orderedExtensions: string[] = []; - - for (const ext of [ - ".js", - ".mjs", - ".cjs", - ".ts", - ".cts", - ".mts", - ...Object.keys(interpret.extensions), - ]) { - if (!seenExtensions.has(ext)) { - seenExtensions.add(ext); - orderedExtensions.push(ext); - } - } - - // Read each candidate directory once and match in-memory instead of - // probing every `` combination with a separate `fs.access` - // call (which is up to ~100 sequential syscalls when no config exists). - // Entries are lowercased so the membership check is case-insensitive; the - // actual existence is then confirmed with `access`, which keeps exact - // filesystem semantics (case-sensitive or not) identical to before. - const directoryEntriesCache = new Map | null>(); - const readDirectoryEntries = async (directory: string) => { - let entries = directoryEntriesCache.get(directory); - - if (typeof entries === "undefined") { - try { - entries = new Set( - (await fs.promises.readdir(directory)).map((entry) => entry.toLowerCase()), - ); - } catch { - entries = null; - } - - directoryEntriesCache.set(directory, entries); - } - - return entries; - }; - - let foundDefaultConfigFile; - - // Order defines the priority, in decreasing order - configFileSearch: for (const filename of DEFAULT_CONFIGURATION_FILES) { - const resolvedBase = path.resolve(filename); - const entries = await readDirectoryEntries(path.dirname(resolvedBase)); - const basename = path.basename(resolvedBase); - - for (const ext of orderedExtensions) { - // Fast path: skip candidates absent from the directory listing. When - // the directory can't be listed (e.g. execute-only permissions), - // `entries` is `null`, so probe every candidate directly with `access` - // to keep discovery working in restricted-permission directories. - if (entries && !entries.has((basename + ext).toLowerCase())) { - continue; - } - - const candidate = resolvedBase + ext; - - // Confirm with `access` to preserve exact existence semantics (e.g. - // broken symlinks are listed by `readdir` but fail `access`). - try { - await fs.promises.access(candidate, fs.constants.F_OK); - foundDefaultConfigFile = candidate; - break configFileSearch; - } catch { - // Listed but not accessible, keep looking - } - } - } + const foundDefaultConfigFile = await this.#findDefaultConfigFile(); if (foundDefaultConfigFile) { const loadedConfig = await loadConfigByPath(foundDefaultConfigFile, options.argv); From 1d130baa0c4a9d60fd19aec8a5c47682809cdcdb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 12:54:33 +0000 Subject: [PATCH 12/12] docs(webpack-cli): condense comments to single sentences https://claude.ai/code/session_01PEtzv6Xqv2yXQaQsZaeoSF --- packages/webpack-cli/src/webpack-cli.ts | 33 +++++-------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 67a1895bd4e..3b897068c60 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -654,9 +654,7 @@ class WebpackCLI { commandOptions = options.options; } - // Keep all option names (including the `no-` negated forms commander - // registers) for "did you mean" suggestions on unknown options, since not - // every option is registered on commander below. + // Keep all option names (including `no-` negated forms) for "did you mean" suggestions, since not every option is registered below. const allOptionNames: string[] = []; for (const option of commandOptions) { @@ -669,9 +667,7 @@ class WebpackCLI { (command as Command & { allOptionNames?: string[] }).allOptionNames = allOptionNames; - // For help we register every option (help lists them all). Otherwise we - // register only the options actually present in argv, avoiding the cost of - // building ~850 commander Options per run. Unrecognized flags still error. + // Register every option for help, otherwise only the ones present in argv. const neededOptions = forHelp ? undefined : this.#neededOptionNames(); for (const option of commandOptions) { @@ -723,10 +719,7 @@ class WebpackCLI { names.add(name.slice(3)); } } else { - // Short option(s): either a single option with an attached value - // (`-d`) or combined boolean flags (`-abc` => `-a -b -c`). Since - // we can't tell which without the option definitions, register every - // letter; over-registering an unused option is harmless. + // Register every letter of a short token to cover both attached values (`-d`) and combined flags (`-abc`); over-registering is harmless. for (const char of token.slice(1).split("=", 1)[0]) { names.add(char); } @@ -1009,12 +1002,7 @@ class WebpackCLI { return (error as Error).name === "ValidationError"; } - // Building arguments from the webpack/dev-server schema walks a large JSON - // schema and is repeated within a single run (e.g. once per command and again - // in `loadConfig`). Cache the result per webpack module and schema. The values - // are large (~1MB each) and only needed while setting up a command, so they are - // held via `WeakRef` to let the GC reclaim them afterwards (important for - // long-running `serve`/`watch`); a miss simply rebuilds them. + // Cache the expensive schema-to-arguments walk per webpack module and schema, held via `WeakRef` so the GC can reclaim the ~1MB result after command setup (a miss simply rebuilds it). #argumentsCache = new WeakMap< object, Map>> @@ -2296,14 +2284,7 @@ class WebpackCLI { await this.program.parseAsync(args, parseOptions); } - // Finds the highest-priority default configuration file (used when no - // `--config` is passed). Reads each candidate directory once and matches - // in-memory instead of probing every `` combination with a - // separate `fs.access` call (up to ~100 sequential syscalls when no config - // exists). Entries are lowercased so the membership check is case-insensitive; - // the actual existence is confirmed with `access`, which keeps exact - // filesystem semantics (case-sensitive or not). When a directory can't be - // listed (e.g. execute-only permissions), every candidate is probed directly. + // Finds the highest-priority default config file by reading each candidate directory once (case-insensitively) and confirming with `access`, instead of probing every `` combination separately. async #findDefaultConfigFile(): Promise { const interpret = await import("interpret"); // Prioritize popular extensions first to avoid unnecessary fs calls @@ -2351,9 +2332,7 @@ class WebpackCLI { const basename = path.basename(resolvedBase); for (const ext of orderedExtensions) { - // Fast path: skip candidates absent from the directory listing. When the - // directory can't be listed, `entries` is `null`, so probe every - // candidate directly with `access`. + // Skip candidates absent from the listing, but when the directory can't be listed (`entries` is `null`) probe every candidate directly. if (entries && !entries.has((basename + ext).toLowerCase())) { continue; }