Skip to content

Commit edef29d

Browse files
committed
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
1 parent b60f44c commit edef29d

1 file changed

Lines changed: 79 additions & 75 deletions

File tree

packages/webpack-cli/src/webpack-cli.ts

Lines changed: 79 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)