load-oxfmt-config 0.18.0 → 0.19.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 +2 -1
- package/dist/index.mjs +103 -35
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -516,8 +516,9 @@ The caching system maintains two separate caches:
|
|
|
516
516
|
|
|
517
517
|
- Failed operations automatically clear their cache entries
|
|
518
518
|
- Use `useCache: false` to bypass cache for specific calls
|
|
519
|
+
- Each process-level cache retains at most 1,000 least-recently used entries
|
|
519
520
|
- 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.
|
|
520
|
-
- Cache
|
|
521
|
+
- Cache entries persist until least-recently used eviction or process exit
|
|
521
522
|
|
|
522
523
|
## Related
|
|
523
524
|
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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 { hasOwn, interopDefault, isBoolean, isNumber, isObject,
|
|
5
|
-
import {
|
|
4
|
+
import { hasOwn, interopDefault, isBoolean, isNumber, isObject, isString, isUndefined } from "@ntnyq/utils";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
6
7
|
import { pathToFileURL } from "node:url";
|
|
7
8
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
8
9
|
import { parseBuffer } from "editorconfig";
|
|
@@ -78,6 +79,34 @@ const EDITORCONFIG_GLOBAL_SECTION_NAMES = ["*"];
|
|
|
78
79
|
//#endregion
|
|
79
80
|
//#region src/utils.ts
|
|
80
81
|
/**
|
|
82
|
+
* Read a cache value and move it to the most-recent position.
|
|
83
|
+
*
|
|
84
|
+
* @param cache - Cache map to read.
|
|
85
|
+
* @param key - Cache key.
|
|
86
|
+
* @returns Cached value, or undefined when missing.
|
|
87
|
+
*/
|
|
88
|
+
function getCacheValue(cache, key) {
|
|
89
|
+
const value = cache.get(key);
|
|
90
|
+
if (value === void 0) return;
|
|
91
|
+
cache.delete(key);
|
|
92
|
+
cache.set(key, value);
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Store a cache value and evict the least-recent entry when over capacity.
|
|
97
|
+
*
|
|
98
|
+
* @param cache - Cache map to update.
|
|
99
|
+
* @param key - Cache key.
|
|
100
|
+
* @param value - Value to cache.
|
|
101
|
+
*/
|
|
102
|
+
function setCacheValue(cache, key, value) {
|
|
103
|
+
cache.delete(key);
|
|
104
|
+
cache.set(key, value);
|
|
105
|
+
if (cache.size <= 1e3) return;
|
|
106
|
+
const oldestKey = cache.keys().next();
|
|
107
|
+
if (!oldestKey.done) cache.delete(oldestKey.value);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
81
110
|
* Return a cached promise by key, creating and storing it on miss.
|
|
82
111
|
*
|
|
83
112
|
* If the promise rejects, the cache entry is removed so future calls can retry.
|
|
@@ -88,13 +117,13 @@ const EDITORCONFIG_GLOBAL_SECTION_NAMES = ["*"];
|
|
|
88
117
|
* @returns Cached or newly created promise.
|
|
89
118
|
*/
|
|
90
119
|
function cachePromise(cache, key, factory) {
|
|
91
|
-
const cached = cache
|
|
120
|
+
const cached = getCacheValue(cache, key);
|
|
92
121
|
if (cached) return cached;
|
|
93
122
|
const task = factory().catch((error) => {
|
|
94
|
-
cache.delete(key);
|
|
123
|
+
if (cache.get(key) === task) cache.delete(key);
|
|
95
124
|
throw error;
|
|
96
125
|
});
|
|
97
|
-
cache
|
|
126
|
+
setCacheValue(cache, key, task);
|
|
98
127
|
return task;
|
|
99
128
|
}
|
|
100
129
|
/**
|
|
@@ -115,6 +144,19 @@ function toPosixPath(path) {
|
|
|
115
144
|
function splitPathSegments(path) {
|
|
116
145
|
return path.split(/[\\/]+/u).filter(Boolean);
|
|
117
146
|
}
|
|
147
|
+
/**
|
|
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.
|
|
152
|
+
*
|
|
153
|
+
* @param path - Relative path candidate.
|
|
154
|
+
* @returns True when the path escapes or is absolute.
|
|
155
|
+
*/
|
|
156
|
+
function isPathOutsideBase(path) {
|
|
157
|
+
const normalizedPath = toPosixPath(path);
|
|
158
|
+
return normalizedPath === ".." || normalizedPath.startsWith("../") || normalizedPath.startsWith("/") || /^[A-Za-z]:\//u.test(normalizedPath);
|
|
159
|
+
}
|
|
118
160
|
//#endregion
|
|
119
161
|
//#region src/config.ts
|
|
120
162
|
/**
|
|
@@ -149,7 +191,9 @@ const freshJavaScriptModuleCache = /* @__PURE__ */ new Map();
|
|
|
149
191
|
* @returns True when the value can be treated as an oxfmt options object.
|
|
150
192
|
*/
|
|
151
193
|
function isConfigObject(value) {
|
|
152
|
-
|
|
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;
|
|
153
197
|
}
|
|
154
198
|
/**
|
|
155
199
|
* Read a parsed JSON-like config value as an oxfmt config object.
|
|
@@ -200,14 +244,14 @@ async function importJitiConfigModule(resolvedPath, useCache) {
|
|
|
200
244
|
}).import(resolvedPath);
|
|
201
245
|
}
|
|
202
246
|
/**
|
|
203
|
-
* Build a cache-busting import key from entry file
|
|
247
|
+
* Build a cache-busting import key from entry file contents.
|
|
204
248
|
*
|
|
205
249
|
* @param resolvedPath - Absolute config file path.
|
|
206
|
-
* @returns Stable key that changes when the entry file
|
|
250
|
+
* @returns Stable key that changes when the entry file contents change.
|
|
207
251
|
*/
|
|
208
252
|
async function getFreshImportCacheKey(resolvedPath) {
|
|
209
|
-
const
|
|
210
|
-
return
|
|
253
|
+
const content = await readFile(resolvedPath);
|
|
254
|
+
return createHash("sha256").update(content).digest("hex");
|
|
211
255
|
}
|
|
212
256
|
/**
|
|
213
257
|
* Read a Node-style `code` field from an unknown thrown value.
|
|
@@ -222,12 +266,13 @@ function getErrorCode(error) {
|
|
|
222
266
|
* Check whether a loader error should continue the JS config fallback chain.
|
|
223
267
|
*
|
|
224
268
|
* @param error - Thrown value from require or native import.
|
|
269
|
+
* @param resolvedPaths - Entry module paths that may appear in parser errors.
|
|
225
270
|
* @returns True when the error is from loader compatibility, not config execution.
|
|
226
271
|
*/
|
|
227
|
-
function isJavaScriptLoaderFallbackError(error) {
|
|
272
|
+
function isJavaScriptLoaderFallbackError(error, resolvedPaths) {
|
|
228
273
|
const code = getErrorCode(error);
|
|
229
274
|
if (code && requireFallbackErrorCodes.has(code)) return true;
|
|
230
|
-
return error instanceof SyntaxError && syntaxFallbackMessages.some((pattern) => pattern.test(error.message));
|
|
275
|
+
return error instanceof SyntaxError && resolvedPaths.some((resolvedPath) => error.stack?.startsWith(`${resolvedPath}:`) === true) && syntaxFallbackMessages.some((pattern) => pattern.test(error.message));
|
|
231
276
|
}
|
|
232
277
|
/**
|
|
233
278
|
* Import an ESM config module with entry-file cache busting.
|
|
@@ -287,12 +332,14 @@ function requireFreshCommonJSConfig(resolvedPath) {
|
|
|
287
332
|
* @returns Imported module namespace with a default config export.
|
|
288
333
|
*/
|
|
289
334
|
async function importFreshJavaScriptConfigModule(resolvedPath) {
|
|
335
|
+
const canonicalPath = await realpath(resolvedPath);
|
|
336
|
+
const loaderPaths = canonicalPath === resolvedPath ? [resolvedPath] : [resolvedPath, canonicalPath];
|
|
290
337
|
const cacheKey = await getFreshImportCacheKey(resolvedPath);
|
|
291
|
-
const cachedModule = freshJavaScriptModuleCache
|
|
338
|
+
const cachedModule = getCacheValue(freshJavaScriptModuleCache, resolvedPath);
|
|
292
339
|
if (cachedModule?.cacheKey === cacheKey) return cachedModule.namespace;
|
|
293
340
|
if (cachedModule) {
|
|
294
341
|
const namespace = await importNativeFreshModule(resolvedPath);
|
|
295
|
-
freshJavaScriptModuleCache
|
|
342
|
+
setCacheValue(freshJavaScriptModuleCache, resolvedPath, {
|
|
296
343
|
cacheKey,
|
|
297
344
|
namespace
|
|
298
345
|
});
|
|
@@ -302,7 +349,7 @@ async function importFreshJavaScriptConfigModule(resolvedPath) {
|
|
|
302
349
|
const configModule = requireFreshModule(resolvedPath);
|
|
303
350
|
if (Object.prototype.toString.call(configModule) === "[object Module]") {
|
|
304
351
|
const namespace = configModule;
|
|
305
|
-
freshJavaScriptModuleCache
|
|
352
|
+
setCacheValue(freshJavaScriptModuleCache, resolvedPath, {
|
|
306
353
|
cacheKey,
|
|
307
354
|
namespace
|
|
308
355
|
});
|
|
@@ -310,16 +357,16 @@ async function importFreshJavaScriptConfigModule(resolvedPath) {
|
|
|
310
357
|
}
|
|
311
358
|
return { default: readConfigModuleExports(configModule) };
|
|
312
359
|
} catch (error) {
|
|
313
|
-
if (!isJavaScriptLoaderFallbackError(error)) throw error;
|
|
360
|
+
if (!isJavaScriptLoaderFallbackError(error, loaderPaths)) throw error;
|
|
314
361
|
try {
|
|
315
362
|
const namespace = await importNativeFreshModule(resolvedPath);
|
|
316
|
-
freshJavaScriptModuleCache
|
|
363
|
+
setCacheValue(freshJavaScriptModuleCache, resolvedPath, {
|
|
317
364
|
cacheKey,
|
|
318
365
|
namespace
|
|
319
366
|
});
|
|
320
367
|
return namespace;
|
|
321
368
|
} catch (importError) {
|
|
322
|
-
if (!isJavaScriptLoaderFallbackError(importError)) throw importError;
|
|
369
|
+
if (!isJavaScriptLoaderFallbackError(importError, loaderPaths)) throw importError;
|
|
323
370
|
return importJitiConfigModule(resolvedPath, false);
|
|
324
371
|
}
|
|
325
372
|
}
|
|
@@ -762,26 +809,25 @@ async function matchIgnoreFile(filepath, ignoreFilePath, useCache, baseDir) {
|
|
|
762
809
|
const matcher = await loadIgnoreMatcher(ignoreFilePath, useCache);
|
|
763
810
|
if (!matcher) return false;
|
|
764
811
|
const relativeToIgnore = relativeSafe(baseDir ?? dirname(ignoreFilePath), filepath);
|
|
765
|
-
if (relativeToIgnore
|
|
812
|
+
if (isPathOutsideBase(relativeToIgnore)) return false;
|
|
766
813
|
return matcher.ignores(relativeToIgnore);
|
|
767
814
|
}
|
|
768
815
|
/**
|
|
769
|
-
* Match a
|
|
816
|
+
* Match a path against loaded ignore matchers in precedence order.
|
|
770
817
|
*
|
|
771
|
-
* @param filepath - Absolute file path.
|
|
772
|
-
* @param
|
|
773
|
-
* @param
|
|
774
|
-
* @returns
|
|
818
|
+
* @param filepath - Absolute file or directory path to test.
|
|
819
|
+
* @param matcherEntries - Loaded matchers in increasing precedence order.
|
|
820
|
+
* @param isDirectory - Whether the target path is a directory.
|
|
821
|
+
* @returns Final ignore state after all matching rules.
|
|
775
822
|
*/
|
|
776
|
-
|
|
823
|
+
function matchLoadedIgnoreMatchers(filepath, matcherEntries, isDirectory = false) {
|
|
777
824
|
let ignored = false;
|
|
778
825
|
let matched = false;
|
|
779
|
-
for (const entry of
|
|
780
|
-
|
|
781
|
-
if (
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const result = matcher.test(relativeToIgnore);
|
|
826
|
+
for (const entry of matcherEntries) {
|
|
827
|
+
let relativeToIgnore = relativeSafe(entry.baseDir, filepath);
|
|
828
|
+
if (isPathOutsideBase(relativeToIgnore)) continue;
|
|
829
|
+
if (isDirectory && relativeToIgnore && !relativeToIgnore.endsWith("/")) relativeToIgnore += "/";
|
|
830
|
+
const result = entry.matcher.test(relativeToIgnore);
|
|
785
831
|
if (result.ignored) {
|
|
786
832
|
ignored = true;
|
|
787
833
|
matched = true;
|
|
@@ -796,6 +842,28 @@ async function matchIgnoreFileChain(filepath, ignoreFileEntries, useCache) {
|
|
|
796
842
|
};
|
|
797
843
|
}
|
|
798
844
|
/**
|
|
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
|
+
/**
|
|
799
867
|
* Resolve an ignore file path against cwd when needed.
|
|
800
868
|
*
|
|
801
869
|
* @param path - Absolute or relative path.
|
|
@@ -920,13 +988,13 @@ function validateConfigIgnorePatterns(patterns) {
|
|
|
920
988
|
function matchConfigIgnorePatterns(filepath, configDir, patterns, useCache) {
|
|
921
989
|
validateConfigIgnorePatterns(patterns);
|
|
922
990
|
const relativeFile = relativeSafe(configDir, filepath);
|
|
923
|
-
if (relativeFile
|
|
991
|
+
if (isPathOutsideBase(relativeFile)) return false;
|
|
924
992
|
if (!useCache) return ignore().add(patterns).ignores(relativeFile);
|
|
925
993
|
const cacheKey = `${configDir}::${JSON.stringify(patterns)}`;
|
|
926
|
-
const cachedMatcher = configIgnoreMatcherCache
|
|
994
|
+
const cachedMatcher = getCacheValue(configIgnoreMatcherCache, cacheKey);
|
|
927
995
|
if (cachedMatcher) return cachedMatcher.ignores(relativeFile);
|
|
928
996
|
const matcher = ignore().add(patterns);
|
|
929
|
-
configIgnoreMatcherCache
|
|
997
|
+
setCacheValue(configIgnoreMatcherCache, cacheKey, matcher);
|
|
930
998
|
return matcher.ignores(relativeFile);
|
|
931
999
|
}
|
|
932
1000
|
/**
|
|
@@ -961,7 +1029,7 @@ async function isOxfmtIgnored(options) {
|
|
|
961
1029
|
};
|
|
962
1030
|
const explicitIgnorePaths = (typeof options.ignorePath === "string" ? [options.ignorePath] : options.ignorePath)?.map((path) => resolveIgnoreFilePath(path, cwd));
|
|
963
1031
|
const { paths: gitignorePaths, repoRoot } = await collectGitignorePaths(filepath);
|
|
964
|
-
const gitignoreResult = await matchIgnoreFileChain(filepath, [...gitignorePaths].reverse().map((path) => ({ path })), useCache);
|
|
1032
|
+
const gitignoreResult = await matchIgnoreFileChain(filepath, [...gitignorePaths].reverse().map((path) => ({ path })), useCache, true);
|
|
965
1033
|
if (gitignoreResult.ignored) return {
|
|
966
1034
|
ignored: true,
|
|
967
1035
|
reason: "gitignore"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "load-oxfmt-config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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,7 +42,7 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@ntnyq/utils": "^0.21.
|
|
45
|
+
"@ntnyq/utils": "^0.21.1",
|
|
46
46
|
"editorconfig": "^3.0.2",
|
|
47
47
|
"ignore": "^7.0.6",
|
|
48
48
|
"jiti": "^2.7.0",
|
|
@@ -50,21 +50,21 @@
|
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@ntnyq/tsconfig": "^3.1.0",
|
|
53
|
-
"@types/node": "^26.
|
|
53
|
+
"@types/node": "^26.2.0",
|
|
54
54
|
"@typescript/native-preview": "^7.0.0-dev.20260707.2",
|
|
55
|
-
"bumpp": "^12.
|
|
55
|
+
"bumpp": "^12.2.0",
|
|
56
56
|
"changelogithub": "14.0.0",
|
|
57
57
|
"husky": "^9.1.7",
|
|
58
58
|
"nano-staged": "^1.0.2",
|
|
59
59
|
"npm": "11.18.0",
|
|
60
60
|
"npm-run-all2": "^9.0.3",
|
|
61
|
-
"oxfmt": "^0.
|
|
62
|
-
"oxlint": "^1.
|
|
61
|
+
"oxfmt": "^0.63.0",
|
|
62
|
+
"oxlint": "^1.78.0",
|
|
63
63
|
"tsdown": "^0.22.14",
|
|
64
64
|
"vitest": "^4.1.10"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"oxfmt": ">=0.
|
|
67
|
+
"oxfmt": ">=0.63.0"
|
|
68
68
|
},
|
|
69
69
|
"nano-staged": {
|
|
70
70
|
"*.{js,ts,mjs,tsx}": "oxlint --fix",
|