load-oxfmt-config 0.12.0 → 0.13.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.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  ## Features
11
11
 
12
12
  - 🔍 **Auto-discovery** - Automatically searches for config files in current and parent directories
13
- - 📦 **Multiple formats** - Auto-discovers `.oxfmtrc.json`, `.oxfmtrc.jsonc`, and `oxfmt.config.ts`, and also supports explicit `.js` / `.mjs` / `.cjs` / `.mts` / `.cts` config paths
13
+ - 📦 **Multiple formats** - Auto-discovers `.oxfmtrc.json`, `.oxfmtrc.jsonc`, and `oxfmt.config.ts`, and also supports explicit `.json` / `.jsonc` / `.js` / `.mjs` / `.cjs` / `.ts` / `.mts` / `.cts` config paths
14
14
  - 🧩 **EditorConfig fallback** - Merges supported `.editorconfig` fields into the returned oxfmt config result
15
15
  - 🚫 **Ignore resolution** - Resolves ignore status with oxfmt CLI-like global + config-scoped semantics
16
16
  - ⚡ **Built-in caching** - Caches both file resolution and parsed configs for optimal performance
@@ -247,10 +247,10 @@ Control how `.editorconfig` files are read and merged:
247
247
  - **`false`** — Disable `.editorconfig` reading entirely.
248
248
  - **`EditorconfigOption`** — Enable with additional settings:
249
249
 
250
- | Property | Type | Default | Description |
251
- | --------- | --------- | ----------- | --------------------------------------------------------------------------------------------------------------------- |
252
- | `onlyCwd` | `boolean` | `false` | When `true`, only look for `.editorconfig` in `cwd` itself — no upward traversal. |
253
- | `cwd` | `string` | `undefined` | Override the directory from which `.editorconfig` resolution starts, instead of the config file's directory or `cwd`. |
250
+ | Property | Type | Default | Description |
251
+ | --------- | --------- | ----------- | ------------------------------------------------------------------------------------------------------------- |
252
+ | `onlyCwd` | `boolean` | `false` | When `true`, only look for `.editorconfig` in `cwd` itself — no upward traversal. |
253
+ | `cwd` | `string` | `undefined` | Override the directory from which `.editorconfig` resolution starts, instead of the default lookup directory. |
254
254
 
255
255
  **Returns:** `Promise<LoadOxfmtConfigResult>`
256
256
 
@@ -309,8 +309,9 @@ When provided, nested config lookup is disabled (same as oxfmt CLI `-c`).
309
309
  - **Type:** `string | string[]`
310
310
  - **Default:** `undefined`
311
311
 
312
- Ignore files to use instead of default `.gitignore` hierarchy + cwd `.prettierignore`.
312
+ Ignore files to use instead of the default cwd `.prettierignore`.
313
313
  Can be passed multiple times in CLI style.
314
+ Explicit ignore paths do not replace `.gitignore` or `.git/info/exclude` handling.
314
315
 
315
316
  #### `withNodeModules`
316
317
 
@@ -415,6 +416,9 @@ export default {
415
416
  }
416
417
  ```
417
418
 
419
+ > JavaScript and TypeScript config files are executed while loading config. Only load them from repositories you trust.
420
+
421
+ JavaScript and TypeScript config files must provide a default export whose value is an object.
418
422
  When `configPath` is passed explicitly, extensions `.json`, `.jsonc`, `.ts`, `.mts`, `.cts`, `.js`, `.mjs`, and `.cjs` are supported. Extensionless paths are also accepted and parsed as JSON.
419
423
 
420
424
  ## `.editorconfig` Support
@@ -443,21 +447,20 @@ Set `loadConfigForIgnorePatterns: false` to skip config loading entirely and kee
443
447
 
444
448
  Global ignore includes:
445
449
 
446
- - Default ignored directories: `.git`, `.svn`, `.jj`, `node_modules`
447
- - Default lockfiles: `package-lock.json`, `npm-shrinkwrap.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `bun.lockb`
450
+ - Default ignored directories: `.git`, `.jj`, `.sl`, `.svn`, `.hg`, `node_modules`
451
+ - Default lockfiles: `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `MODULE.bazel.lock`, `bun.lock`, `deno.lock`, `composer.lock`, `Package.resolved`, `Pipfile.lock`, `flake.lock`, `Cargo.lock`, `Gopkg.lock`, `pdm.lock`, `poetry.lock`, `uv.lock`, `npm-shrinkwrap.json`, `bun.lockb`
448
452
  - Ignore files:
449
- - If `ignorePath` is provided: use those files only (multiple supported)
450
- - If `ignorePath` is not provided:
451
- - Read `.gitignore` from the file's directory upward until the git repo boundary
452
- - Read `<repo>/.git/info/exclude` when inside a git repo
453
- - Read `.prettierignore` from `cwd`
453
+ - Always read `.gitignore` from the file's directory upward until the git repo boundary
454
+ - Always read `<repo>/.git/info/exclude` when inside a git repo
455
+ - If `ignorePath` is provided: read those files instead of `cwd/.prettierignore`
456
+ - If `ignorePath` is not provided: read `.prettierignore` from `cwd`
454
457
 
455
458
  Notes:
456
459
 
457
460
  - `node_modules` can be included by passing `withNodeModules: true`.
458
461
 
459
462
  - The default lockfile list mirrors oxfmt documentation intent (`package-lock.json`, `pnpm-lock.yaml`, etc.) and common ecosystem lockfiles. It is not guaranteed to be a complete internal oxfmt list.
460
- - `ignorePatterns` are always interpreted relative to the resolved oxfmt config directory.
463
+ - `ignorePatterns` use gitignore semantics and are interpreted relative to the resolved oxfmt config directory.
461
464
  - `includeConfigIgnorePatterns` defaults to `true` to preserve current behavior.
462
465
  - `loadConfigForIgnorePatterns` defaults to `true` to preserve current behavior.
463
466
  - Nested config behavior follows oxfmt semantics:
@@ -475,7 +478,7 @@ The merged result follows this order:
475
478
  3. Overrides generated from `.editorconfig` sections
476
479
  4. Overrides declared directly in the oxfmt config file
477
480
 
478
- This means explicit oxfmt config values always win over `.editorconfig` fallback values.
481
+ This means explicit root-level oxfmt config values win over root-level `.editorconfig` fallback values. Section-specific `.editorconfig` entries are represented as generated `overrides` in the static result, and explicit oxfmt `overrides` are appended after them.
479
482
 
480
483
  ## Limitations
481
484
 
@@ -512,6 +515,7 @@ The caching system maintains two separate caches:
512
515
 
513
516
  - Failed operations automatically clear their cache entries
514
517
  - Use `useCache: false` to bypass cache for specific calls
518
+ - For native ESM config files (`.mjs` or `.js` under `"type": "module"`), `useCache: false` cache-busts the entry config file. Imported ESM helper modules still follow Node.js module cache behavior for the current process.
515
519
  - Cache persists for the lifetime of the Node.js process
516
520
 
517
521
  ## Related
package/dist/index.d.mts CHANGED
@@ -2,7 +2,7 @@ import { FormatConfig } from "oxfmt";
2
2
 
3
3
  //#region src/types.d.ts
4
4
  /**
5
- * Object-form editorconfig option, enabling fine-grained control.
5
+ * Object-form `.editorconfig` option for fine-grained lookup control.
6
6
  */
7
7
  interface EditorconfigOption {
8
8
  /**
@@ -14,38 +14,41 @@ interface EditorconfigOption {
14
14
  onlyCwd?: boolean;
15
15
  /**
16
16
  * Override the directory from which `.editorconfig` resolution starts.
17
- * When set, editorconfig is searched from this directory instead of from
18
- * the config file's directory (or the top-level `cwd`).
19
17
  *
20
- * This is useful when the oxfmt config path is pre-resolved and you still
21
- * want editorconfig to be resolved relative to each file's directory.
18
+ * When set, `.editorconfig` is searched from this directory instead of from
19
+ * the config lookup directory. This is useful when the oxfmt config path is
20
+ * pre-resolved and EditorConfig should still be resolved from another
21
+ * directory, such as each file's directory.
22
22
  */
23
23
  cwd?: string;
24
24
  }
25
25
  /**
26
- * Format option override for a single matching rule
26
+ * Format option override for a single matching rule.
27
27
  */
28
28
  interface OxfmtConfigOverride {
29
29
  /**
30
- * Glob patterns to match files
30
+ * Glob patterns to match files.
31
31
  */
32
32
  files: string[];
33
33
  /**
34
- * Glob patterns to exclude files
34
+ * Glob patterns to exclude files.
35
35
  */
36
36
  excludeFiles?: string[];
37
37
  /**
38
- * Format options to apply
38
+ * Format options to apply.
39
39
  */
40
40
  options?: FormatConfig;
41
41
  }
42
+ /**
43
+ * Options for loading and merging oxfmt configuration.
44
+ */
42
45
  interface LoadOxfmtConfigOptions {
43
46
  /**
44
- * Path to the configuration file
47
+ * Path to the configuration file.
45
48
  */
46
49
  configPath?: string;
47
50
  /**
48
- * Current working directory
51
+ * Current working directory.
49
52
  */
50
53
  cwd?: string;
51
54
  /**
@@ -62,7 +65,9 @@ interface LoadOxfmtConfigOptions {
62
65
  */
63
66
  disableNestedConfig?: boolean;
64
67
  /**
65
- * Whether to use cache
68
+ * Whether to use in-memory caches for path resolution and parsed config contents.
69
+ *
70
+ * @default true
66
71
  */
67
72
  useCache?: boolean;
68
73
  /**
@@ -77,16 +82,17 @@ interface LoadOxfmtConfigOptions {
77
82
  editorconfig?: boolean | EditorconfigOption;
78
83
  }
79
84
  /**
80
- * Final oxfmt options (including overrides)
85
+ * Final oxfmt options, including config `ignorePatterns` and `overrides`.
81
86
  */
82
87
  interface OxfmtOptions extends FormatConfig {
83
88
  /**
84
- * Ignore files matching these glob patterns
85
- * Patterns are based on the location of the Oxfmt configuration file
89
+ * Ignore files matching these glob patterns.
90
+ *
91
+ * Patterns are based on the location of the oxfmt configuration file.
86
92
  */
87
93
  ignorePatterns?: string[];
88
94
  /**
89
- * Array of format option overrides
95
+ * Array of format option overrides.
90
96
  */
91
97
  overrides?: OxfmtConfigOverride[];
92
98
  }
@@ -95,15 +101,15 @@ interface OxfmtOptions extends FormatConfig {
95
101
  */
96
102
  interface LoadOxfmtConfigResult {
97
103
  /**
98
- * Final merged config (oxfmt + optional editorconfig mapping)
104
+ * Final merged config from oxfmt and optional `.editorconfig` mapping.
99
105
  */
100
106
  config: OxfmtOptions;
101
107
  /**
102
- * Absolute path of resolved config file
108
+ * Absolute path of resolved config file.
103
109
  */
104
110
  filepath?: string;
105
111
  /**
106
- * Directory of resolved config file
112
+ * Directory of resolved config file.
107
113
  */
108
114
  dirname?: string;
109
115
  }
@@ -113,6 +119,7 @@ interface LoadOxfmtConfigResult {
113
119
  interface IsOxfmtIgnoredOptions {
114
120
  /**
115
121
  * Current working directory.
122
+ *
116
123
  * Also the base directory for default `.prettierignore` lookup.
117
124
  */
118
125
  cwd?: string;
@@ -122,37 +129,46 @@ interface IsOxfmtIgnoredOptions {
122
129
  filepath: string;
123
130
  /**
124
131
  * Explicit oxfmt config path.
132
+ *
125
133
  * When provided, nested config lookup is disabled (same as oxfmt CLI -c).
126
134
  */
127
135
  configPath?: string;
128
136
  /**
129
- * Ignore files to use instead of default `.gitignore` hierarchy + cwd `.prettierignore`.
130
- * Can be passed multiple times in CLI style.
137
+ * Ignore files to use instead of the default cwd `.prettierignore`.
138
+ *
139
+ * Explicit ignore paths do not replace `.gitignore` or `.git/info/exclude`
140
+ * handling. They can be passed multiple times in CLI style.
131
141
  */
132
142
  ignorePath?: string | string[];
133
143
  /**
134
144
  * Whether node_modules should be included.
145
+ *
135
146
  * @default false
136
147
  */
137
148
  withNodeModules?: boolean;
138
149
  /**
139
150
  * Disable nested config lookup.
151
+ *
140
152
  * @default false
141
153
  */
142
154
  disableNestedConfig?: boolean;
143
155
  /**
144
156
  * Whether to use in-memory cache.
157
+ *
145
158
  * @default true
146
159
  */
147
160
  useCache?: boolean;
148
161
  /**
149
162
  * Whether to include ignore patterns defined in the config file.
163
+ *
150
164
  * @default true
151
165
  */
152
166
  includeConfigIgnorePatterns?: boolean;
153
167
  /**
154
168
  * Whether to load resolved oxfmt config when evaluating config ignore patterns.
169
+ *
155
170
  * When false, only global ignore is applied and config loading is skipped.
171
+ *
156
172
  * @default true
157
173
  */
158
174
  loadConfigForIgnorePatterns?: boolean;
@@ -166,7 +182,7 @@ interface IsOxfmtIgnoredResult {
166
182
  */
167
183
  ignored: boolean;
168
184
  /**
169
- * Matched ignore source.
185
+ * Matched ignore source when `ignored` is true.
170
186
  */
171
187
  reason?: 'default-dir' | 'lockfile' | 'gitignore' | 'git-info-exclude' | 'prettierignore' | 'ignore-path' | 'config-ignore-patterns';
172
188
  }
package/dist/index.mjs CHANGED
@@ -1,11 +1,12 @@
1
+ import { createRequire } from "node:module";
1
2
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
2
3
  import process from "node:process";
3
4
  import { interopDefault, isBoolean, isNumber, isObject } from "@ntnyq/utils";
4
5
  import { readFile, stat } from "node:fs/promises";
6
+ import { pathToFileURL } from "node:url";
5
7
  import { parse, printParseErrorCode } from "jsonc-parser";
6
8
  import { parseBuffer } from "editorconfig";
7
9
  import ignore from "ignore";
8
- import picomatch from "picomatch";
9
10
  //#region src/constants.ts
10
11
  /**
11
12
  * Supported configuration files for oxfmt.
@@ -34,8 +35,10 @@ const OXFMT_EXPLICIT_CONFIG_EXTENSIONS = [
34
35
  */
35
36
  const DEFAULT_IGNORED_DIRS = [
36
37
  ".git",
37
- ".svn",
38
38
  ".jj",
39
+ ".sl",
40
+ ".svn",
41
+ ".hg",
39
42
  "node_modules"
40
43
  ];
41
44
  /**
@@ -46,10 +49,21 @@ const DEFAULT_IGNORED_DIRS = [
46
49
  */
47
50
  const DEFAULT_IGNORED_LOCKFILES = [
48
51
  "package-lock.json",
49
- "npm-shrinkwrap.json",
50
52
  "pnpm-lock.yaml",
51
53
  "yarn.lock",
54
+ "MODULE.bazel.lock",
52
55
  "bun.lock",
56
+ "deno.lock",
57
+ "composer.lock",
58
+ "Package.resolved",
59
+ "Pipfile.lock",
60
+ "flake.lock",
61
+ "Cargo.lock",
62
+ "Gopkg.lock",
63
+ "pdm.lock",
64
+ "poetry.lock",
65
+ "uv.lock",
66
+ "npm-shrinkwrap.json",
53
67
  "bun.lockb"
54
68
  ];
55
69
  /**
@@ -63,6 +77,168 @@ const EDITORCONFIG_GLOBAL_SECTION_NAMES = ["*"];
63
77
  //#endregion
64
78
  //#region src/config-file.ts
65
79
  /**
80
+ * CommonJS require scoped to this ESM module for loading `.cjs` config files.
81
+ */
82
+ const require = createRequire(import.meta.url);
83
+ /**
84
+ * Node error codes that mean CommonJS require should fall back to ESM loading.
85
+ */
86
+ const requireFallbackErrorCodes = /* @__PURE__ */ new Set(["ERR_REQUIRE_ASYNC_MODULE", "ERR_REQUIRE_ESM"]);
87
+ /**
88
+ * Syntax errors that indicate module syntax was parsed through CommonJS.
89
+ */
90
+ const syntaxFallbackMessages = [
91
+ /Cannot use import statement outside a module/u,
92
+ /Unexpected token 'export'/u,
93
+ /Unexpected token 'import'/u
94
+ ];
95
+ /**
96
+ * Check whether an unknown config export is an object accepted by oxfmt.
97
+ *
98
+ * @param value - Runtime value to validate.
99
+ * @returns True when the value can be treated as an oxfmt options object.
100
+ */
101
+ function isConfigObject(value) {
102
+ return typeof value === "object" && value !== null && !Array.isArray(value);
103
+ }
104
+ /**
105
+ * Read the default export from a JavaScript or TypeScript config module.
106
+ *
107
+ * @param mod - Imported config module namespace.
108
+ * @returns Oxfmt config object from the module default export.
109
+ * @throws When the module has no default export or the default export is not an object.
110
+ */
111
+ function readConfigDefaultExport(mod) {
112
+ if (!Object.hasOwn(mod, "default")) throw new Error("Configuration file has no default export.");
113
+ const config = mod["default"];
114
+ if (!isConfigObject(config)) throw new Error("Configuration file must have a default export that is an object.");
115
+ return config;
116
+ }
117
+ /**
118
+ * Read a CommonJS module export as an oxfmt config object.
119
+ *
120
+ * @param config - Runtime CommonJS export value.
121
+ * @returns Oxfmt config object from the module export.
122
+ * @throws When the module export is not an object.
123
+ */
124
+ function readConfigModuleExports(config) {
125
+ if (!isConfigObject(config)) throw new Error("Configuration file must export an object.");
126
+ return config;
127
+ }
128
+ /**
129
+ * Import a JavaScript or TypeScript config module through jiti.
130
+ *
131
+ * @param resolvedPath - Absolute config file path to import.
132
+ * @param useCache - Whether jiti should use its filesystem and module caches.
133
+ * @returns Imported module namespace.
134
+ */
135
+ async function importJitiConfigModule(resolvedPath, useCache) {
136
+ return (await interopDefault(import("jiti")))(import.meta.url, useCache ? void 0 : {
137
+ fsCache: false,
138
+ moduleCache: false
139
+ }).import(resolvedPath);
140
+ }
141
+ /**
142
+ * Build a cache-busting import key from entry file metadata.
143
+ *
144
+ * @param resolvedPath - Absolute config file path.
145
+ * @returns Stable key that changes when the entry file mtime or size changes.
146
+ */
147
+ async function getFreshImportCacheKey(resolvedPath) {
148
+ const stats = await stat(resolvedPath, { bigint: true });
149
+ return `${stats.mtimeNs}:${stats.size}`;
150
+ }
151
+ /**
152
+ * Read a Node-style `code` field from an unknown thrown value.
153
+ *
154
+ * @param error - Thrown value to inspect.
155
+ * @returns Error code when present.
156
+ */
157
+ function getErrorCode(error) {
158
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
159
+ }
160
+ /**
161
+ * Check whether a loader error should continue the JS config fallback chain.
162
+ *
163
+ * @param error - Thrown value from require or native import.
164
+ * @returns True when the error is from loader compatibility, not config execution.
165
+ */
166
+ function isJavaScriptLoaderFallbackError(error) {
167
+ const code = getErrorCode(error);
168
+ if (code && requireFallbackErrorCodes.has(code)) return true;
169
+ return error instanceof SyntaxError && syntaxFallbackMessages.some((pattern) => pattern.test(error.message));
170
+ }
171
+ /**
172
+ * Import an ESM config module with entry-file cache busting.
173
+ *
174
+ * @param resolvedPath - Absolute config file path to import.
175
+ * @returns Imported module namespace.
176
+ */
177
+ async function importNativeFreshModule(resolvedPath) {
178
+ const url = pathToFileURL(resolvedPath);
179
+ url.searchParams.set("oxfmtConfigCacheBust", await getFreshImportCacheKey(resolvedPath));
180
+ return import(url.href);
181
+ }
182
+ /**
183
+ * Delete a CommonJS module and all cached children reachable from it.
184
+ *
185
+ * @param requirePath - Resolved CommonJS module id.
186
+ * @param seen - Visited module ids used to avoid cycles.
187
+ */
188
+ function deleteRequireCacheTree(requirePath, seen = /* @__PURE__ */ new Set()) {
189
+ if (seen.has(requirePath)) return;
190
+ seen.add(requirePath);
191
+ const cachedModule = require.cache[requirePath];
192
+ if (cachedModule) for (const child of cachedModule.children) deleteRequireCacheTree(child.id, seen);
193
+ Reflect.deleteProperty(require.cache, requirePath);
194
+ }
195
+ /**
196
+ * Require a CommonJS module after removing it from Node's require cache.
197
+ *
198
+ * @param resolvedPath - Absolute config file path to require.
199
+ * @returns Runtime module export value.
200
+ */
201
+ function requireFreshModule(resolvedPath) {
202
+ const requirePath = require.resolve(resolvedPath);
203
+ deleteRequireCacheTree(requirePath);
204
+ return require(requirePath);
205
+ }
206
+ /**
207
+ * Load a CommonJS config file without reusing Node's require cache.
208
+ *
209
+ * @param resolvedPath - Absolute `.cjs` config file path.
210
+ * @returns Oxfmt config object from default or module exports.
211
+ */
212
+ function requireFreshCommonJSConfig(resolvedPath) {
213
+ const configModule = requireFreshModule(resolvedPath);
214
+ if (Object.prototype.toString.call(configModule) === "[object Module]") return readConfigDefaultExport(configModule);
215
+ return readConfigModuleExports(configModule);
216
+ }
217
+ /**
218
+ * Load a JavaScript config file without reusing stale module contents.
219
+ *
220
+ * This supports CommonJS, native ESM, and jiti fallback loading so package type
221
+ * and syntax differences can be handled consistently.
222
+ *
223
+ * @param resolvedPath - Absolute `.js` config file path.
224
+ * @returns Imported module namespace with a default config export.
225
+ */
226
+ async function importFreshJavaScriptConfigModule(resolvedPath) {
227
+ try {
228
+ const configModule = requireFreshModule(resolvedPath);
229
+ if (Object.prototype.toString.call(configModule) === "[object Module]") return importNativeFreshModule(resolvedPath);
230
+ return { default: readConfigModuleExports(configModule) };
231
+ } catch (error) {
232
+ if (!isJavaScriptLoaderFallbackError(error)) throw error;
233
+ try {
234
+ return await importNativeFreshModule(resolvedPath);
235
+ } catch (importError) {
236
+ if (!isJavaScriptLoaderFallbackError(importError)) throw importError;
237
+ return importJitiConfigModule(resolvedPath, false);
238
+ }
239
+ }
240
+ }
241
+ /**
66
242
  * Build a cache key for path resolution (cwd + configPath).
67
243
  *
68
244
  * @param cwd - Current working directory.
@@ -110,7 +286,10 @@ async function resolveOxfmtrcPath(cwd, configPath) {
110
286
  const configFilePath = join(currentDir, filename);
111
287
  try {
112
288
  if ((await stat(configFilePath)).isFile()) return configFilePath;
113
- } catch {}
289
+ } catch (error) {
290
+ const code = error.code;
291
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
292
+ }
114
293
  }
115
294
  const parentDir = join(currentDir, "..");
116
295
  if (parentDir === currentDir) break;
@@ -121,13 +300,17 @@ async function resolveOxfmtrcPath(cwd, configPath) {
121
300
  * Read and parse an oxfmt config file.
122
301
  *
123
302
  * @param resolvedPath - Absolute path to config file.
303
+ * @param options - Config loading options.
124
304
  * @returns Parsed config object.
125
305
  */
126
- async function readConfigFromFile(resolvedPath) {
306
+ async function readConfigFromFile(resolvedPath, options = {}) {
127
307
  const extension = extname(resolvedPath);
308
+ const useCache = options.useCache !== false;
128
309
  if (extension === ".ts" || extension === ".mts" || extension === ".cts" || extension === ".js" || extension === ".mjs" || extension === ".cjs") {
129
- const mod = await (await interopDefault(import("jiti")))(import.meta.url).import(resolvedPath);
130
- return mod["default"] ?? mod;
310
+ if (!useCache && extension === ".cjs") return requireFreshCommonJSConfig(resolvedPath);
311
+ if (!useCache && extension === ".mjs") return readConfigDefaultExport(await importNativeFreshModule(resolvedPath));
312
+ if (!useCache && extension === ".js") return readConfigDefaultExport(await importFreshJavaScriptConfigModule(resolvedPath));
313
+ return readConfigDefaultExport(await importJitiConfigModule(resolvedPath, useCache));
131
314
  }
132
315
  const content = await readFile(resolvedPath, "utf8");
133
316
  if (extension === ".jsonc") {
@@ -247,7 +430,10 @@ async function resolveEditorconfigPath(startDir, onlyCwd = false) {
247
430
  const editorconfigPath = join(currentDir, EDITORCONFIG_FILE);
248
431
  try {
249
432
  if ((await stat(editorconfigPath)).isFile()) return editorconfigPath;
250
- } catch {}
433
+ } catch (error) {
434
+ const code = error.code;
435
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
436
+ }
251
437
  if (onlyCwd) break;
252
438
  const parentDir = join(currentDir, "..");
253
439
  if (parentDir === currentDir) break;
@@ -380,7 +566,13 @@ async function readEditorconfigFromFile(editorconfigPath, anchorDir) {
380
566
  }
381
567
  //#endregion
382
568
  //#region src/core.ts
569
+ /**
570
+ * Cache resolved config paths keyed by the effective lookup directory and optional config path.
571
+ */
383
572
  const resolveCache = /* @__PURE__ */ new Map();
573
+ /**
574
+ * Cache parsed and merged config objects keyed by resolved config and EditorConfig paths.
575
+ */
384
576
  const configCache = /* @__PURE__ */ new Map();
385
577
  /**
386
578
  * Resolve config + editorconfig and return merged config with metadata.
@@ -408,12 +600,12 @@ async function loadOxfmtConfig(options = {}) {
408
600
  const editorconfigCwd = isEditorconfigOptionsObject && editorconfig.cwd ? resolve(editorconfig.cwd) : void 0;
409
601
  const resolveKey = getResolveCacheKey(configLookupCwd, options.configPath);
410
602
  const editorconfigSearchDir = editorconfigCwd || getEditorconfigSearchDir(configLookupCwd, options.configPath);
411
- const editorconfigResolveKey = editorconfigCwd ? getEditorconfigResolveCacheKey(`${editorconfigCwd}::${options.configPath || ""}`) : getEditorconfigResolveCacheKey(resolveKey);
603
+ const editorconfigResolveKey = editorconfigCwd ? getEditorconfigResolveCacheKey(`${editorconfigCwd}::${options.configPath || ""}::onlyCwd=${String(onlyCwd)}`) : getEditorconfigResolveCacheKey(`${resolveKey}::onlyCwd=${String(onlyCwd)}`);
412
604
  const resolvedPath = useCache ? await cachePromise(resolveCache, resolveKey, () => resolveOxfmtrcPath(configLookupCwd, options.configPath)) : await resolveOxfmtrcPath(configLookupCwd, options.configPath);
413
605
  const editorconfigPath = useEditorconfig ? await (useCache ? cachePromise(resolveCache, editorconfigResolveKey, () => resolveEditorconfigPath(editorconfigSearchDir, onlyCwd)) : resolveEditorconfigPath(editorconfigSearchDir, onlyCwd)) : void 0;
414
606
  const anchorDir = dirname(resolvedPath || editorconfigPath || cwd);
415
607
  const loadTask = async () => {
416
- const oxfmtConfig = resolvedPath ? await readConfigFromFile(resolvedPath).catch((error) => {
608
+ const oxfmtConfig = resolvedPath ? await readConfigFromFile(resolvedPath, { useCache }).catch((error) => {
417
609
  throw new Error(`Failed to parse oxfmt configuration file at ${resolvedPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
418
610
  }) : {};
419
611
  if (!editorconfigPath) return oxfmtConfig;
@@ -440,8 +632,15 @@ async function loadOxfmtConfig(options = {}) {
440
632
  }
441
633
  //#endregion
442
634
  //#region src/ignore.ts
635
+ /**
636
+ * Cache parsed ignore file matchers by ignore file path.
637
+ */
443
638
  const ignoreMatcherCache = /* @__PURE__ */ new Map();
444
639
  /**
640
+ * Cache compiled matchers for config-level `ignorePatterns`.
641
+ */
642
+ const configIgnoreMatcherCache = /* @__PURE__ */ new Map();
643
+ /**
445
644
  * Check whether a file is under oxfmt's default ignored directories.
446
645
  *
447
646
  * @param filepath - Absolute file path to test.
@@ -509,6 +708,27 @@ async function matchIgnoreFile(filepath, ignoreFilePath, useCache, baseDir) {
509
708
  return matcher.ignores(relativeToIgnore);
510
709
  }
511
710
  /**
711
+ * Match a file path against ordered ignore files while preserving negation state.
712
+ *
713
+ * @param filepath - Absolute file path.
714
+ * @param ignoreFileEntries - Ignore files in increasing precedence order.
715
+ * @param useCache - Whether to use matcher cache.
716
+ * @returns True when the ordered ignore files mark the file as ignored.
717
+ */
718
+ async function matchIgnoreFileChain(filepath, ignoreFileEntries, useCache) {
719
+ let ignored = false;
720
+ for (const entry of ignoreFileEntries) {
721
+ const matcher = await loadIgnoreMatcher(entry.path, useCache);
722
+ if (!matcher) continue;
723
+ const relativeToIgnore = relativeSafe(entry.baseDir ?? dirname(entry.path), filepath);
724
+ if (relativeToIgnore === ".." || relativeToIgnore.startsWith("../")) continue;
725
+ const result = matcher.test(relativeToIgnore);
726
+ if (result.ignored) ignored = true;
727
+ else if (result.unignored) ignored = false;
728
+ }
729
+ return ignored;
730
+ }
731
+ /**
512
732
  * Resolve an ignore file path against cwd when needed.
513
733
  *
514
734
  * @param path - Absolute or relative path.
@@ -580,19 +800,19 @@ async function collectGitignorePaths(filepath) {
580
800
  * @param filepath - Absolute file path.
581
801
  * @param configDir - Resolved config directory.
582
802
  * @param patterns - Config ignore patterns.
803
+ * @param useCache - Whether to reuse compiled pattern matchers.
583
804
  * @returns True when patterns mark the file as ignored.
584
805
  */
585
- function matchConfigIgnorePatterns(filepath, configDir, patterns) {
806
+ function matchConfigIgnorePatterns(filepath, configDir, patterns, useCache) {
586
807
  const relativeFile = relativeSafe(configDir, filepath);
587
- let ignored = false;
588
- for (const rawPattern of patterns) {
589
- if (!rawPattern) continue;
590
- const isNegative = rawPattern.startsWith("!");
591
- const pattern = isNegative ? rawPattern.slice(1) : rawPattern;
592
- if (!pattern) continue;
593
- if (picomatch(pattern, { dot: true })(relativeFile)) ignored = !isNegative;
594
- }
595
- return ignored;
808
+ if (relativeFile === ".." || relativeFile.startsWith("../")) return false;
809
+ if (!useCache) return ignore().add(patterns).ignores(relativeFile);
810
+ const cacheKey = `${configDir}::${JSON.stringify(patterns)}`;
811
+ const cachedMatcher = configIgnoreMatcherCache.get(cacheKey);
812
+ if (cachedMatcher) return cachedMatcher.ignores(relativeFile);
813
+ const matcher = ignore().add(patterns);
814
+ configIgnoreMatcherCache.set(cacheKey, matcher);
815
+ return matcher.ignores(relativeFile);
596
816
  }
597
817
  /**
598
818
  * Resolve whether a file should be ignored using oxfmt-like CLI semantics.
@@ -625,28 +845,26 @@ async function isOxfmtIgnored(options) {
625
845
  reason: "lockfile"
626
846
  };
627
847
  const explicitIgnorePaths = (typeof options.ignorePath === "string" ? [options.ignorePath] : options.ignorePath)?.map((path) => resolveIgnoreFilePath(path, cwd));
628
- if (explicitIgnorePaths && explicitIgnorePaths.length > 0) {
629
- for (const ignoreFilePath of explicitIgnorePaths) if (await matchIgnoreFile(filepath, ignoreFilePath, useCache)) return {
630
- ignored: true,
631
- reason: "ignore-path"
632
- };
633
- } else {
634
- const { paths: gitignorePaths, repoRoot } = await collectGitignorePaths(filepath);
635
- for (const ignorePath of gitignorePaths) if (await matchIgnoreFile(filepath, ignorePath, useCache)) return {
848
+ const { paths: gitignorePaths, repoRoot } = await collectGitignorePaths(filepath);
849
+ if (await matchIgnoreFileChain(filepath, [...gitignorePaths].reverse().map((path) => ({ path })), useCache)) return {
850
+ ignored: true,
851
+ reason: "gitignore"
852
+ };
853
+ if (repoRoot) {
854
+ if (await matchIgnoreFile(filepath, join(repoRoot, ".git", "info", "exclude"), useCache, repoRoot)) return {
636
855
  ignored: true,
637
- reason: "gitignore"
856
+ reason: "git-info-exclude"
638
857
  };
639
- if (repoRoot) {
640
- if (await matchIgnoreFile(filepath, join(repoRoot, ".git", "info", "exclude"), useCache, repoRoot)) return {
641
- ignored: true,
642
- reason: "git-info-exclude"
643
- };
644
- }
645
- if (await matchIgnoreFile(filepath, resolve(cwd, ".prettierignore"), useCache)) return {
858
+ }
859
+ if (explicitIgnorePaths && explicitIgnorePaths.length > 0) {
860
+ if (await matchIgnoreFileChain(filepath, explicitIgnorePaths.map((path) => ({ path })), useCache)) return {
646
861
  ignored: true,
647
- reason: "prettierignore"
862
+ reason: "ignore-path"
648
863
  };
649
- }
864
+ } else if (await matchIgnoreFile(filepath, resolve(cwd, ".prettierignore"), useCache)) return {
865
+ ignored: true,
866
+ reason: "prettierignore"
867
+ };
650
868
  if (!loadConfigForIgnorePatterns) return { ignored: false };
651
869
  const configResult = await loadOxfmtConfig({
652
870
  cwd,
@@ -656,7 +874,7 @@ async function isOxfmtIgnored(options) {
656
874
  editorconfig: false,
657
875
  useCache
658
876
  });
659
- if (includeConfigIgnorePatterns && configResult.dirname && configResult.config.ignorePatterns && configResult.config.ignorePatterns.length > 0 && matchConfigIgnorePatterns(filepath, configResult.dirname, configResult.config.ignorePatterns)) return {
877
+ if (includeConfigIgnorePatterns && configResult.dirname && configResult.config.ignorePatterns && configResult.config.ignorePatterns.length > 0 && matchConfigIgnorePatterns(filepath, configResult.dirname, configResult.config.ignorePatterns, useCache)) return {
660
878
  ignored: true,
661
879
  reason: "config-ignore-patterns"
662
880
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "load-oxfmt-config",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Load and resolve oxfmt configuration files, including explicit JS/TS config paths, and merge supported `.editorconfig` settings for Oxfmt.",
5
5
  "keywords": [
6
6
  "editorconfig",
@@ -42,29 +42,27 @@
42
42
  "access": "public"
43
43
  },
44
44
  "dependencies": {
45
- "@ntnyq/utils": "^0.15.0",
45
+ "@ntnyq/utils": "^0.16.0",
46
46
  "editorconfig": "^3.0.2",
47
47
  "ignore": "^7.0.5",
48
48
  "jiti": "^2.7.0",
49
- "jsonc-parser": "^3.3.1",
50
- "picomatch": "^4.0.4"
49
+ "jsonc-parser": "^3.3.1"
51
50
  },
52
51
  "devDependencies": {
53
52
  "@ntnyq/tsconfig": "^3.1.0",
54
- "@types/node": "^26.0.0",
55
- "@types/picomatch": "^4.0.3",
56
- "@typescript/native-preview": "^7.0.0-dev.20260622.1",
53
+ "@types/node": "^26.0.1",
54
+ "@typescript/native-preview": "^7.0.0-dev.20260629.1",
57
55
  "bumpp": "^11.1.0",
58
56
  "husky": "^9.1.7",
59
57
  "nano-staged": "^1.0.2",
60
58
  "npm-run-all2": "^9.0.2",
61
- "oxfmt": "^0.56.0",
62
- "oxlint": "^1.71.0",
59
+ "oxfmt": "^0.57.0",
60
+ "oxlint": "^1.72.0",
63
61
  "tsdown": "^0.22.3",
64
62
  "vitest": "^4.1.9"
65
63
  },
66
64
  "peerDependencies": {
67
- "oxfmt": ">=0.56.0"
65
+ "oxfmt": ">=0.57.0"
68
66
  },
69
67
  "nano-staged": {
70
68
  "*.{js,ts,mjs,tsx}": "oxlint --fix",