load-oxfmt-config 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +57 -11
  2. package/dist/index.d.mts +146 -129
  3. package/dist/index.mjs +571 -466
  4. package/package.json +22 -44
package/dist/index.mjs CHANGED
@@ -7,6 +7,7 @@ import { readFile, realpath, stat } from "node:fs/promises";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { parse, printParseErrorCode } from "jsonc-parser";
9
9
  import { parseBuffer } from "editorconfig";
10
+ import { Minimatch } from "minimatch";
10
11
  import ignore from "ignore";
11
12
  //#region src/constants.ts
12
13
  /**
@@ -79,6 +80,26 @@ const EDITORCONFIG_GLOBAL_SECTION_NAMES = ["*"];
79
80
  //#endregion
80
81
  //#region src/utils.ts
81
82
  /**
83
+ * Return a cached promise by key, creating and storing it on miss.
84
+ *
85
+ * If the promise rejects, the cache entry is removed so future calls can retry.
86
+ *
87
+ * @param cache - Map used to store inflight/resolved promises.
88
+ * @param key - Cache key.
89
+ * @param factory - Factory to create the promise when missing.
90
+ * @returns Cached or newly created promise.
91
+ */
92
+ function cachePromise(cache, key, factory) {
93
+ const cached = getCacheValue(cache, key);
94
+ if (cached) return cached;
95
+ const task = factory().catch((error) => {
96
+ if (cache.get(key) === task) cache.delete(key);
97
+ throw error;
98
+ });
99
+ setCacheValue(cache, key, task);
100
+ return task;
101
+ }
102
+ /**
82
103
  * Read a cache value and move it to the most-recent position.
83
104
  *
84
105
  * @param cache - Cache map to read.
@@ -93,6 +114,19 @@ function getCacheValue(cache, key) {
93
114
  return value;
94
115
  }
95
116
  /**
117
+ * Check whether a relative path points outside its intended base directory.
118
+ *
119
+ * This also recognizes Windows drive-qualified paths when running on another
120
+ * platform so the behavior can be tested consistently.
121
+ *
122
+ * @param path - Relative path candidate.
123
+ * @returns True when the path escapes or is absolute.
124
+ */
125
+ function isPathOutsideBase(path) {
126
+ const normalizedPath = toPosixPath(path);
127
+ return normalizedPath === ".." || normalizedPath.startsWith("../") || normalizedPath.startsWith("/") || /^[A-Za-z]:\//u.test(normalizedPath);
128
+ }
129
+ /**
96
130
  * Store a cache value and evict the least-recent entry when over capacity.
97
131
  *
98
132
  * @param cache - Cache map to update.
@@ -107,35 +141,6 @@ function setCacheValue(cache, key, value) {
107
141
  if (!oldestKey.done) cache.delete(oldestKey.value);
108
142
  }
109
143
  /**
110
- * Return a cached promise by key, creating and storing it on miss.
111
- *
112
- * If the promise rejects, the cache entry is removed so future calls can retry.
113
- *
114
- * @param cache - Map used to store inflight/resolved promises.
115
- * @param key - Cache key.
116
- * @param factory - Factory to create the promise when missing.
117
- * @returns Cached or newly created promise.
118
- */
119
- function cachePromise(cache, key, factory) {
120
- const cached = getCacheValue(cache, key);
121
- if (cached) return cached;
122
- const task = factory().catch((error) => {
123
- if (cache.get(key) === task) cache.delete(key);
124
- throw error;
125
- });
126
- setCacheValue(cache, key, task);
127
- return task;
128
- }
129
- /**
130
- * Normalize a filesystem path to POSIX-style separators.
131
- *
132
- * @param path - Original path.
133
- * @returns Path using `/` as separator.
134
- */
135
- function toPosixPath(path) {
136
- return path.replaceAll("\\", "/");
137
- }
138
- /**
139
144
  * Split a path into non-empty segments.
140
145
  *
141
146
  * @param path - Original path.
@@ -145,17 +150,13 @@ function splitPathSegments(path) {
145
150
  return path.split(/[\\/]+/u).filter(Boolean);
146
151
  }
147
152
  /**
148
- * Check whether a relative path points outside its intended base directory.
149
- *
150
- * This also recognizes Windows drive-qualified paths when running on another
151
- * platform so the behavior can be tested consistently.
153
+ * Normalize a filesystem path to POSIX-style separators.
152
154
  *
153
- * @param path - Relative path candidate.
154
- * @returns True when the path escapes or is absolute.
155
+ * @param path - Original path.
156
+ * @returns Path using `/` as separator.
155
157
  */
156
- function isPathOutsideBase(path) {
157
- const normalizedPath = toPosixPath(path);
158
- return normalizedPath === ".." || normalizedPath.startsWith("../") || normalizedPath.startsWith("/") || /^[A-Za-z]:\//u.test(normalizedPath);
158
+ function toPosixPath(path) {
159
+ return path.replaceAll("\\", "/");
159
160
  }
160
161
  //#endregion
161
162
  //#region src/config.ts
@@ -185,105 +186,109 @@ const syntaxFallbackMessages = [
185
186
  */
186
187
  const freshJavaScriptModuleCache = /* @__PURE__ */ new Map();
187
188
  /**
188
- * Check whether an unknown config export is an object accepted by oxfmt.
189
- *
190
- * @param value - Runtime value to validate.
191
- * @returns True when the value can be treated as an oxfmt options object.
192
- */
193
- function isConfigObject(value) {
194
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
195
- const prototype = Object.getPrototypeOf(value);
196
- return prototype === Object.prototype || prototype === null;
197
- }
198
- /**
199
- * Read a parsed JSON-like config value as an oxfmt config object.
200
- *
201
- * @param config - Runtime config value.
202
- * @returns Oxfmt config object.
203
- * @throws When the config value is not an object.
204
- */
205
- function readParsedConfigObject(config) {
206
- if (!isConfigObject(config)) throw new Error("Configuration file must be an object.");
207
- return config;
208
- }
209
- /**
210
- * Read the default export from a JavaScript or TypeScript config module.
189
+ * Build a cache key for config content; prefixes missing entries.
211
190
  *
212
- * @param mod - Imported config module namespace.
213
- * @returns Oxfmt config object from the module default export.
214
- * @throws When the module has no default export or the default export is not an object.
191
+ * @param resolvedPath - Resolved oxfmt config path.
192
+ * @param editorconfigPath - Resolved editorconfig path.
193
+ * @param resolveKey - Resolution cache key.
194
+ * @returns Cache key for config content cache.
215
195
  */
216
- function readConfigDefaultExport(mod) {
217
- if (!hasOwn(mod, "default")) throw new Error("Configuration file has no default export.");
218
- const config = mod["default"];
219
- if (!isConfigObject(config)) throw new Error("Configuration file must have a default export that is an object.");
220
- return config;
196
+ function getConfigCacheKey(resolvedPath, editorconfigPath, resolveKey) {
197
+ return JSON.stringify([
198
+ resolvedPath ?? null,
199
+ editorconfigPath ?? null,
200
+ resolveKey
201
+ ]);
221
202
  }
222
203
  /**
223
- * Read a CommonJS module export as an oxfmt config object.
204
+ * Build a cache key for path resolution (cwd + configPath).
224
205
  *
225
- * @param config - Runtime CommonJS export value.
226
- * @returns Oxfmt config object from the module export.
227
- * @throws When the module export is not an object.
206
+ * @param cwd - Current working directory.
207
+ * @param configPath - Optional config path.
208
+ * @returns Cache key for resolve cache.
228
209
  */
229
- function readConfigModuleExports(config) {
230
- if (!isConfigObject(config)) throw new Error("Configuration file must export an object.");
231
- return config;
210
+ function getResolveCacheKey(cwd, configPath) {
211
+ return JSON.stringify([cwd, configPath ?? null]);
232
212
  }
233
213
  /**
234
- * Import a JavaScript or TypeScript config module through jiti.
214
+ * Read and parse an oxfmt config file.
235
215
  *
236
- * @param resolvedPath - Absolute config file path to import.
237
- * @param useCache - Whether jiti should use its filesystem and module caches.
238
- * @returns Imported module namespace.
216
+ * @param resolvedPath - Absolute path to config file.
217
+ * @param options - Config loading options.
218
+ * @param options.useCache - Whether to reuse cached config modules.
219
+ * @returns Parsed config object.
239
220
  */
240
- async function importJitiConfigModule(resolvedPath, useCache) {
241
- return (await interopDefault(import("jiti")))(import.meta.url, useCache ? void 0 : {
242
- fsCache: false,
243
- moduleCache: false
244
- }).import(resolvedPath);
221
+ async function readConfigFromFile(resolvedPath, options = {}) {
222
+ const extension = extname(resolvedPath);
223
+ const useCache = options.useCache !== false;
224
+ if ([
225
+ ".cjs",
226
+ ".cts",
227
+ ".js",
228
+ ".mjs",
229
+ ".mts",
230
+ ".ts"
231
+ ].includes(extension)) {
232
+ if (!useCache && extension === ".cjs") return requireFreshCommonJSConfig(resolvedPath);
233
+ if (!useCache && extension === ".mjs") return readConfigDefaultExport(await importNativeFreshModule(resolvedPath));
234
+ if (!useCache && extension === ".js") return readConfigDefaultExport(await importFreshJavaScriptConfigModule(resolvedPath));
235
+ return readConfigDefaultExport(await importJitiConfigModule(resolvedPath, useCache));
236
+ }
237
+ const content = await readFile(resolvedPath, "utf8");
238
+ if (extension === ".jsonc") {
239
+ const parseErrors = [];
240
+ const parsed = parse(content, parseErrors, {
241
+ allowEmptyContent: true,
242
+ allowTrailingComma: true
243
+ });
244
+ if (parseErrors.length > 0) {
245
+ const firstError = parseErrors[0];
246
+ const errorCode = isUndefined(firstError) ? "Unknown" : printParseErrorCode(firstError.error);
247
+ throw new Error(`Invalid JSONC syntax: ${errorCode}`);
248
+ }
249
+ return readParsedConfigObject(isUndefined(parsed) && content.trim() === "" ? {} : parsed);
250
+ }
251
+ if (extension === ".json") return readParsedConfigObject(JSON.parse(content));
252
+ if (!extension) return readParsedConfigObject(JSON.parse(content));
253
+ if (!OXFMT_EXPLICIT_CONFIG_EXTENSIONS.includes(extension)) throw new Error(`Unsupported oxfmt config extension "${extension}" at ${resolvedPath}`);
254
+ return readParsedConfigObject(JSON.parse(content));
245
255
  }
246
256
  /**
247
- * Build a cache-busting import key from entry file contents.
257
+ * Resolve the oxfmt config file path.
248
258
  *
249
- * @param resolvedPath - Absolute config file path.
250
- * @returns Stable key that changes when the entry file contents change.
251
- */
252
- async function getFreshImportCacheKey(resolvedPath) {
253
- const content = await readFile(resolvedPath);
254
- return createHash("sha256").update(content).digest("hex");
255
- }
256
- /**
257
- * Read a Node-style `code` field from an unknown thrown value.
259
+ * - If `configPath` is provided, absolute paths are returned as-is;
260
+ * relative paths are joined to `cwd`.
261
+ * - Otherwise, walk upward from `cwd` to find known filenames.
258
262
  *
259
- * @param error - Thrown value to inspect.
260
- * @returns Error code when present.
261
- */
262
- function getErrorCode(error) {
263
- return isObject(error) && "code" in error && isString(error.code) ? error.code : void 0;
264
- }
265
- /**
266
- * Check whether a loader error should continue the JS config fallback chain.
263
+ * @param cwd - Starting directory for resolution.
264
+ * @param configPath - Optional explicit path.
265
+ * @returns Absolute path to config file, or undefined when not found.
267
266
  *
268
- * @param error - Thrown value from require or native import.
269
- * @param resolvedPaths - Entry module paths that may appear in parser errors.
270
- * @returns True when the error is from loader compatibility, not config execution.
271
- */
272
- function isJavaScriptLoaderFallbackError(error, resolvedPaths) {
273
- const code = getErrorCode(error);
274
- if (code && requireFallbackErrorCodes.has(code)) return true;
275
- return error instanceof SyntaxError && resolvedPaths.some((resolvedPath) => error.stack?.startsWith(`${resolvedPath}:`) === true) && syntaxFallbackMessages.some((pattern) => pattern.test(error.message));
276
- }
277
- /**
278
- * Import an ESM config module with entry-file cache busting.
267
+ * @example
268
+ * ```ts
269
+ * import { resolveOxfmtrcPath } from 'load-oxfmt-config'
279
270
  *
280
- * @param resolvedPath - Absolute config file path to import.
281
- * @returns Imported module namespace.
271
+ * const path = await resolveOxfmtrcPath(process.cwd())
272
+ * ```
282
273
  */
283
- async function importNativeFreshModule(resolvedPath) {
284
- const url = pathToFileURL(resolvedPath);
285
- url.searchParams.set("oxfmtConfigCacheBust", await getFreshImportCacheKey(resolvedPath));
286
- return import(url.href);
274
+ async function resolveOxfmtrcPath(cwd, configPath) {
275
+ const resolvedCwd = resolve(cwd);
276
+ if (configPath) return isAbsolute(configPath) ? configPath : join(resolvedCwd, configPath);
277
+ let currentDir = resolvedCwd;
278
+ while (true) {
279
+ for (const filename of OXFMT_CONFIG_FILES) {
280
+ const configFilePath = join(currentDir, filename);
281
+ try {
282
+ if ((await stat(configFilePath)).isFile()) return configFilePath;
283
+ } catch (error) {
284
+ const code = error.code;
285
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
286
+ }
287
+ }
288
+ const parentDir = join(currentDir, "..");
289
+ if (parentDir === currentDir) break;
290
+ currentDir = parentDir;
291
+ }
287
292
  }
288
293
  /**
289
294
  * Delete a CommonJS module and all cached children reachable from it.
@@ -301,26 +306,23 @@ function deleteRequireCacheTree(requirePath, seen = /* @__PURE__ */ new Set()) {
301
306
  Reflect.deleteProperty(require.cache, requirePath);
302
307
  }
303
308
  /**
304
- * Require a CommonJS module after removing it from Node's require cache.
309
+ * Read a Node-style `code` field from an unknown thrown value.
305
310
  *
306
- * @param resolvedPath - Absolute config file path to require.
307
- * @returns Runtime module export value.
311
+ * @param error - Thrown value to inspect.
312
+ * @returns Error code when present.
308
313
  */
309
- function requireFreshModule(resolvedPath) {
310
- const requirePath = require.resolve(resolvedPath);
311
- deleteRequireCacheTree(requirePath);
312
- return require(requirePath);
314
+ function getErrorCode(error) {
315
+ return isObject(error) && "code" in error && isString(error.code) ? error.code : void 0;
313
316
  }
314
317
  /**
315
- * Load a CommonJS config file without reusing Node's require cache.
318
+ * Build a cache-busting import key from entry file contents.
316
319
  *
317
- * @param resolvedPath - Absolute `.cjs` config file path.
318
- * @returns Oxfmt config object from default or module exports.
320
+ * @param resolvedPath - Absolute config file path.
321
+ * @returns Stable key that changes when the entry file contents change.
319
322
  */
320
- function requireFreshCommonJSConfig(resolvedPath) {
321
- const configModule = requireFreshModule(resolvedPath);
322
- if (Object.prototype.toString.call(configModule) === "[object Module]") return readConfigDefaultExport(configModule);
323
- return readConfigModuleExports(configModule);
323
+ async function getFreshImportCacheKey(resolvedPath) {
324
+ const content = await readFile(resolvedPath);
325
+ return createHash("sha256").update(content).digest("hex");
324
326
  }
325
327
  /**
326
328
  * Load a JavaScript config file without reusing stale module contents.
@@ -372,105 +374,117 @@ async function importFreshJavaScriptConfigModule(resolvedPath) {
372
374
  }
373
375
  }
374
376
  /**
375
- * Build a cache key for path resolution (cwd + configPath).
377
+ * Import a JavaScript or TypeScript config module through jiti.
376
378
  *
377
- * @param cwd - Current working directory.
378
- * @param configPath - Optional config path.
379
- * @returns Cache key for resolve cache.
379
+ * @param resolvedPath - Absolute config file path to import.
380
+ * @param useCache - Whether jiti should use its filesystem and module caches.
381
+ * @returns Imported module namespace.
380
382
  */
381
- function getResolveCacheKey(cwd, configPath) {
382
- return JSON.stringify([cwd, configPath ?? null]);
383
+ async function importJitiConfigModule(resolvedPath, useCache) {
384
+ return (await interopDefault(import("jiti")))(import.meta.url, useCache ? void 0 : {
385
+ fsCache: false,
386
+ moduleCache: false
387
+ }).import(resolvedPath);
383
388
  }
384
389
  /**
385
- * Build a cache key for config content; prefixes missing entries.
390
+ * Import an ESM config module with entry-file cache busting.
386
391
  *
387
- * @param resolvedPath - Resolved oxfmt config path.
388
- * @param editorconfigPath - Resolved editorconfig path.
389
- * @param resolveKey - Resolution cache key.
390
- * @returns Cache key for config content cache.
392
+ * @param resolvedPath - Absolute config file path to import.
393
+ * @returns Imported module namespace.
391
394
  */
392
- function getConfigCacheKey(resolvedPath, editorconfigPath, resolveKey) {
393
- return JSON.stringify([
394
- resolvedPath ?? null,
395
- editorconfigPath ?? null,
396
- resolveKey
397
- ]);
395
+ async function importNativeFreshModule(resolvedPath) {
396
+ const url = pathToFileURL(resolvedPath);
397
+ url.searchParams.set("oxfmtConfigCacheBust", await getFreshImportCacheKey(resolvedPath));
398
+ return import(url.href);
398
399
  }
399
400
  /**
400
- * Resolve the oxfmt config file path.
401
- *
402
- * - If `configPath` is provided, absolute paths are returned as-is;
403
- * relative paths are joined to `cwd`.
404
- * - Otherwise, walk upward from `cwd` to find known filenames.
401
+ * Check whether an unknown config export is an object accepted by oxfmt.
405
402
  *
406
- * @param cwd - Starting directory for resolution.
407
- * @param configPath - Optional explicit path.
408
- * @returns Absolute path to config file, or undefined when not found.
403
+ * @param value - Runtime value to validate.
404
+ * @returns True when the value can be treated as an oxfmt options object.
405
+ */
406
+ function isConfigObject(value) {
407
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
408
+ const prototype = Object.getPrototypeOf(value);
409
+ return prototype === Object.prototype || prototype === null;
410
+ }
411
+ /**
412
+ * Check whether a loader error should continue the JS config fallback chain.
409
413
  *
410
- * @example
411
- * ```ts
412
- * import { resolveOxfmtrcPath } from 'load-oxfmt-config'
414
+ * @param error - Thrown value from require or native import.
415
+ * @param resolvedPaths - Entry module paths that may appear in parser errors.
416
+ * @returns True when the error is from loader compatibility, not config execution.
417
+ */
418
+ function isJavaScriptLoaderFallbackError(error, resolvedPaths) {
419
+ const code = getErrorCode(error);
420
+ if (code && requireFallbackErrorCodes.has(code)) return true;
421
+ return error instanceof SyntaxError && resolvedPaths.some((resolvedPath) => error.stack?.startsWith(`${resolvedPath}:`) === true) && syntaxFallbackMessages.some((pattern) => pattern.test(error.message));
422
+ }
423
+ /**
424
+ * Read the default export from a JavaScript or TypeScript config module.
413
425
  *
414
- * const path = await resolveOxfmtrcPath(process.cwd())
415
- * ```
426
+ * @param mod - Imported config module namespace.
427
+ * @returns Oxfmt config object from the module default export.
428
+ * @throws When the module has no default export or the default export is not an object.
416
429
  */
417
- async function resolveOxfmtrcPath(cwd, configPath) {
418
- const resolvedCwd = resolve(cwd);
419
- if (configPath) return isAbsolute(configPath) ? configPath : join(resolvedCwd, configPath);
420
- let currentDir = resolvedCwd;
421
- while (true) {
422
- for (const filename of OXFMT_CONFIG_FILES) {
423
- const configFilePath = join(currentDir, filename);
424
- try {
425
- if ((await stat(configFilePath)).isFile()) return configFilePath;
426
- } catch (error) {
427
- const code = error.code;
428
- if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
429
- }
430
- }
431
- const parentDir = join(currentDir, "..");
432
- if (parentDir === currentDir) break;
433
- currentDir = parentDir;
434
- }
430
+ function readConfigDefaultExport(mod) {
431
+ if (!hasOwn(mod, "default")) throw new Error("Configuration file has no default export.");
432
+ const config = mod.default;
433
+ if (!isConfigObject(config)) throw new Error("Configuration file must have a default export that is an object.");
434
+ return config;
435
435
  }
436
436
  /**
437
- * Read and parse an oxfmt config file.
437
+ * Read a CommonJS module export as an oxfmt config object.
438
438
  *
439
- * @param resolvedPath - Absolute path to config file.
440
- * @param options - Config loading options.
441
- * @returns Parsed config object.
439
+ * @param config - Runtime CommonJS export value.
440
+ * @returns Oxfmt config object from the module export.
441
+ * @throws When the module export is not an object.
442
442
  */
443
- async function readConfigFromFile(resolvedPath, options = {}) {
444
- const extension = extname(resolvedPath);
445
- const useCache = options.useCache !== false;
446
- if (extension === ".ts" || extension === ".mts" || extension === ".cts" || extension === ".js" || extension === ".mjs" || extension === ".cjs") {
447
- if (!useCache && extension === ".cjs") return requireFreshCommonJSConfig(resolvedPath);
448
- if (!useCache && extension === ".mjs") return readConfigDefaultExport(await importNativeFreshModule(resolvedPath));
449
- if (!useCache && extension === ".js") return readConfigDefaultExport(await importFreshJavaScriptConfigModule(resolvedPath));
450
- return readConfigDefaultExport(await importJitiConfigModule(resolvedPath, useCache));
451
- }
452
- const content = await readFile(resolvedPath, "utf8");
453
- if (extension === ".jsonc") {
454
- const parseErrors = [];
455
- const parsed = parse(content, parseErrors, {
456
- allowTrailingComma: true,
457
- allowEmptyContent: true
458
- });
459
- if (parseErrors.length > 0) {
460
- const firstError = parseErrors[0];
461
- const errorCode = isUndefined(firstError) ? "Unknown" : printParseErrorCode(firstError.error);
462
- throw new Error(`Invalid JSONC syntax: ${errorCode}`);
463
- }
464
- return readParsedConfigObject(isUndefined(parsed) && content.trim() === "" ? {} : parsed);
465
- }
466
- if (extension === ".json") return readParsedConfigObject(JSON.parse(content));
467
- if (!extension) return readParsedConfigObject(JSON.parse(content));
468
- if (!OXFMT_EXPLICIT_CONFIG_EXTENSIONS.includes(extension)) throw new Error(`Unsupported oxfmt config extension "${extension}" at ${resolvedPath}`);
469
- return readParsedConfigObject(JSON.parse(content));
443
+ function readConfigModuleExports(config) {
444
+ if (!isConfigObject(config)) throw new Error("Configuration file must export an object.");
445
+ return config;
446
+ }
447
+ /**
448
+ * Read a parsed JSON-like config value as an oxfmt config object.
449
+ *
450
+ * @param config - Runtime config value.
451
+ * @returns Oxfmt config object.
452
+ * @throws When the config value is not an object.
453
+ */
454
+ function readParsedConfigObject(config) {
455
+ if (!isConfigObject(config)) throw new Error("Configuration file must be an object.");
456
+ return config;
457
+ }
458
+ /**
459
+ * Load a CommonJS config file without reusing Node's require cache.
460
+ *
461
+ * @param resolvedPath - Absolute `.cjs` config file path.
462
+ * @returns Oxfmt config object from default or module exports.
463
+ */
464
+ function requireFreshCommonJSConfig(resolvedPath) {
465
+ const configModule = requireFreshModule(resolvedPath);
466
+ if (Object.prototype.toString.call(configModule) === "[object Module]") return readConfigDefaultExport(configModule);
467
+ return readConfigModuleExports(configModule);
468
+ }
469
+ /**
470
+ * Require a CommonJS module after removing it from Node's require cache.
471
+ *
472
+ * @param resolvedPath - Absolute config file path to require.
473
+ * @returns Runtime module export value.
474
+ */
475
+ function requireFreshModule(resolvedPath) {
476
+ const requirePath = require.resolve(resolvedPath);
477
+ deleteRequireCacheTree(requirePath);
478
+ return require(requirePath);
470
479
  }
471
480
  //#endregion
472
481
  //#region src/editorconfig.ts
473
482
  /**
483
+ * Shared input caches for static and per-file EditorConfig resolution.
484
+ */
485
+ const sectionCache = /* @__PURE__ */ new Map();
486
+ const matcherCache = /* @__PURE__ */ new Map();
487
+ /**
474
488
  * Builds the cache key used for resolved EditorConfig lookups.
475
489
  *
476
490
  * @param resolveKey - The base resolution key for the current lookup.
@@ -492,19 +506,6 @@ function getEditorconfigSearchDir(cwd, configPath) {
492
506
  return dirname(resolvedConfigPath);
493
507
  }
494
508
  /**
495
- * Merges root-level EditorConfig options with oxfmt config options.
496
- *
497
- * @param oxfmtConfig - The explicit oxfmt options.
498
- * @param editorconfigRootOptions - Root options derived from .editorconfig.
499
- * @returns A single root options object where oxfmt takes precedence.
500
- */
501
- function mergeRootOptions(oxfmtConfig, editorconfigRootOptions) {
502
- return {
503
- ...editorconfigRootOptions,
504
- ...oxfmtConfig
505
- };
506
- }
507
- /**
508
509
  * Merges EditorConfig-derived overrides with explicit oxfmt overrides.
509
510
  *
510
511
  * @param oxfmtConfig - Explicit root options and overrides from the oxfmt config.
@@ -522,6 +523,63 @@ function mergeOverrides(oxfmtConfig, editorconfigOverrides) {
522
523
  return mergedOverrides.length > 0 ? mergedOverrides : void 0;
523
524
  }
524
525
  /**
526
+ * Merges root-level EditorConfig options with oxfmt config options.
527
+ *
528
+ * @param oxfmtConfig - The explicit oxfmt options.
529
+ * @param editorconfigRootOptions - Root options derived from .editorconfig.
530
+ * @returns A single root options object where oxfmt takes precedence.
531
+ */
532
+ function mergeRootOptions(oxfmtConfig, editorconfigRootOptions) {
533
+ return {
534
+ ...editorconfigRootOptions,
535
+ ...oxfmtConfig
536
+ };
537
+ }
538
+ /**
539
+ * Reads a .editorconfig file and converts it into root options and override entries.
540
+ *
541
+ * @param editorconfigPath - The absolute path to the .editorconfig file.
542
+ * @param anchorDir - The directory used to rebase section patterns.
543
+ * @param options - Optional per-file resolution and cache controls.
544
+ * @param options.filepath - Resolve matching sections for this absolute file path.
545
+ * @param options.useCache - Reuse parsed sections and compiled matchers.
546
+ * @returns Parsed EditorConfig data ready to merge into oxfmt config.
547
+ */
548
+ async function readEditorconfigFromFile(editorconfigPath, anchorDir, options = {}) {
549
+ const useCache = options.useCache !== false;
550
+ const readSections = async () => parseBuffer(await readFile(editorconfigPath));
551
+ const parsedSections = useCache ? await cachePromise(sectionCache, editorconfigPath, readSections) : await readSections();
552
+ const editorconfigDir = dirname(editorconfigPath);
553
+ const rootOptions = {};
554
+ const overrides = [];
555
+ const relativeFile = options.filepath ? toPosixPath(relative(editorconfigDir, options.filepath)) : void 0;
556
+ if (relativeFile !== void 0 && isPathOutsideBase(relativeFile)) return {
557
+ overrides,
558
+ rootOptions
559
+ };
560
+ for (const [sectionName, sectionBody] of parsedSections) {
561
+ if (!sectionName) continue;
562
+ const mappedOptions = mapEditorconfigSectionToOptions(sectionBody);
563
+ if (Object.keys(mappedOptions).length === 0) continue;
564
+ if (relativeFile !== void 0) {
565
+ if (matchesEditorconfigSection(sectionName, relativeFile, useCache)) Object.assign(rootOptions, mappedOptions);
566
+ continue;
567
+ }
568
+ if (isEditorconfigGlobalSection(sectionName)) {
569
+ Object.assign(rootOptions, mappedOptions);
570
+ if (overrides.length === 0) continue;
571
+ }
572
+ overrides.push({
573
+ options: mappedOptions,
574
+ files: [rebaseEditorconfigPattern(sectionName, editorconfigDir, anchorDir)]
575
+ });
576
+ }
577
+ return {
578
+ overrides,
579
+ rootOptions
580
+ };
581
+ }
582
+ /**
525
583
  * Resolves the nearest .editorconfig file starting from a directory and optionally walking upward.
526
584
  *
527
585
  * @param startDir - The directory where the search starts.
@@ -554,6 +612,61 @@ function isEditorconfigGlobalSection(sectionName) {
554
612
  return Boolean(sectionName && EDITORCONFIG_GLOBAL_SECTION_NAMES.includes(sectionName));
555
613
  }
556
614
  /**
615
+ * Maps a parsed EditorConfig section to the subset of oxfmt formatter options it supports.
616
+ *
617
+ * @param section - The parsed EditorConfig section body.
618
+ * @returns Formatter options derived from the section.
619
+ */
620
+ function mapEditorconfigSectionToOptions(section) {
621
+ const options = {};
622
+ const endOfLine = parseEditorconfigEndOfLine(section.end_of_line);
623
+ if (endOfLine) options.endOfLine = endOfLine;
624
+ if (section.indent_style === "tab") options.useTabs = true;
625
+ else if (section.indent_style === "space") options.useTabs = false;
626
+ const singleQuote = parseEditorconfigQuoteType(section.quote_type);
627
+ if (isBoolean(singleQuote)) options.singleQuote = singleQuote;
628
+ const tabWidth = parseEditorconfigTabWidth(section, options.useTabs);
629
+ if (isNumber(tabWidth)) options.tabWidth = tabWidth;
630
+ if (section.max_line_length && section.max_line_length !== "unset") {
631
+ const parsedPrintWidth = Number(section.max_line_length);
632
+ if (Number.isFinite(parsedPrintWidth)) options.printWidth = parsedPrintWidth;
633
+ }
634
+ const insertFinalNewline = parseEditorconfigBoolean(section.insert_final_newline);
635
+ if (isBoolean(insertFinalNewline)) options.insertFinalNewline = insertFinalNewline;
636
+ return options;
637
+ }
638
+ /**
639
+ * Match a section against its source-relative file path without oxfmt rebasing.
640
+ *
641
+ * @param pattern - Original section name.
642
+ * @param filepath - File path relative to the EditorConfig directory.
643
+ * @param useCache - Whether compiled matchers can be reused.
644
+ * @returns Whether the section applies to the file.
645
+ */
646
+ function matchesEditorconfigSection(pattern, filepath, useCache) {
647
+ let matcher = useCache ? getCacheValue(matcherCache, pattern) : void 0;
648
+ if (!matcher) {
649
+ matcher = new Minimatch(normalizeEditorconfigPattern(pattern), {
650
+ dot: true,
651
+ nocomment: true,
652
+ noext: true,
653
+ nonegate: true
654
+ });
655
+ if (useCache) setCacheValue(matcherCache, pattern, matcher);
656
+ }
657
+ return matcher.match(filepath);
658
+ }
659
+ /**
660
+ * Keep basename patterns recursive without changing path-scoped patterns.
661
+ *
662
+ * @param pattern - Original EditorConfig section name.
663
+ * @returns Pattern suitable for matching a path relative to EditorConfig.
664
+ */
665
+ function normalizeEditorconfigPattern(pattern) {
666
+ if (pattern === "*" || pattern === "**") return "**";
667
+ return pattern.includes("/") ? pattern : `**/${pattern}`;
668
+ }
669
+ /**
557
670
  * Parses an EditorConfig boolean string.
558
671
  *
559
672
  * @param value - The raw EditorConfig value.
@@ -570,7 +683,11 @@ function parseEditorconfigBoolean(value) {
570
683
  * @returns The normalized line ending value, or undefined when unsupported.
571
684
  */
572
685
  function parseEditorconfigEndOfLine(value) {
573
- if (value === "lf" || value === "crlf" || value === "cr") return value;
686
+ switch (value) {
687
+ case "cr":
688
+ case "crlf":
689
+ case "lf": return value;
690
+ }
574
691
  }
575
692
  /**
576
693
  * Parses the single quote preference from an EditorConfig section.
@@ -590,8 +707,8 @@ function parseEditorconfigQuoteType(value) {
590
707
  * @returns The resolved tab width, or undefined when no numeric value is available.
591
708
  */
592
709
  function parseEditorconfigTabWidth(section, useTabs) {
593
- const indentSize = section["indent_size"];
594
- const tabWidth = section["tab_width"];
710
+ const indentSize = section.indent_size;
711
+ const tabWidth = section.tab_width;
595
712
  if (useTabs === false && indentSize && indentSize !== "unset" && indentSize !== "tab") {
596
713
  const parsedIndentSize = Number(indentSize);
597
714
  if (Number.isFinite(parsedIndentSize)) return parsedIndentSize;
@@ -602,30 +719,6 @@ function parseEditorconfigTabWidth(section, useTabs) {
602
719
  }
603
720
  }
604
721
  /**
605
- * Maps a parsed EditorConfig section to the subset of oxfmt formatter options it supports.
606
- *
607
- * @param section - The parsed EditorConfig section body.
608
- * @returns Formatter options derived from the section.
609
- */
610
- function mapEditorconfigSectionToOptions(section) {
611
- const options = {};
612
- const endOfLine = parseEditorconfigEndOfLine(section["end_of_line"]);
613
- if (endOfLine) options.endOfLine = endOfLine;
614
- if (section["indent_style"] === "tab") options.useTabs = true;
615
- else if (section["indent_style"] === "space") options.useTabs = false;
616
- const singleQuote = parseEditorconfigQuoteType(section["quote_type"]);
617
- if (isBoolean(singleQuote)) options.singleQuote = singleQuote;
618
- const tabWidth = parseEditorconfigTabWidth(section, options.useTabs);
619
- if (isNumber(tabWidth)) options.tabWidth = tabWidth;
620
- if (section["max_line_length"] && section["max_line_length"] !== "unset") {
621
- const parsedPrintWidth = Number(section["max_line_length"]);
622
- if (Number.isFinite(parsedPrintWidth)) options.printWidth = parsedPrintWidth;
623
- }
624
- const insertFinalNewline = parseEditorconfigBoolean(section["insert_final_newline"]);
625
- if (isBoolean(insertFinalNewline)) options.insertFinalNewline = insertFinalNewline;
626
- return options;
627
- }
628
- /**
629
722
  * Rebases an EditorConfig section pattern from the config directory to the target anchor directory.
630
723
  *
631
724
  * @param pattern - The original EditorConfig section pattern.
@@ -634,40 +727,10 @@ function mapEditorconfigSectionToOptions(section) {
634
727
  * @returns The rebased glob pattern.
635
728
  */
636
729
  function rebaseEditorconfigPattern(pattern, editorconfigDir, anchorDir) {
730
+ const normalizedPattern = normalizeEditorconfigPattern(pattern);
637
731
  const relativePrefix = toPosixPath(relative(anchorDir, editorconfigDir));
638
- if (!relativePrefix || relativePrefix === ".") return pattern;
639
- return `${relativePrefix}/${pattern.replace(/^\//u, "")}`;
640
- }
641
- /**
642
- * Reads a .editorconfig file and converts it into root options and override entries.
643
- *
644
- * @param editorconfigPath - The absolute path to the .editorconfig file.
645
- * @param anchorDir - The directory used to rebase section patterns.
646
- * @returns Parsed EditorConfig data ready to merge into oxfmt config.
647
- */
648
- async function readEditorconfigFromFile(editorconfigPath, anchorDir) {
649
- const content = await readFile(editorconfigPath);
650
- const parsedSections = parseBuffer(content);
651
- const editorconfigDir = dirname(editorconfigPath);
652
- const rootOptions = {};
653
- const overrides = [];
654
- for (const [sectionName, sectionBody] of parsedSections) {
655
- if (!sectionName) continue;
656
- const mappedOptions = mapEditorconfigSectionToOptions(sectionBody);
657
- if (Object.keys(mappedOptions).length === 0) continue;
658
- if (isEditorconfigGlobalSection(sectionName)) {
659
- Object.assign(rootOptions, mappedOptions);
660
- continue;
661
- }
662
- overrides.push({
663
- files: [rebaseEditorconfigPattern(sectionName, editorconfigDir, anchorDir)],
664
- options: mappedOptions
665
- });
666
- }
667
- return {
668
- overrides,
669
- rootOptions
670
- };
732
+ if (!relativePrefix || relativePrefix === ".") return normalizedPattern;
733
+ return `${relativePrefix}/${normalizedPattern.replace(/^\//u, "")}`;
671
734
  }
672
735
  //#endregion
673
736
  //#region src/core.ts
@@ -680,6 +743,10 @@ const resolveCache = /* @__PURE__ */ new Map();
680
743
  */
681
744
  const configCache = /* @__PURE__ */ new Map();
682
745
  /**
746
+ * Share raw config reads across files without caching file-specific fallbacks.
747
+ */
748
+ const sourceCache = /* @__PURE__ */ new Map();
749
+ /**
683
750
  * Resolve config + editorconfig and return merged config with metadata.
684
751
  *
685
752
  * @param options - Loader settings.
@@ -694,6 +761,26 @@ const configCache = /* @__PURE__ */ new Map();
694
761
  * ```
695
762
  */
696
763
  async function loadOxfmtConfig(options = {}) {
764
+ return loadConfig(options);
765
+ }
766
+ /**
767
+ * Load config with EditorConfig fallbacks resolved for one file.
768
+ * Explicit oxfmt overrides remain unevaluated and retain their config-relative globs.
769
+ *
770
+ * @param options - Loader settings with the target file path.
771
+ * @returns Config with per-file EditorConfig defaults and config metadata.
772
+ */
773
+ async function loadOxfmtConfigForFile(options) {
774
+ return loadConfig(options, true);
775
+ }
776
+ /**
777
+ * Share discovery and caches between static and per-file loading.
778
+ *
779
+ * @param options - Loader settings.
780
+ * @param resolveEditorconfig - Resolve EditorConfig sections against the target file.
781
+ * @returns Loaded config and metadata.
782
+ */
783
+ async function loadConfig(options, resolveEditorconfig = false) {
697
784
  const useCache = options.useCache !== false;
698
785
  const cwd = resolve(options.cwd || process.cwd());
699
786
  const filepath = options.filepath ? resolve(cwd, options.filepath) : void 0;
@@ -709,12 +796,17 @@ async function loadOxfmtConfig(options = {}) {
709
796
  const resolvedPath = useCache ? await cachePromise(resolveCache, resolveKey, () => resolveOxfmtrcPath(configLookupCwd, options.configPath)) : await resolveOxfmtrcPath(configLookupCwd, options.configPath);
710
797
  const editorconfigPath = useEditorconfig ? await (useCache ? cachePromise(resolveCache, editorconfigResolveKey, () => resolveEditorconfigPath(editorconfigSearchDir, onlyCwd)) : resolveEditorconfigPath(editorconfigSearchDir, onlyCwd)) : void 0;
711
798
  const anchorDir = dirname(resolvedPath || editorconfigPath || cwd);
799
+ const configKey = getConfigCacheKey(resolvedPath, editorconfigPath, JSON.stringify([resolveKey, resolveEditorconfig ? filepath : null]));
712
800
  const loadTask = async () => {
713
- const oxfmtConfig = resolvedPath ? await readConfigFromFile(resolvedPath, { useCache }).catch((error) => {
801
+ const readConfig = async () => resolvedPath ? await readConfigFromFile(resolvedPath, { useCache }).catch((error) => {
714
802
  throw new Error(`Failed to parse oxfmt configuration file at ${resolvedPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
715
803
  }) : {};
804
+ const oxfmtConfig = useCache && resolvedPath ? await cachePromise(sourceCache, resolvedPath, readConfig) : await readConfig();
716
805
  if (!editorconfigPath) return oxfmtConfig;
717
- const editorconfigData = await readEditorconfigFromFile(editorconfigPath, anchorDir);
806
+ const editorconfigData = await readEditorconfigFromFile(editorconfigPath, anchorDir, {
807
+ filepath: resolveEditorconfig ? filepath : void 0,
808
+ useCache
809
+ });
718
810
  const mergedConfig = mergeRootOptions(oxfmtConfig, editorconfigData.rootOptions);
719
811
  const mergedOverrides = mergeOverrides(oxfmtConfig, editorconfigData.overrides);
720
812
  if (!mergedOverrides) return mergedConfig;
@@ -723,15 +815,11 @@ async function loadOxfmtConfig(options = {}) {
723
815
  overrides: mergedOverrides
724
816
  };
725
817
  };
726
- const hasNoConfigSources = !resolvedPath && !editorconfigPath;
727
818
  return {
728
- config: await (async () => {
729
- if (hasNoConfigSources) return useCache ? await cachePromise(configCache, getConfigCacheKey(resolvedPath, editorconfigPath, resolveKey), () => Promise.resolve({})) : {};
730
- return useCache ? await cachePromise(configCache, getConfigCacheKey(resolvedPath, editorconfigPath, resolveKey), loadTask) : await loadTask();
731
- })(),
819
+ config: useCache ? await cachePromise(configCache, configKey, loadTask) : await loadTask(),
732
820
  ...resolvedPath ? {
733
- filepath: resolvedPath,
734
- dirname: dirname(resolvedPath)
821
+ dirname: dirname(resolvedPath),
822
+ filepath: resolvedPath
735
823
  } : {}
736
824
  };
737
825
  }
@@ -746,10 +834,108 @@ const ignoreMatcherCache = /* @__PURE__ */ new Map();
746
834
  */
747
835
  const configIgnoreMatcherCache = /* @__PURE__ */ new Map();
748
836
  /**
837
+ * Resolve whether a file should be ignored using oxfmt-like CLI semantics.
838
+ *
839
+ * @param options - Ignore resolution options.
840
+ * @returns Ignore status with optional reason.
841
+ *
842
+ * @example
843
+ * ```ts
844
+ * import { isOxfmtIgnored } from 'load-oxfmt-config'
845
+ *
846
+ * const result = await isOxfmtIgnored({
847
+ * cwd: process.cwd(),
848
+ * filepath: 'src/generated/file.ts',
849
+ * })
850
+ * ```
851
+ */
852
+ async function isOxfmtIgnored(options) {
853
+ const useCache = options.useCache !== false;
854
+ const includeConfigIgnorePatterns = options.includeConfigIgnorePatterns !== false;
855
+ const loadConfigForIgnorePatterns = options.loadConfigForIgnorePatterns !== false;
856
+ const cwd = resolve(options.cwd ?? process.cwd());
857
+ const filepath = isAbsolute(options.filepath) ? resolve(options.filepath) : resolve(cwd, options.filepath);
858
+ if (options.includeDefaultIgnores !== false) {
859
+ const defaultResult = await resolveDefaultIgnores(filepath, cwd, options, useCache);
860
+ if (defaultResult.ignored) return defaultResult;
861
+ }
862
+ if (!loadConfigForIgnorePatterns) return { ignored: false };
863
+ const configResult = await loadOxfmtConfig({
864
+ cwd,
865
+ filepath,
866
+ ...options.disableNestedConfig === true ? { disableNestedConfig: true } : {},
867
+ ...options.configPath ? { configPath: options.configPath } : {},
868
+ editorconfig: false,
869
+ useCache
870
+ });
871
+ if (includeConfigIgnorePatterns && configResult.dirname && configResult.config.ignorePatterns && configResult.config.ignorePatterns.length > 0 && matchConfigIgnorePatterns(filepath, configResult.dirname, configResult.config.ignorePatterns, useCache)) return {
872
+ ignored: true,
873
+ reason: "config-ignore-patterns"
874
+ };
875
+ return { ignored: false };
876
+ }
877
+ /**
878
+ * Collect `.gitignore` files from file directory up to git repo boundary.
879
+ *
880
+ * @param filepath - Absolute file path to test.
881
+ * @returns Collected ignore files and repo root when found.
882
+ */
883
+ async function collectGitignorePaths(filepath) {
884
+ const fileDir = dirname(filepath);
885
+ const repoRoot = await findGitRepoRoot(fileDir);
886
+ const paths = [];
887
+ let current = fileDir;
888
+ while (true) {
889
+ paths.push(join(current, ".gitignore"));
890
+ if (repoRoot && current === repoRoot) break;
891
+ const parent = dirname(current);
892
+ if (parent === current) break;
893
+ current = parent;
894
+ }
895
+ return {
896
+ paths,
897
+ repoRoot
898
+ };
899
+ }
900
+ /**
901
+ * Find the nearest git repo root by walking up from a start directory.
902
+ *
903
+ * @param fromDir - Directory to start from.
904
+ * @returns Repo root directory, or undefined when no git boundary is found.
905
+ */
906
+ async function findGitRepoRoot(fromDir) {
907
+ let current = fromDir;
908
+ while (true) {
909
+ if (await hasGitEntry(current)) return current;
910
+ const parent = dirname(current);
911
+ if (parent === current) return;
912
+ current = parent;
913
+ }
914
+ }
915
+ /**
916
+ * Check whether a directory looks like a git repo root.
917
+ *
918
+ * A `.git` entry can be either a directory (regular repo) or a file (worktree/submodule).
919
+ *
920
+ * @param dir - Directory to inspect.
921
+ * @returns True when `.git` exists under the directory.
922
+ */
923
+ async function hasGitEntry(dir) {
924
+ try {
925
+ await stat(join(dir, ".git"));
926
+ return true;
927
+ } catch (error) {
928
+ const code = error.code;
929
+ if (code === "ENOENT" || code === "ENOTDIR") return false;
930
+ throw error;
931
+ }
932
+ }
933
+ /**
749
934
  * Check whether a file is under oxfmt's default ignored directories.
750
935
  *
751
936
  * @param filepath - Absolute file path to test.
752
937
  * @param options - Matching options.
938
+ * @param options.withNodeModules - Whether node_modules should remain eligible.
753
939
  * @returns True when the path should be ignored by default directory rules.
754
940
  */
755
941
  function isDefaultIgnoredDir(filepath, options) {
@@ -787,14 +973,25 @@ function loadIgnoreMatcher(ignoreFilePath, useCache) {
787
973
  return cachePromise(ignoreMatcherCache, ignoreFilePath, loadTask);
788
974
  }
789
975
  /**
790
- * Build a POSIX relative path.
976
+ * Match `ignorePatterns` from config with support for negated patterns.
791
977
  *
792
- * @param from - Base directory.
793
- * @param to - Target path.
794
- * @returns POSIX relative path.
978
+ * @param filepath - Absolute file path.
979
+ * @param configDir - Resolved config directory.
980
+ * @param patterns - Config ignore patterns.
981
+ * @param useCache - Whether to reuse compiled pattern matchers.
982
+ * @returns True when patterns mark the file as ignored.
795
983
  */
796
- function relativeSafe(from, to) {
797
- return toPosixPath(relative(from, to));
984
+ function matchConfigIgnorePatterns(filepath, configDir, patterns, useCache) {
985
+ validateConfigIgnorePatterns(patterns);
986
+ const relativeFile = relativeSafe(configDir, filepath);
987
+ if (isPathOutsideBase(relativeFile)) return false;
988
+ if (!useCache) return ignore().add(patterns).ignores(relativeFile);
989
+ const cacheKey = `${configDir}::${JSON.stringify(patterns)}`;
990
+ const cachedMatcher = getCacheValue(configIgnoreMatcherCache, cacheKey);
991
+ if (cachedMatcher) return cachedMatcher.ignores(relativeFile);
992
+ const matcher = ignore().add(patterns);
993
+ setCacheValue(configIgnoreMatcherCache, cacheKey, matcher);
994
+ return matcher.ignores(relativeFile);
798
995
  }
799
996
  /**
800
997
  * Match file path against one ignore file.
@@ -813,6 +1010,28 @@ async function matchIgnoreFile(filepath, ignoreFilePath, useCache, baseDir) {
813
1010
  return matcher.ignores(relativeToIgnore);
814
1011
  }
815
1012
  /**
1013
+ * Match a file path against ordered ignore files while preserving negation state.
1014
+ *
1015
+ * @param filepath - Absolute file path.
1016
+ * @param ignoreFileEntries - Ignore files in increasing precedence order.
1017
+ * @param useCache - Whether to use matcher cache.
1018
+ * @param stopAtIgnoredDirectory - Whether ignored directories prevent reading nested ignore files.
1019
+ * @returns True when the ordered ignore files mark the file as ignored.
1020
+ */
1021
+ async function matchIgnoreFileChain(filepath, ignoreFileEntries, useCache, stopAtIgnoredDirectory = false) {
1022
+ const matcherEntries = [];
1023
+ for (const entry of ignoreFileEntries) {
1024
+ const baseDir = entry.baseDir ?? dirname(entry.path);
1025
+ if (stopAtIgnoredDirectory && matchLoadedIgnoreMatchers(baseDir, matcherEntries, true).ignored) break;
1026
+ const matcher = await loadIgnoreMatcher(entry.path, useCache);
1027
+ if (matcher) matcherEntries.push({
1028
+ baseDir,
1029
+ matcher
1030
+ });
1031
+ }
1032
+ return matchLoadedIgnoreMatchers(filepath, matcherEntries);
1033
+ }
1034
+ /**
816
1035
  * Match a path against loaded ignore matchers in precedence order.
817
1036
  *
818
1037
  * @param filepath - Absolute file or directory path to test.
@@ -842,54 +1061,57 @@ function matchLoadedIgnoreMatchers(filepath, matcherEntries, isDirectory = false
842
1061
  };
843
1062
  }
844
1063
  /**
845
- * Match a file path against ordered ignore files while preserving negation state.
846
- *
847
- * @param filepath - Absolute file path.
848
- * @param ignoreFileEntries - Ignore files in increasing precedence order.
849
- * @param useCache - Whether to use matcher cache.
850
- * @param stopAtIgnoredDirectory - Whether ignored directories prevent reading nested ignore files.
851
- * @returns True when the ordered ignore files mark the file as ignored.
852
- */
853
- async function matchIgnoreFileChain(filepath, ignoreFileEntries, useCache, stopAtIgnoredDirectory = false) {
854
- const matcherEntries = [];
855
- for (const entry of ignoreFileEntries) {
856
- const baseDir = entry.baseDir ?? dirname(entry.path);
857
- if (stopAtIgnoredDirectory && matchLoadedIgnoreMatchers(baseDir, matcherEntries, true).ignored) break;
858
- const matcher = await loadIgnoreMatcher(entry.path, useCache);
859
- if (matcher) matcherEntries.push({
860
- baseDir,
861
- matcher
862
- });
863
- }
864
- return matchLoadedIgnoreMatchers(filepath, matcherEntries);
865
- }
866
- /**
867
- * Resolve an ignore file path against cwd when needed.
1064
+ * Build a POSIX relative path.
868
1065
  *
869
- * @param path - Absolute or relative path.
870
- * @param cwd - Current working directory.
871
- * @returns Absolute ignore file path.
1066
+ * @param from - Base directory.
1067
+ * @param to - Target path.
1068
+ * @returns POSIX relative path.
872
1069
  */
873
- function resolveIgnoreFilePath(path, cwd) {
874
- return isAbsolute(path) ? path : resolve(cwd, path);
1070
+ function relativeSafe(from, to) {
1071
+ return toPosixPath(relative(from, to));
875
1072
  }
876
1073
  /**
877
- * Check whether a directory looks like a git repo root.
1074
+ * Evaluate global ignore rules separately from explicit config patterns.
878
1075
  *
879
- * A `.git` entry can be either a directory (regular repo) or a file (worktree/submodule).
880
- *
881
- * @param dir - Directory to inspect.
882
- * @returns True when `.git` exists under the directory.
1076
+ * @param filepath - Absolute target file path.
1077
+ * @param cwd - Base directory for ignore file resolution.
1078
+ * @param options - Ignore file and node_modules settings.
1079
+ * @param useCache - Whether ignore matchers can be reused.
1080
+ * @returns Global ignore status.
883
1081
  */
884
- async function hasGitEntry(dir) {
885
- try {
886
- await stat(join(dir, ".git"));
887
- return true;
888
- } catch (error) {
889
- const code = error.code;
890
- if (code === "ENOENT" || code === "ENOTDIR") return false;
891
- throw error;
1082
+ async function resolveDefaultIgnores(filepath, cwd, options, useCache) {
1083
+ if (isDefaultIgnoredDir(filepath, options.withNodeModules ? { withNodeModules: true } : {})) return {
1084
+ ignored: true,
1085
+ reason: "default-dir"
1086
+ };
1087
+ if (isLockfile(filepath)) return {
1088
+ ignored: true,
1089
+ reason: "lockfile"
1090
+ };
1091
+ const explicitIgnorePaths = (typeof options.ignorePath === "string" ? [options.ignorePath] : options.ignorePath)?.map((path) => resolveIgnoreFilePath(path, cwd));
1092
+ const { paths: gitignorePaths, repoRoot } = await collectGitignorePaths(filepath);
1093
+ const gitignoreResult = await matchIgnoreFileChain(filepath, [...gitignorePaths].reverse().map((path) => ({ path })), useCache, true);
1094
+ if (gitignoreResult.ignored) return {
1095
+ ignored: true,
1096
+ reason: "gitignore"
1097
+ };
1098
+ if (repoRoot && !gitignoreResult.matched) {
1099
+ const infoExcludePath = await resolveGitInfoExcludePath(repoRoot);
1100
+ if (infoExcludePath && await matchIgnoreFile(filepath, infoExcludePath, useCache, repoRoot)) return {
1101
+ ignored: true,
1102
+ reason: "git-info-exclude"
1103
+ };
892
1104
  }
1105
+ if (explicitIgnorePaths && explicitIgnorePaths.length > 0) {
1106
+ if ((await matchIgnoreFileChain(filepath, explicitIgnorePaths.map((path) => ({ path })), useCache)).ignored) return {
1107
+ ignored: true,
1108
+ reason: "ignore-path"
1109
+ };
1110
+ } else if (await matchIgnoreFile(filepath, resolve(cwd, ".prettierignore"), useCache)) return {
1111
+ ignored: true,
1112
+ reason: "prettierignore"
1113
+ };
1114
+ return { ignored: false };
893
1115
  }
894
1116
  /**
895
1117
  * Resolve the git info exclude file from a repo root.
@@ -927,42 +1149,14 @@ async function resolveGitInfoExcludePath(repoRoot) {
927
1149
  }
928
1150
  }
929
1151
  /**
930
- * Find the nearest git repo root by walking up from a start directory.
931
- *
932
- * @param fromDir - Directory to start from.
933
- * @returns Repo root directory, or undefined when no git boundary is found.
934
- */
935
- async function findGitRepoRoot(fromDir) {
936
- let current = fromDir;
937
- while (true) {
938
- if (await hasGitEntry(current)) return current;
939
- const parent = dirname(current);
940
- if (parent === current) return;
941
- current = parent;
942
- }
943
- }
944
- /**
945
- * Collect `.gitignore` files from file directory up to git repo boundary.
1152
+ * Resolve an ignore file path against cwd when needed.
946
1153
  *
947
- * @param filepath - Absolute file path to test.
948
- * @returns Collected ignore files and repo root when found.
1154
+ * @param path - Absolute or relative path.
1155
+ * @param cwd - Current working directory.
1156
+ * @returns Absolute ignore file path.
949
1157
  */
950
- async function collectGitignorePaths(filepath) {
951
- const fileDir = dirname(filepath);
952
- const repoRoot = await findGitRepoRoot(fileDir);
953
- const paths = [];
954
- let current = fileDir;
955
- while (true) {
956
- paths.push(join(current, ".gitignore"));
957
- if (repoRoot && current === repoRoot) break;
958
- const parent = dirname(current);
959
- if (parent === current) break;
960
- current = parent;
961
- }
962
- return {
963
- paths,
964
- repoRoot
965
- };
1158
+ function resolveIgnoreFilePath(path, cwd) {
1159
+ return isAbsolute(path) ? path : resolve(cwd, path);
966
1160
  }
967
1161
  /**
968
1162
  * Validate config ignore patterns using oxfmt's path constraints.
@@ -976,94 +1170,5 @@ function validateConfigIgnorePatterns(patterns) {
976
1170
  if ((pathPart.endsWith(String.raw`\ `) ? pathPart : pathPart.trimEnd()).split("/").includes("..")) throw new Error(`Invalid pattern \`${pattern}\` in \`ignorePatterns\`: \`..\` is not supported, patterns are resolved within the config file's directory`);
977
1171
  }
978
1172
  }
979
- /**
980
- * Match `ignorePatterns` from config with support for negated patterns.
981
- *
982
- * @param filepath - Absolute file path.
983
- * @param configDir - Resolved config directory.
984
- * @param patterns - Config ignore patterns.
985
- * @param useCache - Whether to reuse compiled pattern matchers.
986
- * @returns True when patterns mark the file as ignored.
987
- */
988
- function matchConfigIgnorePatterns(filepath, configDir, patterns, useCache) {
989
- validateConfigIgnorePatterns(patterns);
990
- const relativeFile = relativeSafe(configDir, filepath);
991
- if (isPathOutsideBase(relativeFile)) return false;
992
- if (!useCache) return ignore().add(patterns).ignores(relativeFile);
993
- const cacheKey = `${configDir}::${JSON.stringify(patterns)}`;
994
- const cachedMatcher = getCacheValue(configIgnoreMatcherCache, cacheKey);
995
- if (cachedMatcher) return cachedMatcher.ignores(relativeFile);
996
- const matcher = ignore().add(patterns);
997
- setCacheValue(configIgnoreMatcherCache, cacheKey, matcher);
998
- return matcher.ignores(relativeFile);
999
- }
1000
- /**
1001
- * Resolve whether a file should be ignored using oxfmt-like CLI semantics.
1002
- *
1003
- * @param options - Ignore resolution options.
1004
- * @returns Ignore status with optional reason.
1005
- *
1006
- * @example
1007
- * ```ts
1008
- * import { isOxfmtIgnored } from 'load-oxfmt-config'
1009
- *
1010
- * const result = await isOxfmtIgnored({
1011
- * cwd: process.cwd(),
1012
- * filepath: 'src/generated/file.ts',
1013
- * })
1014
- * ```
1015
- */
1016
- async function isOxfmtIgnored(options) {
1017
- const useCache = options.useCache !== false;
1018
- const includeConfigIgnorePatterns = options.includeConfigIgnorePatterns !== false;
1019
- const loadConfigForIgnorePatterns = options.loadConfigForIgnorePatterns !== false;
1020
- const cwd = resolve(options.cwd ?? process.cwd());
1021
- const filepath = isAbsolute(options.filepath) ? resolve(options.filepath) : resolve(cwd, options.filepath);
1022
- if (isDefaultIgnoredDir(filepath, options.withNodeModules ? { withNodeModules: true } : {})) return {
1023
- ignored: true,
1024
- reason: "default-dir"
1025
- };
1026
- if (isLockfile(filepath)) return {
1027
- ignored: true,
1028
- reason: "lockfile"
1029
- };
1030
- const explicitIgnorePaths = (typeof options.ignorePath === "string" ? [options.ignorePath] : options.ignorePath)?.map((path) => resolveIgnoreFilePath(path, cwd));
1031
- const { paths: gitignorePaths, repoRoot } = await collectGitignorePaths(filepath);
1032
- const gitignoreResult = await matchIgnoreFileChain(filepath, [...gitignorePaths].reverse().map((path) => ({ path })), useCache, true);
1033
- if (gitignoreResult.ignored) return {
1034
- ignored: true,
1035
- reason: "gitignore"
1036
- };
1037
- if (repoRoot && !gitignoreResult.matched) {
1038
- const infoExcludePath = await resolveGitInfoExcludePath(repoRoot);
1039
- if (infoExcludePath && await matchIgnoreFile(filepath, infoExcludePath, useCache, repoRoot)) return {
1040
- ignored: true,
1041
- reason: "git-info-exclude"
1042
- };
1043
- }
1044
- if (explicitIgnorePaths && explicitIgnorePaths.length > 0) {
1045
- if ((await matchIgnoreFileChain(filepath, explicitIgnorePaths.map((path) => ({ path })), useCache)).ignored) return {
1046
- ignored: true,
1047
- reason: "ignore-path"
1048
- };
1049
- } else if (await matchIgnoreFile(filepath, resolve(cwd, ".prettierignore"), useCache)) return {
1050
- ignored: true,
1051
- reason: "prettierignore"
1052
- };
1053
- if (!loadConfigForIgnorePatterns) return { ignored: false };
1054
- const configResult = await loadOxfmtConfig({
1055
- cwd,
1056
- filepath,
1057
- ...options.disableNestedConfig === true ? { disableNestedConfig: true } : {},
1058
- ...options.configPath ? { configPath: options.configPath } : {},
1059
- editorconfig: false,
1060
- useCache
1061
- });
1062
- if (includeConfigIgnorePatterns && configResult.dirname && configResult.config.ignorePatterns && configResult.config.ignorePatterns.length > 0 && matchConfigIgnorePatterns(filepath, configResult.dirname, configResult.config.ignorePatterns, useCache)) return {
1063
- ignored: true,
1064
- reason: "config-ignore-patterns"
1065
- };
1066
- return { ignored: false };
1067
- }
1068
1173
  //#endregion
1069
- export { getConfigCacheKey, getResolveCacheKey, isOxfmtIgnored, loadOxfmtConfig, readConfigFromFile, resolveOxfmtrcPath };
1174
+ export { getConfigCacheKey, getResolveCacheKey, isOxfmtIgnored, loadOxfmtConfig, loadOxfmtConfigForFile, readConfigFromFile, resolveOxfmtrcPath };