load-oxfmt-config 0.13.0 → 0.15.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 +10 -9
- package/dist/index.d.mts +51 -24
- package/dist/index.mjs +180 -68
- package/package.json +16 -11
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.
|
|
13
|
+
- 📦 **Multiple formats** - Auto-discovers `.oxfmtrc.json`, `.oxfmtrc.jsonc`, `oxfmt.config.ts`, and `oxfmt.config.mts`, 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
|
|
@@ -32,6 +32,7 @@ pnpm add load-oxfmt-config
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
> `oxfmt` is a peer dependency and should be installed alongside this package.
|
|
35
|
+
> Node.js `^22.13.0 || >=24` is supported.
|
|
35
36
|
|
|
36
37
|
## Usage
|
|
37
38
|
|
|
@@ -370,6 +371,7 @@ When `configPath` is not provided, the loader automatically searches for config
|
|
|
370
371
|
- `.oxfmtrc.json`
|
|
371
372
|
- `.oxfmtrc.jsonc`
|
|
372
373
|
- `oxfmt.config.ts`
|
|
374
|
+
- `oxfmt.config.mts`
|
|
373
375
|
3. **Stops when:**
|
|
374
376
|
- A valid config file is found
|
|
375
377
|
- Reaches the filesystem root
|
|
@@ -407,7 +409,7 @@ JSON with comments support:
|
|
|
407
409
|
}
|
|
408
410
|
```
|
|
409
411
|
|
|
410
|
-
### TypeScript (`oxfmt.config.ts`)
|
|
412
|
+
### TypeScript (`oxfmt.config.ts` / `oxfmt.config.mts`)
|
|
411
413
|
|
|
412
414
|
```ts
|
|
413
415
|
export default {
|
|
@@ -460,7 +462,7 @@ Notes:
|
|
|
460
462
|
- `node_modules` can be included by passing `withNodeModules: true`.
|
|
461
463
|
|
|
462
464
|
- 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.
|
|
463
|
-
- `ignorePatterns` use gitignore semantics and are interpreted relative to the resolved oxfmt config directory.
|
|
465
|
+
- `ignorePatterns` use gitignore semantics and are interpreted relative to the resolved oxfmt config directory. Parent-directory (`..`) path segments are rejected because patterns cannot match files outside that directory.
|
|
464
466
|
- `includeConfigIgnorePatterns` defaults to `true` to preserve current behavior.
|
|
465
467
|
- `loadConfigForIgnorePatterns` defaults to `true` to preserve current behavior.
|
|
466
468
|
- Nested config behavior follows oxfmt semantics:
|
|
@@ -471,14 +473,13 @@ Notes:
|
|
|
471
473
|
|
|
472
474
|
## Precedence
|
|
473
475
|
|
|
474
|
-
The merged result follows
|
|
476
|
+
The merged result follows oxfmt's fallback semantics:
|
|
475
477
|
|
|
476
|
-
1. Root-
|
|
477
|
-
2. Root-level values from `.oxfmtrc.json`, `.oxfmtrc.jsonc`, or `oxfmt.config.
|
|
478
|
-
3. Overrides
|
|
479
|
-
4. Overrides declared directly in the oxfmt config file
|
|
478
|
+
1. Root and section-specific `.editorconfig` values provide defaults for unset fields
|
|
479
|
+
2. Root-level values from `.oxfmtrc.json`, `.oxfmtrc.jsonc`, `oxfmt.config.ts`, or `oxfmt.config.mts` take precedence over all `.editorconfig` values
|
|
480
|
+
3. Overrides declared directly in the oxfmt config file have the highest priority
|
|
480
481
|
|
|
481
|
-
|
|
482
|
+
Section-specific `.editorconfig` entries are represented as generated `overrides` in the static result. Fields already defined at the oxfmt config root are omitted from those generated overrides, and explicit oxfmt `overrides` are appended after them.
|
|
482
483
|
|
|
483
484
|
## Limitations
|
|
484
485
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FormatConfig } from "oxfmt";
|
|
2
|
-
|
|
3
2
|
//#region src/types.d.ts
|
|
4
3
|
/**
|
|
5
4
|
* Object-form `.editorconfig` option for fine-grained lookup control.
|
|
@@ -88,7 +87,8 @@ interface OxfmtOptions extends FormatConfig {
|
|
|
88
87
|
/**
|
|
89
88
|
* Ignore files matching these glob patterns.
|
|
90
89
|
*
|
|
91
|
-
* Patterns
|
|
90
|
+
* Patterns use gitignore-style matching rooted at the oxfmt configuration
|
|
91
|
+
* directory. Parent-directory (`..`) path segments are rejected.
|
|
92
92
|
*/
|
|
93
93
|
ignorePatterns?: string[];
|
|
94
94
|
/**
|
|
@@ -187,27 +187,6 @@ interface IsOxfmtIgnoredResult {
|
|
|
187
187
|
reason?: 'default-dir' | 'lockfile' | 'gitignore' | 'git-info-exclude' | 'prettierignore' | 'ignore-path' | 'config-ignore-patterns';
|
|
188
188
|
}
|
|
189
189
|
//#endregion
|
|
190
|
-
//#region src/config-file.d.ts
|
|
191
|
-
/**
|
|
192
|
-
* Resolve the oxfmt config file path.
|
|
193
|
-
*
|
|
194
|
-
* - If `configPath` is provided, absolute paths are returned as-is;
|
|
195
|
-
* relative paths are joined to `cwd`.
|
|
196
|
-
* - Otherwise, walk upward from `cwd` to find known filenames.
|
|
197
|
-
*
|
|
198
|
-
* @param cwd - Starting directory for resolution.
|
|
199
|
-
* @param configPath - Optional explicit path.
|
|
200
|
-
* @returns Absolute path to config file, or undefined when not found.
|
|
201
|
-
*
|
|
202
|
-
* @example
|
|
203
|
-
* ```ts
|
|
204
|
-
* import { resolveOxfmtrcPath } from 'load-oxfmt-config'
|
|
205
|
-
*
|
|
206
|
-
* const path = await resolveOxfmtrcPath(process.cwd())
|
|
207
|
-
* ```
|
|
208
|
-
*/
|
|
209
|
-
declare function resolveOxfmtrcPath(cwd: string, configPath?: string): Promise<string | undefined>;
|
|
210
|
-
//#endregion
|
|
211
190
|
//#region src/core.d.ts
|
|
212
191
|
/**
|
|
213
192
|
* Resolve config + editorconfig and return merged config with metadata.
|
|
@@ -244,4 +223,52 @@ declare function loadOxfmtConfig(options?: LoadOxfmtConfigOptions): Promise<Load
|
|
|
244
223
|
*/
|
|
245
224
|
declare function isOxfmtIgnored(options: IsOxfmtIgnoredOptions): Promise<IsOxfmtIgnoredResult>;
|
|
246
225
|
//#endregion
|
|
247
|
-
|
|
226
|
+
//#region src/config.d.ts
|
|
227
|
+
/**
|
|
228
|
+
* Build a cache key for path resolution (cwd + configPath).
|
|
229
|
+
*
|
|
230
|
+
* @param cwd - Current working directory.
|
|
231
|
+
* @param configPath - Optional config path.
|
|
232
|
+
* @returns Cache key for resolve cache.
|
|
233
|
+
*/
|
|
234
|
+
declare function getResolveCacheKey(cwd: string, configPath?: string): string;
|
|
235
|
+
/**
|
|
236
|
+
* Build a cache key for config content; prefixes missing entries.
|
|
237
|
+
*
|
|
238
|
+
* @param resolvedPath - Resolved oxfmt config path.
|
|
239
|
+
* @param editorconfigPath - Resolved editorconfig path.
|
|
240
|
+
* @param resolveKey - Resolution cache key.
|
|
241
|
+
* @returns Cache key for config content cache.
|
|
242
|
+
*/
|
|
243
|
+
declare function getConfigCacheKey(resolvedPath: string | undefined, editorconfigPath: string | undefined, resolveKey: string): string;
|
|
244
|
+
/**
|
|
245
|
+
* Resolve the oxfmt config file path.
|
|
246
|
+
*
|
|
247
|
+
* - If `configPath` is provided, absolute paths are returned as-is;
|
|
248
|
+
* relative paths are joined to `cwd`.
|
|
249
|
+
* - Otherwise, walk upward from `cwd` to find known filenames.
|
|
250
|
+
*
|
|
251
|
+
* @param cwd - Starting directory for resolution.
|
|
252
|
+
* @param configPath - Optional explicit path.
|
|
253
|
+
* @returns Absolute path to config file, or undefined when not found.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* import { resolveOxfmtrcPath } from 'load-oxfmt-config'
|
|
258
|
+
*
|
|
259
|
+
* const path = await resolveOxfmtrcPath(process.cwd())
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
declare function resolveOxfmtrcPath(cwd: string, configPath?: string): Promise<string | undefined>;
|
|
263
|
+
/**
|
|
264
|
+
* Read and parse an oxfmt config file.
|
|
265
|
+
*
|
|
266
|
+
* @param resolvedPath - Absolute path to config file.
|
|
267
|
+
* @param options - Config loading options.
|
|
268
|
+
* @returns Parsed config object.
|
|
269
|
+
*/
|
|
270
|
+
declare function readConfigFromFile(resolvedPath: string, options?: {
|
|
271
|
+
useCache?: boolean;
|
|
272
|
+
}): Promise<OxfmtOptions>;
|
|
273
|
+
//#endregion
|
|
274
|
+
export { EditorconfigOption, IsOxfmtIgnoredOptions, IsOxfmtIgnoredResult, LoadOxfmtConfigOptions, LoadOxfmtConfigResult, OxfmtConfigOverride, OxfmtOptions, getConfigCacheKey, getResolveCacheKey, isOxfmtIgnored, loadOxfmtConfig, readConfigFromFile, resolveOxfmtrcPath };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { interopDefault, isBoolean, isNumber, isObject } from "@ntnyq/utils";
|
|
4
|
+
import { hasOwn, interopDefault, isBoolean, isNumber, isObject, isRecord, isString, isUndefined } from "@ntnyq/utils";
|
|
5
5
|
import { readFile, stat } from "node:fs/promises";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
@@ -15,7 +15,8 @@ import ignore from "ignore";
|
|
|
15
15
|
const OXFMT_CONFIG_FILES = [
|
|
16
16
|
".oxfmtrc.json",
|
|
17
17
|
".oxfmtrc.jsonc",
|
|
18
|
-
"oxfmt.config.ts"
|
|
18
|
+
"oxfmt.config.ts",
|
|
19
|
+
"oxfmt.config.mts"
|
|
19
20
|
];
|
|
20
21
|
/**
|
|
21
22
|
* Supported extensions for explicit config paths.
|
|
@@ -75,7 +76,47 @@ const EDITORCONFIG_FILE = ".editorconfig";
|
|
|
75
76
|
*/
|
|
76
77
|
const EDITORCONFIG_GLOBAL_SECTION_NAMES = ["*"];
|
|
77
78
|
//#endregion
|
|
78
|
-
//#region src/
|
|
79
|
+
//#region src/utils.ts
|
|
80
|
+
/**
|
|
81
|
+
* Return a cached promise by key, creating and storing it on miss.
|
|
82
|
+
*
|
|
83
|
+
* If the promise rejects, the cache entry is removed so future calls can retry.
|
|
84
|
+
*
|
|
85
|
+
* @param cache - Map used to store inflight/resolved promises.
|
|
86
|
+
* @param key - Cache key.
|
|
87
|
+
* @param factory - Factory to create the promise when missing.
|
|
88
|
+
* @returns Cached or newly created promise.
|
|
89
|
+
*/
|
|
90
|
+
function cachePromise(cache, key, factory) {
|
|
91
|
+
const cached = cache.get(key);
|
|
92
|
+
if (cached) return cached;
|
|
93
|
+
const task = factory().catch((error) => {
|
|
94
|
+
cache.delete(key);
|
|
95
|
+
throw error;
|
|
96
|
+
});
|
|
97
|
+
cache.set(key, task);
|
|
98
|
+
return task;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Normalize a filesystem path to POSIX-style separators.
|
|
102
|
+
*
|
|
103
|
+
* @param path - Original path.
|
|
104
|
+
* @returns Path using `/` as separator.
|
|
105
|
+
*/
|
|
106
|
+
function toPosixPath(path) {
|
|
107
|
+
return path.replaceAll("\\", "/");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Split a path into non-empty segments.
|
|
111
|
+
*
|
|
112
|
+
* @param path - Original path.
|
|
113
|
+
* @returns Path segments.
|
|
114
|
+
*/
|
|
115
|
+
function splitPathSegments(path) {
|
|
116
|
+
return path.split(/[\\/]+/u).filter(Boolean);
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/config.ts
|
|
79
120
|
/**
|
|
80
121
|
* CommonJS require scoped to this ESM module for loading `.cjs` config files.
|
|
81
122
|
*/
|
|
@@ -93,13 +134,33 @@ const syntaxFallbackMessages = [
|
|
|
93
134
|
/Unexpected token 'import'/u
|
|
94
135
|
];
|
|
95
136
|
/**
|
|
137
|
+
* ESM namespaces loaded from `.js` config files while bypassing normal caches.
|
|
138
|
+
*
|
|
139
|
+
* Node can load synchronous ESM through `require()`, but deleting
|
|
140
|
+
* `require.cache` does not invalidate the ESM loader cache. Remember the entry
|
|
141
|
+
* file version so changed configs can switch to a cache-busted native import
|
|
142
|
+
* without evaluating the first load twice.
|
|
143
|
+
*/
|
|
144
|
+
const freshJavaScriptModuleCache = /* @__PURE__ */ new Map();
|
|
145
|
+
/**
|
|
96
146
|
* Check whether an unknown config export is an object accepted by oxfmt.
|
|
97
147
|
*
|
|
98
148
|
* @param value - Runtime value to validate.
|
|
99
149
|
* @returns True when the value can be treated as an oxfmt options object.
|
|
100
150
|
*/
|
|
101
151
|
function isConfigObject(value) {
|
|
102
|
-
return
|
|
152
|
+
return isRecord(value);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Read a parsed JSON-like config value as an oxfmt config object.
|
|
156
|
+
*
|
|
157
|
+
* @param config - Runtime config value.
|
|
158
|
+
* @returns Oxfmt config object.
|
|
159
|
+
* @throws When the config value is not an object.
|
|
160
|
+
*/
|
|
161
|
+
function readParsedConfigObject(config) {
|
|
162
|
+
if (!isConfigObject(config)) throw new Error("Configuration file must be an object.");
|
|
163
|
+
return config;
|
|
103
164
|
}
|
|
104
165
|
/**
|
|
105
166
|
* Read the default export from a JavaScript or TypeScript config module.
|
|
@@ -109,7 +170,7 @@ function isConfigObject(value) {
|
|
|
109
170
|
* @throws When the module has no default export or the default export is not an object.
|
|
110
171
|
*/
|
|
111
172
|
function readConfigDefaultExport(mod) {
|
|
112
|
-
if (!
|
|
173
|
+
if (!hasOwn(mod, "default")) throw new Error("Configuration file has no default export.");
|
|
113
174
|
const config = mod["default"];
|
|
114
175
|
if (!isConfigObject(config)) throw new Error("Configuration file must have a default export that is an object.");
|
|
115
176
|
return config;
|
|
@@ -155,7 +216,7 @@ async function getFreshImportCacheKey(resolvedPath) {
|
|
|
155
216
|
* @returns Error code when present.
|
|
156
217
|
*/
|
|
157
218
|
function getErrorCode(error) {
|
|
158
|
-
return
|
|
219
|
+
return isObject(error) && "code" in error && isString(error.code) ? error.code : void 0;
|
|
159
220
|
}
|
|
160
221
|
/**
|
|
161
222
|
* Check whether a loader error should continue the JS config fallback chain.
|
|
@@ -189,7 +250,9 @@ function deleteRequireCacheTree(requirePath, seen = /* @__PURE__ */ new Set()) {
|
|
|
189
250
|
if (seen.has(requirePath)) return;
|
|
190
251
|
seen.add(requirePath);
|
|
191
252
|
const cachedModule = require.cache[requirePath];
|
|
192
|
-
if (cachedModule)
|
|
253
|
+
if (cachedModule) {
|
|
254
|
+
for (const child of cachedModule.children) if (!splitPathSegments(child.id).includes("node_modules")) deleteRequireCacheTree(child.id, seen);
|
|
255
|
+
}
|
|
193
256
|
Reflect.deleteProperty(require.cache, requirePath);
|
|
194
257
|
}
|
|
195
258
|
/**
|
|
@@ -224,14 +287,37 @@ function requireFreshCommonJSConfig(resolvedPath) {
|
|
|
224
287
|
* @returns Imported module namespace with a default config export.
|
|
225
288
|
*/
|
|
226
289
|
async function importFreshJavaScriptConfigModule(resolvedPath) {
|
|
290
|
+
const cacheKey = await getFreshImportCacheKey(resolvedPath);
|
|
291
|
+
const cachedModule = freshJavaScriptModuleCache.get(resolvedPath);
|
|
292
|
+
if (cachedModule?.cacheKey === cacheKey) return cachedModule.namespace;
|
|
293
|
+
if (cachedModule) {
|
|
294
|
+
const namespace = await importNativeFreshModule(resolvedPath);
|
|
295
|
+
freshJavaScriptModuleCache.set(resolvedPath, {
|
|
296
|
+
cacheKey,
|
|
297
|
+
namespace
|
|
298
|
+
});
|
|
299
|
+
return namespace;
|
|
300
|
+
}
|
|
227
301
|
try {
|
|
228
302
|
const configModule = requireFreshModule(resolvedPath);
|
|
229
|
-
if (Object.prototype.toString.call(configModule) === "[object Module]")
|
|
303
|
+
if (Object.prototype.toString.call(configModule) === "[object Module]") {
|
|
304
|
+
const namespace = configModule;
|
|
305
|
+
freshJavaScriptModuleCache.set(resolvedPath, {
|
|
306
|
+
cacheKey,
|
|
307
|
+
namespace
|
|
308
|
+
});
|
|
309
|
+
return namespace;
|
|
310
|
+
}
|
|
230
311
|
return { default: readConfigModuleExports(configModule) };
|
|
231
312
|
} catch (error) {
|
|
232
313
|
if (!isJavaScriptLoaderFallbackError(error)) throw error;
|
|
233
314
|
try {
|
|
234
|
-
|
|
315
|
+
const namespace = await importNativeFreshModule(resolvedPath);
|
|
316
|
+
freshJavaScriptModuleCache.set(resolvedPath, {
|
|
317
|
+
cacheKey,
|
|
318
|
+
namespace
|
|
319
|
+
});
|
|
320
|
+
return namespace;
|
|
235
321
|
} catch (importError) {
|
|
236
322
|
if (!isJavaScriptLoaderFallbackError(importError)) throw importError;
|
|
237
323
|
return importJitiConfigModule(resolvedPath, false);
|
|
@@ -246,7 +332,7 @@ async function importFreshJavaScriptConfigModule(resolvedPath) {
|
|
|
246
332
|
* @returns Cache key for resolve cache.
|
|
247
333
|
*/
|
|
248
334
|
function getResolveCacheKey(cwd, configPath) {
|
|
249
|
-
return
|
|
335
|
+
return JSON.stringify([cwd, configPath ?? null]);
|
|
250
336
|
}
|
|
251
337
|
/**
|
|
252
338
|
* Build a cache key for config content; prefixes missing entries.
|
|
@@ -257,7 +343,11 @@ function getResolveCacheKey(cwd, configPath) {
|
|
|
257
343
|
* @returns Cache key for config content cache.
|
|
258
344
|
*/
|
|
259
345
|
function getConfigCacheKey(resolvedPath, editorconfigPath, resolveKey) {
|
|
260
|
-
return
|
|
346
|
+
return JSON.stringify([
|
|
347
|
+
resolvedPath ?? null,
|
|
348
|
+
editorconfigPath ?? null,
|
|
349
|
+
resolveKey
|
|
350
|
+
]);
|
|
261
351
|
}
|
|
262
352
|
/**
|
|
263
353
|
* Resolve the oxfmt config file path.
|
|
@@ -321,55 +411,15 @@ async function readConfigFromFile(resolvedPath, options = {}) {
|
|
|
321
411
|
});
|
|
322
412
|
if (parseErrors.length > 0) {
|
|
323
413
|
const firstError = parseErrors[0];
|
|
324
|
-
const errorCode = firstError
|
|
414
|
+
const errorCode = isUndefined(firstError) ? "Unknown" : printParseErrorCode(firstError.error);
|
|
325
415
|
throw new Error(`Invalid JSONC syntax: ${errorCode}`);
|
|
326
416
|
}
|
|
327
|
-
return parsed
|
|
417
|
+
return readParsedConfigObject(isUndefined(parsed) && content.trim() === "" ? {} : parsed);
|
|
328
418
|
}
|
|
329
|
-
if (extension === ".json") return JSON.parse(content);
|
|
330
|
-
if (!extension) return JSON.parse(content);
|
|
419
|
+
if (extension === ".json") return readParsedConfigObject(JSON.parse(content));
|
|
420
|
+
if (!extension) return readParsedConfigObject(JSON.parse(content));
|
|
331
421
|
if (!OXFMT_EXPLICIT_CONFIG_EXTENSIONS.includes(extension)) throw new Error(`Unsupported oxfmt config extension "${extension}" at ${resolvedPath}`);
|
|
332
|
-
return JSON.parse(content);
|
|
333
|
-
}
|
|
334
|
-
//#endregion
|
|
335
|
-
//#region src/utils.ts
|
|
336
|
-
/**
|
|
337
|
-
* Return a cached promise by key, creating and storing it on miss.
|
|
338
|
-
*
|
|
339
|
-
* If the promise rejects, the cache entry is removed so future calls can retry.
|
|
340
|
-
*
|
|
341
|
-
* @param cache - Map used to store inflight/resolved promises.
|
|
342
|
-
* @param key - Cache key.
|
|
343
|
-
* @param factory - Factory to create the promise when missing.
|
|
344
|
-
* @returns Cached or newly created promise.
|
|
345
|
-
*/
|
|
346
|
-
function cachePromise(cache, key, factory) {
|
|
347
|
-
const cached = cache.get(key);
|
|
348
|
-
if (cached) return cached;
|
|
349
|
-
const task = factory().catch((error) => {
|
|
350
|
-
cache.delete(key);
|
|
351
|
-
throw error;
|
|
352
|
-
});
|
|
353
|
-
cache.set(key, task);
|
|
354
|
-
return task;
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Normalize a filesystem path to POSIX-style separators.
|
|
358
|
-
*
|
|
359
|
-
* @param path - Original path.
|
|
360
|
-
* @returns Path using `/` as separator.
|
|
361
|
-
*/
|
|
362
|
-
function toPosixPath(path) {
|
|
363
|
-
return path.replaceAll("\\", "/");
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Split a path into non-empty segments.
|
|
367
|
-
*
|
|
368
|
-
* @param path - Original path.
|
|
369
|
-
* @returns Path segments.
|
|
370
|
-
*/
|
|
371
|
-
function splitPathSegments(path) {
|
|
372
|
-
return path.split(/[\\/]+/u).filter(Boolean);
|
|
422
|
+
return readParsedConfigObject(JSON.parse(content));
|
|
373
423
|
}
|
|
374
424
|
//#endregion
|
|
375
425
|
//#region src/editorconfig.ts
|
|
@@ -409,12 +459,18 @@ function mergeRootOptions(oxfmtConfig, editorconfigRootOptions) {
|
|
|
409
459
|
/**
|
|
410
460
|
* Merges EditorConfig-derived overrides with explicit oxfmt overrides.
|
|
411
461
|
*
|
|
412
|
-
* @param
|
|
462
|
+
* @param oxfmtConfig - Explicit root options and overrides from the oxfmt config.
|
|
413
463
|
* @param editorconfigOverrides - Overrides derived from .editorconfig sections.
|
|
414
464
|
* @returns The merged overrides array, or undefined when no overrides exist.
|
|
415
465
|
*/
|
|
416
|
-
function mergeOverrides(
|
|
417
|
-
const mergedOverrides = [...editorconfigOverrides
|
|
466
|
+
function mergeOverrides(oxfmtConfig, editorconfigOverrides) {
|
|
467
|
+
const mergedOverrides = [...editorconfigOverrides.flatMap((override) => {
|
|
468
|
+
const options = Object.fromEntries(Object.entries(override.options || {}).filter(([name]) => !Object.hasOwn(oxfmtConfig, name)));
|
|
469
|
+
return Object.keys(options).length > 0 ? [{
|
|
470
|
+
...override,
|
|
471
|
+
options
|
|
472
|
+
}] : [];
|
|
473
|
+
}), ...oxfmtConfig.overrides || []];
|
|
418
474
|
return mergedOverrides.length > 0 ? mergedOverrides : void 0;
|
|
419
475
|
}
|
|
420
476
|
/**
|
|
@@ -611,7 +667,7 @@ async function loadOxfmtConfig(options = {}) {
|
|
|
611
667
|
if (!editorconfigPath) return oxfmtConfig;
|
|
612
668
|
const editorconfigData = await readEditorconfigFromFile(editorconfigPath, anchorDir);
|
|
613
669
|
const mergedConfig = mergeRootOptions(oxfmtConfig, editorconfigData.rootOptions);
|
|
614
|
-
const mergedOverrides = mergeOverrides(oxfmtConfig
|
|
670
|
+
const mergedOverrides = mergeOverrides(oxfmtConfig, editorconfigData.overrides);
|
|
615
671
|
if (!mergedOverrides) return mergedConfig;
|
|
616
672
|
return {
|
|
617
673
|
...mergedConfig,
|
|
@@ -717,16 +773,25 @@ async function matchIgnoreFile(filepath, ignoreFilePath, useCache, baseDir) {
|
|
|
717
773
|
*/
|
|
718
774
|
async function matchIgnoreFileChain(filepath, ignoreFileEntries, useCache) {
|
|
719
775
|
let ignored = false;
|
|
776
|
+
let matched = false;
|
|
720
777
|
for (const entry of ignoreFileEntries) {
|
|
721
778
|
const matcher = await loadIgnoreMatcher(entry.path, useCache);
|
|
722
779
|
if (!matcher) continue;
|
|
723
780
|
const relativeToIgnore = relativeSafe(entry.baseDir ?? dirname(entry.path), filepath);
|
|
724
781
|
if (relativeToIgnore === ".." || relativeToIgnore.startsWith("../")) continue;
|
|
725
782
|
const result = matcher.test(relativeToIgnore);
|
|
726
|
-
if (result.ignored)
|
|
727
|
-
|
|
783
|
+
if (result.ignored) {
|
|
784
|
+
ignored = true;
|
|
785
|
+
matched = true;
|
|
786
|
+
} else if (result.unignored) {
|
|
787
|
+
ignored = false;
|
|
788
|
+
matched = true;
|
|
789
|
+
}
|
|
728
790
|
}
|
|
729
|
-
return
|
|
791
|
+
return {
|
|
792
|
+
ignored,
|
|
793
|
+
matched
|
|
794
|
+
};
|
|
730
795
|
}
|
|
731
796
|
/**
|
|
732
797
|
* Resolve an ignore file path against cwd when needed.
|
|
@@ -757,6 +822,38 @@ async function hasGitEntry(dir) {
|
|
|
757
822
|
}
|
|
758
823
|
}
|
|
759
824
|
/**
|
|
825
|
+
* Resolve the git info exclude file from a repo root.
|
|
826
|
+
*
|
|
827
|
+
* Worktrees and submodules store `.git` as a file containing a `gitdir:` pointer.
|
|
828
|
+
*
|
|
829
|
+
* @param repoRoot - Git repository working tree root.
|
|
830
|
+
* @returns Absolute path to the git info exclude file, or undefined when unavailable.
|
|
831
|
+
*/
|
|
832
|
+
async function resolveGitInfoExcludePath(repoRoot) {
|
|
833
|
+
const gitEntryPath = join(repoRoot, ".git");
|
|
834
|
+
try {
|
|
835
|
+
const stats = await stat(gitEntryPath);
|
|
836
|
+
if (stats.isDirectory()) return join(gitEntryPath, "info", "exclude");
|
|
837
|
+
if (!stats.isFile()) return;
|
|
838
|
+
const gitdir = (await readFile(gitEntryPath, "utf8")).split(/\r?\n/u).find((line) => line.startsWith("gitdir:"))?.slice(7).trim();
|
|
839
|
+
if (!gitdir) return;
|
|
840
|
+
const resolvedGitDir = isAbsolute(gitdir) ? gitdir : resolve(repoRoot, gitdir);
|
|
841
|
+
const commonDirPath = join(resolvedGitDir, "commondir");
|
|
842
|
+
try {
|
|
843
|
+
const commonDir = (await readFile(commonDirPath, "utf8")).trim();
|
|
844
|
+
if (commonDir) return join(isAbsolute(commonDir) ? commonDir : resolve(resolvedGitDir, commonDir), "info", "exclude");
|
|
845
|
+
} catch (error) {
|
|
846
|
+
const code = error.code;
|
|
847
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
|
|
848
|
+
}
|
|
849
|
+
return join(resolvedGitDir, "info", "exclude");
|
|
850
|
+
} catch (error) {
|
|
851
|
+
const code = error.code;
|
|
852
|
+
if (code === "ENOENT" || code === "ENOTDIR") return;
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
760
857
|
* Find the nearest git repo root by walking up from a start directory.
|
|
761
858
|
*
|
|
762
859
|
* @param fromDir - Directory to start from.
|
|
@@ -795,6 +892,18 @@ async function collectGitignorePaths(filepath) {
|
|
|
795
892
|
};
|
|
796
893
|
}
|
|
797
894
|
/**
|
|
895
|
+
* Validate config ignore patterns using oxfmt's path constraints.
|
|
896
|
+
*
|
|
897
|
+
* @param patterns - Config ignore patterns.
|
|
898
|
+
* @throws When a pattern contains a parent-directory path segment.
|
|
899
|
+
*/
|
|
900
|
+
function validateConfigIgnorePatterns(patterns) {
|
|
901
|
+
for (const pattern of patterns) {
|
|
902
|
+
const pathPart = pattern.startsWith("!") ? pattern.slice(1) : pattern;
|
|
903
|
+
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`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
798
907
|
* Match `ignorePatterns` from config with support for negated patterns.
|
|
799
908
|
*
|
|
800
909
|
* @param filepath - Absolute file path.
|
|
@@ -804,6 +913,7 @@ async function collectGitignorePaths(filepath) {
|
|
|
804
913
|
* @returns True when patterns mark the file as ignored.
|
|
805
914
|
*/
|
|
806
915
|
function matchConfigIgnorePatterns(filepath, configDir, patterns, useCache) {
|
|
916
|
+
validateConfigIgnorePatterns(patterns);
|
|
807
917
|
const relativeFile = relativeSafe(configDir, filepath);
|
|
808
918
|
if (relativeFile === ".." || relativeFile.startsWith("../")) return false;
|
|
809
919
|
if (!useCache) return ignore().add(patterns).ignores(relativeFile);
|
|
@@ -846,18 +956,20 @@ async function isOxfmtIgnored(options) {
|
|
|
846
956
|
};
|
|
847
957
|
const explicitIgnorePaths = (typeof options.ignorePath === "string" ? [options.ignorePath] : options.ignorePath)?.map((path) => resolveIgnoreFilePath(path, cwd));
|
|
848
958
|
const { paths: gitignorePaths, repoRoot } = await collectGitignorePaths(filepath);
|
|
849
|
-
|
|
959
|
+
const gitignoreResult = await matchIgnoreFileChain(filepath, [...gitignorePaths].reverse().map((path) => ({ path })), useCache);
|
|
960
|
+
if (gitignoreResult.ignored) return {
|
|
850
961
|
ignored: true,
|
|
851
962
|
reason: "gitignore"
|
|
852
963
|
};
|
|
853
|
-
if (repoRoot) {
|
|
854
|
-
|
|
964
|
+
if (repoRoot && !gitignoreResult.matched) {
|
|
965
|
+
const infoExcludePath = await resolveGitInfoExcludePath(repoRoot);
|
|
966
|
+
if (infoExcludePath && await matchIgnoreFile(filepath, infoExcludePath, useCache, repoRoot)) return {
|
|
855
967
|
ignored: true,
|
|
856
968
|
reason: "git-info-exclude"
|
|
857
969
|
};
|
|
858
970
|
}
|
|
859
971
|
if (explicitIgnorePaths && explicitIgnorePaths.length > 0) {
|
|
860
|
-
if (await matchIgnoreFileChain(filepath, explicitIgnorePaths.map((path) => ({ path })), useCache)) return {
|
|
972
|
+
if ((await matchIgnoreFileChain(filepath, explicitIgnorePaths.map((path) => ({ path })), useCache)).ignored) return {
|
|
861
973
|
ignored: true,
|
|
862
974
|
reason: "ignore-path"
|
|
863
975
|
};
|
|
@@ -881,4 +993,4 @@ async function isOxfmtIgnored(options) {
|
|
|
881
993
|
return { ignored: false };
|
|
882
994
|
}
|
|
883
995
|
//#endregion
|
|
884
|
-
export { isOxfmtIgnored, loadOxfmtConfig, resolveOxfmtrcPath };
|
|
996
|
+
export { getConfigCacheKey, getResolveCacheKey, isOxfmtIgnored, loadOxfmtConfig, readConfigFromFile, resolveOxfmtrcPath };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "load-oxfmt-config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
25
|
-
"url": "ntnyq/load-oxfmt-config"
|
|
25
|
+
"url": "git+https://github.com/ntnyq/load-oxfmt-config.git"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"dist"
|
|
@@ -42,32 +42,37 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@ntnyq/utils": "^0.16.
|
|
45
|
+
"@ntnyq/utils": "^0.16.1",
|
|
46
46
|
"editorconfig": "^3.0.2",
|
|
47
|
-
"ignore": "^7.0.
|
|
47
|
+
"ignore": "^7.0.6",
|
|
48
48
|
"jiti": "^2.7.0",
|
|
49
49
|
"jsonc-parser": "^3.3.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@ntnyq/tsconfig": "^3.1.0",
|
|
53
|
-
"@types/node": "^26.
|
|
54
|
-
"@typescript/native-preview": "^7.0.0-dev.
|
|
53
|
+
"@types/node": "^26.1.1",
|
|
54
|
+
"@typescript/native-preview": "^7.0.0-dev.20260707.2",
|
|
55
55
|
"bumpp": "^11.1.0",
|
|
56
|
+
"changelogithub": "14.0.0",
|
|
56
57
|
"husky": "^9.1.7",
|
|
57
58
|
"nano-staged": "^1.0.2",
|
|
59
|
+
"npm": "11.18.0",
|
|
58
60
|
"npm-run-all2": "^9.0.2",
|
|
59
|
-
"oxfmt": "^0.
|
|
60
|
-
"oxlint": "^1.
|
|
61
|
-
"tsdown": "^0.22.
|
|
62
|
-
"vitest": "^4.1.
|
|
61
|
+
"oxfmt": "^0.59.0",
|
|
62
|
+
"oxlint": "^1.74.0",
|
|
63
|
+
"tsdown": "^0.22.8",
|
|
64
|
+
"vitest": "^4.1.10"
|
|
63
65
|
},
|
|
64
66
|
"peerDependencies": {
|
|
65
|
-
"oxfmt": ">=0.
|
|
67
|
+
"oxfmt": ">=0.59.0"
|
|
66
68
|
},
|
|
67
69
|
"nano-staged": {
|
|
68
70
|
"*.{js,ts,mjs,tsx}": "oxlint --fix",
|
|
69
71
|
"*": "oxfmt --no-error-on-unmatched-pattern"
|
|
70
72
|
},
|
|
73
|
+
"engines": {
|
|
74
|
+
"node": "^22.13.0 || >=24"
|
|
75
|
+
},
|
|
71
76
|
"scripts": {
|
|
72
77
|
"build": "tsdown",
|
|
73
78
|
"dev": "tsdown --watch",
|