@@ -2296,6 +2296,84 @@ class WebpackCLI {
22962296 await this . program . parseAsync ( args , parseOptions ) ;
22972297 }
22982298
2299+ // Finds the highest-priority default configuration file (used when no
2300+ // `--config` is passed). Reads each candidate directory once and matches
2301+ // in-memory instead of probing every `<name><ext>` combination with a
2302+ // separate `fs.access` call (up to ~100 sequential syscalls when no config
2303+ // exists). Entries are lowercased so the membership check is case-insensitive;
2304+ // the actual existence is confirmed with `access`, which keeps exact
2305+ // filesystem semantics (case-sensitive or not). When a directory can't be
2306+ // listed (e.g. execute-only permissions), every candidate is probed directly.
2307+ async #findDefaultConfigFile( ) : Promise < string | undefined > {
2308+ const interpret = await import ( "interpret" ) ;
2309+ // Prioritize popular extensions first to avoid unnecessary fs calls
2310+ const seenExtensions = new Set < string > ( ) ;
2311+ const orderedExtensions : string [ ] = [ ] ;
2312+
2313+ for ( const ext of [
2314+ ".js" ,
2315+ ".mjs" ,
2316+ ".cjs" ,
2317+ ".ts" ,
2318+ ".cts" ,
2319+ ".mts" ,
2320+ ...Object . keys ( interpret . extensions ) ,
2321+ ] ) {
2322+ if ( ! seenExtensions . has ( ext ) ) {
2323+ seenExtensions . add ( ext ) ;
2324+ orderedExtensions . push ( ext ) ;
2325+ }
2326+ }
2327+
2328+ const directoryEntriesCache = new Map < string , Set < string > | null > ( ) ;
2329+ const readDirectoryEntries = async ( directory : string ) => {
2330+ let entries = directoryEntriesCache . get ( directory ) ;
2331+
2332+ if ( typeof entries === "undefined" ) {
2333+ try {
2334+ entries = new Set (
2335+ ( await fs . promises . readdir ( directory ) ) . map ( ( entry ) => entry . toLowerCase ( ) ) ,
2336+ ) ;
2337+ } catch {
2338+ entries = null ;
2339+ }
2340+
2341+ directoryEntriesCache . set ( directory , entries ) ;
2342+ }
2343+
2344+ return entries ;
2345+ } ;
2346+
2347+ // Order defines the priority, in decreasing order
2348+ for ( const filename of DEFAULT_CONFIGURATION_FILES ) {
2349+ const resolvedBase = path . resolve ( filename ) ;
2350+ const entries = await readDirectoryEntries ( path . dirname ( resolvedBase ) ) ;
2351+ const basename = path . basename ( resolvedBase ) ;
2352+
2353+ for ( const ext of orderedExtensions ) {
2354+ // Fast path: skip candidates absent from the directory listing. When the
2355+ // directory can't be listed, `entries` is `null`, so probe every
2356+ // candidate directly with `access`.
2357+ if ( entries && ! entries . has ( ( basename + ext ) . toLowerCase ( ) ) ) {
2358+ continue ;
2359+ }
2360+
2361+ const candidate = resolvedBase + ext ;
2362+
2363+ // Confirm with `access` to preserve exact existence semantics (e.g.
2364+ // broken symlinks are listed by `readdir` but fail `access`).
2365+ try {
2366+ await fs . promises . access ( candidate , fs . constants . F_OK ) ;
2367+ return candidate ;
2368+ } catch {
2369+ // Listed but not accessible, keep looking
2370+ }
2371+ }
2372+ }
2373+
2374+ return undefined ;
2375+ }
2376+
22992377 async loadConfig ( options : Options ) {
23002378 const disableInterpret =
23012379 typeof options . disableInterpret !== "undefined" && options . disableInterpret ;
@@ -2467,81 +2545,7 @@ class WebpackCLI {
24672545 }
24682546 }
24692547 } else {
2470- const interpret = await import ( "interpret" ) ;
2471- // Prioritize popular extensions first to avoid unnecessary fs calls
2472- const seenExtensions = new Set < string > ( ) ;
2473- const orderedExtensions : string [ ] = [ ] ;
2474-
2475- for ( const ext of [
2476- ".js" ,
2477- ".mjs" ,
2478- ".cjs" ,
2479- ".ts" ,
2480- ".cts" ,
2481- ".mts" ,
2482- ...Object . keys ( interpret . extensions ) ,
2483- ] ) {
2484- if ( ! seenExtensions . has ( ext ) ) {
2485- seenExtensions . add ( ext ) ;
2486- orderedExtensions . push ( ext ) ;
2487- }
2488- }
2489-
2490- // Read each candidate directory once and match in-memory instead of
2491- // probing every `<name><ext>` combination with a separate `fs.access`
2492- // call (which is up to ~100 sequential syscalls when no config exists).
2493- // Entries are lowercased so the membership check is case-insensitive; the
2494- // actual existence is then confirmed with `access`, which keeps exact
2495- // filesystem semantics (case-sensitive or not) identical to before.
2496- const directoryEntriesCache = new Map < string , Set < string > | null > ( ) ;
2497- const readDirectoryEntries = async ( directory : string ) => {
2498- let entries = directoryEntriesCache . get ( directory ) ;
2499-
2500- if ( typeof entries === "undefined" ) {
2501- try {
2502- entries = new Set (
2503- ( await fs . promises . readdir ( directory ) ) . map ( ( entry ) => entry . toLowerCase ( ) ) ,
2504- ) ;
2505- } catch {
2506- entries = null ;
2507- }
2508-
2509- directoryEntriesCache . set ( directory , entries ) ;
2510- }
2511-
2512- return entries ;
2513- } ;
2514-
2515- let foundDefaultConfigFile ;
2516-
2517- // Order defines the priority, in decreasing order
2518- configFileSearch: for ( const filename of DEFAULT_CONFIGURATION_FILES ) {
2519- const resolvedBase = path . resolve ( filename ) ;
2520- const entries = await readDirectoryEntries ( path . dirname ( resolvedBase ) ) ;
2521- const basename = path . basename ( resolvedBase ) ;
2522-
2523- for ( const ext of orderedExtensions ) {
2524- // Fast path: skip candidates absent from the directory listing. When
2525- // the directory can't be listed (e.g. execute-only permissions),
2526- // `entries` is `null`, so probe every candidate directly with `access`
2527- // to keep discovery working in restricted-permission directories.
2528- if ( entries && ! entries . has ( ( basename + ext ) . toLowerCase ( ) ) ) {
2529- continue ;
2530- }
2531-
2532- const candidate = resolvedBase + ext ;
2533-
2534- // Confirm with `access` to preserve exact existence semantics (e.g.
2535- // broken symlinks are listed by `readdir` but fail `access`).
2536- try {
2537- await fs . promises . access ( candidate , fs . constants . F_OK ) ;
2538- foundDefaultConfigFile = candidate ;
2539- break configFileSearch;
2540- } catch {
2541- // Listed but not accessible, keep looking
2542- }
2543- }
2544- }
2548+ const foundDefaultConfigFile = await this . #findDefaultConfigFile( ) ;
25452549
25462550 if ( foundDefaultConfigFile ) {
25472551 const loadedConfig = await loadConfigByPath ( foundDefaultConfigFile , options . argv ) ;
0 commit comments