@markuplint/file-resolver 5.3.0 → 5.5.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/CHANGELOG.md +24 -0
- package/lib/config-provider.d.ts +26 -4
- package/lib/config-provider.js +155 -54
- package/lib/cosmiconfig.d.ts +49 -0
- package/lib/cosmiconfig.js +455 -32
- package/lib/general-import.d.ts +18 -0
- package/lib/general-import.js +31 -49
- package/lib/import-module.d.ts +28 -0
- package/lib/import-module.js +64 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/ml-file/ml-file.d.ts +4 -0
- package/lib/ml-file/ml-file.js +4 -0
- package/lib/module-exists.d.ts +22 -0
- package/lib/module-exists.js +28 -10
- package/lib/path-to-abs-or-name.d.ts +18 -0
- package/lib/path-to-abs-or-name.js +18 -0
- package/lib/path-utils.d.ts +9 -0
- package/lib/path-utils.js +11 -0
- package/lib/resolve-name-or-abs-path.d.ts +18 -0
- package/lib/resolve-name-or-abs-path.js +24 -5
- package/lib/resolve-pretenders.d.ts +12 -2
- package/lib/resolve-pretenders.js +73 -13
- package/package.json +22 -14
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [5.5.0](https://github.com/markuplint/markuplint/compare/v5.4.0...v5.5.0) (2026-11-10)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **file-resolver:** do not report a config shared by two extends paths as circular ([8594a37](https://github.com/markuplint/markuplint/commit/8594a373feda1a2f869702b6b16d619d156814c4)), closes [#4552](https://github.com/markuplint/markuplint/issues/4552)
|
|
11
|
+
* **file-resolver:** do not swallow errors other than InvalidSelectorError in validation ([821a661](https://github.com/markuplint/markuplint/commit/821a66170e84537c57d826133bdf486c6276f524))
|
|
12
|
+
* **file-resolver:** load @markuplint/pretenders only when a configuration needs it ([32ee579](https://github.com/markuplint/markuplint/commit/32ee579e4b43cf18ba9787f173796c44966c3b40))
|
|
13
|
+
* **file-resolver:** read the report of a JSON module before the code of a missing one ([95e4869](https://github.com/markuplint/markuplint/commit/95e4869c9be3bd53f853e5a99830514bfec5a51b))
|
|
14
|
+
* **file-resolver:** resolve a value spelled as a path from the configuration's directory ([3fbfeca](https://github.com/markuplint/markuplint/commit/3fbfeca83b29ba852709c50667d60ca28f1dfce6)), closes [#4459](https://github.com/markuplint/markuplint/issues/4459)
|
|
15
|
+
* **file-resolver:** take a JSON module through require() when import() refuses it ([15303ba](https://github.com/markuplint/markuplint/commit/15303baae4e56ac26d1e3eb4522230bae1d93efe))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# [5.4.0](https://github.com/markuplint/markuplint/compare/v5.3.0...v5.4.0) (2026-10-29)
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **file-resolver:** do not run a throwing config file again through require() ([21b2e87](https://github.com/markuplint/markuplint/commit/21b2e87d836baa3e0747dee623845ddfc077c091)), closes [#4413](https://github.com/markuplint/markuplint/issues/4413)
|
|
23
|
+
* **file-resolver:** find a Windows path written with forward slashes in the stack ([ea7336c](https://github.com/markuplint/markuplint/commit/ea7336c839365d67a3b0d6427d8d1c2b864769f1)), closes [#4413](https://github.com/markuplint/markuplint/issues/4413)
|
|
24
|
+
* **file-resolver:** find the configuration above a nested package.json ([de7c887](https://github.com/markuplint/markuplint/commit/de7c887aebc5ce59bfdad08802ae23554abbda27)), closes [#4405](https://github.com/markuplint/markuplint/issues/4405)
|
|
25
|
+
* **file-resolver:** report a config file that fails to load by its own path ([0fafeca](https://github.com/markuplint/markuplint/commit/0fafeca63d28164bcbbd7fecdaeefcb593a2ae7f)), closes [#4413](https://github.com/markuplint/markuplint/issues/4413)
|
|
26
|
+
* **file-resolver:** resolve the paths of a configuration from the file that declares it ([b819519](https://github.com/markuplint/markuplint/commit/b819519236e3640b65965376ba2e78ea336e9593)), closes [#4406](https://github.com/markuplint/markuplint/issues/4406)
|
|
27
|
+
* **file-resolver:** search the ancestors of a target whose directory does not exist ([75bdf1c](https://github.com/markuplint/markuplint/commit/75bdf1c7affce5eab88c091ca6a031c89ae29840)), closes [#4412](https://github.com/markuplint/markuplint/issues/4412)
|
|
28
|
+
|
|
29
|
+
|
|
6
30
|
# [5.3.0](https://github.com/markuplint/markuplint/compare/v5.2.0...v5.3.0) (2026-10-21)
|
|
7
31
|
|
|
8
32
|
### Bug Fixes
|
package/lib/config-provider.d.ts
CHANGED
|
@@ -59,13 +59,27 @@ export declare class ConfigProvider {
|
|
|
59
59
|
/**
|
|
60
60
|
* Recursively loads a configuration and all its `extends` dependencies.
|
|
61
61
|
*
|
|
62
|
+
* A `CircularReferenceError` is reported only for a config that extends itself, directly or
|
|
63
|
+
* indirectly. A config reached through more than one `extends` path is loaded once and is not
|
|
64
|
+
* an error.
|
|
65
|
+
*
|
|
62
66
|
* @param key - The config file path or module name to load
|
|
63
67
|
* @param cache - Whether to use cached results
|
|
64
68
|
* @param referrer - The file path of the config that referenced this key
|
|
65
|
-
* @param depth - Current recursion depth (for circular reference detection)
|
|
66
69
|
* @returns A set of loaded config keys and any errors encountered
|
|
67
70
|
*/
|
|
68
|
-
recursiveLoad(key: string, cache: boolean, referrer: string
|
|
71
|
+
recursiveLoad(key: string, cache: boolean, referrer: string): Promise<{
|
|
72
|
+
stack: Set<string>;
|
|
73
|
+
errs: Error[];
|
|
74
|
+
}>;
|
|
75
|
+
/**
|
|
76
|
+
* @deprecated Remove the `depth` argument: it is ignored, and the overload will be removed in v6.
|
|
77
|
+
* @param key - The config file path or module name to load
|
|
78
|
+
* @param cache - Whether to use cached results
|
|
79
|
+
* @param referrer - The file path of the config that referenced this key
|
|
80
|
+
* @param depth - Ignored
|
|
81
|
+
*/
|
|
82
|
+
recursiveLoad(key: string, cache: boolean, referrer: string, depth: number): Promise<{
|
|
69
83
|
stack: Set<string>;
|
|
70
84
|
errs: Error[];
|
|
71
85
|
}>;
|
|
@@ -92,13 +106,21 @@ export declare class ConfigProvider {
|
|
|
92
106
|
* right before calling this) — see #4015.
|
|
93
107
|
*
|
|
94
108
|
* @param targetFile - The file being linted
|
|
95
|
-
* @param names - Config file paths or module names to merge
|
|
109
|
+
* @param names - Config file paths or module names to merge. A key of a configuration already
|
|
110
|
+
* stored (by {@link set} or {@link search}) is taken as it is; any other name is resolved as a
|
|
111
|
+
* value of `extends` is, from the working directory: a module name stays, a path becomes
|
|
112
|
+
* absolute
|
|
96
113
|
* @param cache - Whether to reuse already-loaded/cached entries
|
|
97
114
|
* @returns The fully resolved configuration set including plugins and errors
|
|
98
115
|
*/
|
|
99
116
|
resolve(targetFile: Readonly<MLFile>, names: readonly Nullable<string>[], cache?: boolean): Promise<ConfigSet>;
|
|
100
117
|
/**
|
|
101
|
-
* Searches for a markuplint configuration file starting from the target file's directory
|
|
118
|
+
* Searches for a markuplint configuration file starting from the target file's directory
|
|
119
|
+
* and going up through its ancestors; the nearest one wins. A `package.json` without a
|
|
120
|
+
* `markuplint` field is not a configuration file, so the search passes over it. A target
|
|
121
|
+
* directory that does not exist yet, such as that of an unsaved editor buffer in a new folder,
|
|
122
|
+
* does not end the search either: the search starts from that directory and goes on through
|
|
123
|
+
* its ancestors, the missing ones yielding nothing.
|
|
102
124
|
*
|
|
103
125
|
* @param targetFile - The file whose directory to search from
|
|
104
126
|
* @returns The file path of the found config, or `null` if none was found
|
package/lib/config-provider.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { mergeConfig } from '@markuplint/ml-config';
|
|
2
|
+
import { mergeConfig, rebaseHandWrittenPretenderFilePath } from '@markuplint/ml-config';
|
|
3
3
|
import { ConfigParserError } from '@markuplint/parser-utils';
|
|
4
4
|
import { InvalidSelectorError, createSelector } from '@markuplint/selector';
|
|
5
|
-
import { nonNullableFilter, toNoEmptyStringArrayFromStringOrArray, ConfigLoadError } from '@markuplint/shared';
|
|
5
|
+
import { nonNullableFilter, toNoEmptyStringArrayFromStringOrArray, ConfigLoadError, isFatalError, } from '@markuplint/shared';
|
|
6
6
|
import { clearAutoLoadRulesCache } from './auto-load-rules.js';
|
|
7
|
-
import { load as loadConfig, search, clearExplorerCache } from './cosmiconfig.js';
|
|
7
|
+
import { load as loadConfig, search, clearExplorerCache, configLoadErrorFiles } from './cosmiconfig.js';
|
|
8
8
|
import { log } from './debug.js';
|
|
9
9
|
import { generalImport } from './general-import.js';
|
|
10
10
|
import { getPreset } from './get-preset.js';
|
|
@@ -12,6 +12,7 @@ import { isPluginModuleName } from './is-plugin-module-name.js';
|
|
|
12
12
|
import { isPresetModuleName } from './is-preset-module-name.js';
|
|
13
13
|
import { moduleExists } from './module-exists.js';
|
|
14
14
|
import { relPathToNameOrAbsPath } from './path-to-abs-or-name.js';
|
|
15
|
+
import { resolveNameOrAbsPath } from './resolve-name-or-abs-path.js';
|
|
15
16
|
import { cacheClear, resolvePlugins } from './resolve-plugins.js';
|
|
16
17
|
import { fileExists, uuid } from './utils.js';
|
|
17
18
|
const cpLog = log.extend('config-provider');
|
|
@@ -32,7 +33,6 @@ const KEY_SEPARATOR = '__ML_CONFIG_MERGE__';
|
|
|
32
33
|
export class ConfigProvider {
|
|
33
34
|
#cache = new Map();
|
|
34
35
|
#held = new Set();
|
|
35
|
-
#recursiveLoadKeyAndDepth = new Map();
|
|
36
36
|
#store = new Map();
|
|
37
37
|
/**
|
|
38
38
|
* Stabilizes {@link set}'s auto-generated key for an inline config object
|
|
@@ -108,26 +108,34 @@ export class ConfigProvider {
|
|
|
108
108
|
release();
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
+
async recursiveLoad(key, cache, referrer, depth) {
|
|
112
|
+
return await this.#recursiveLoad(key, cache, referrer, new Set(), new Set());
|
|
113
|
+
}
|
|
111
114
|
/**
|
|
112
|
-
*
|
|
115
|
+
* A config is circular only when it is its own ancestor on the current `extends` path
|
|
116
|
+
* (`ancestors`). Reaching the same config through two different paths (a diamond, e.g. two
|
|
117
|
+
* presets that extend the same sub-presets) is not a cycle, however deep each path is, so
|
|
118
|
+
* comparing depths cannot tell the two apart (#4552).
|
|
113
119
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
120
|
+
* `visited` holds every key already walked in this top-level call. A key met again is skipped:
|
|
121
|
+
* its keys are already in the result and its errors, circular ones included, were already
|
|
122
|
+
* reported once. Without it the walk repeats once per path, which grows exponentially with
|
|
123
|
+
* stacked diamonds, and reports the same error once per path. State lives in the arguments,
|
|
124
|
+
* not on the instance, so concurrent calls do not see each other.
|
|
119
125
|
*/
|
|
120
|
-
async recursiveLoad(key, cache, referrer,
|
|
126
|
+
async #recursiveLoad(key, cache, referrer, ancestors, visited) {
|
|
121
127
|
const stack = new Set();
|
|
122
128
|
const errs = [];
|
|
123
|
-
|
|
124
|
-
if (ancestorDepth != null && ancestorDepth < depth) {
|
|
129
|
+
if (ancestors.has(key)) {
|
|
125
130
|
return {
|
|
126
131
|
stack,
|
|
127
132
|
errs: [new CircularReferenceError(`Circular reference detected: ${key}`)],
|
|
128
133
|
};
|
|
129
134
|
}
|
|
130
|
-
|
|
135
|
+
if (visited.has(key)) {
|
|
136
|
+
return { stack, errs };
|
|
137
|
+
}
|
|
138
|
+
visited.add(key);
|
|
131
139
|
let config = this.#store.get(key);
|
|
132
140
|
if (!config) {
|
|
133
141
|
config = await this.#load(key, cache, referrer);
|
|
@@ -136,7 +144,14 @@ export class ConfigProvider {
|
|
|
136
144
|
return { stack, errs: [] };
|
|
137
145
|
}
|
|
138
146
|
if (config instanceof ConfigLoadError) {
|
|
139
|
-
|
|
147
|
+
// The files whose change can fix the error are recorded as well as `key`, as they can be
|
|
148
|
+
// ones that `key` imports through `$import`, or a target of it that does not exist yet.
|
|
149
|
+
// `key` is added last, so that it stays the file `#resolveBase` validates against, as when
|
|
150
|
+
// the load succeeds.
|
|
151
|
+
for (const filePath of configLoadErrorFiles(config)) {
|
|
152
|
+
stack.add(filePath);
|
|
153
|
+
}
|
|
154
|
+
stack.add(key);
|
|
140
155
|
return {
|
|
141
156
|
stack,
|
|
142
157
|
errs: [config],
|
|
@@ -144,12 +159,18 @@ export class ConfigProvider {
|
|
|
144
159
|
}
|
|
145
160
|
const depKeys = config.extends === null ? null : toNoEmptyStringArrayFromStringOrArray(config.extends);
|
|
146
161
|
if (depKeys) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
for (const
|
|
150
|
-
|
|
162
|
+
ancestors.add(key);
|
|
163
|
+
try {
|
|
164
|
+
for (const depKey of depKeys) {
|
|
165
|
+
const keys = await this.#recursiveLoad(depKey, cache, key, ancestors, visited);
|
|
166
|
+
for (const loadedKey of keys.stack) {
|
|
167
|
+
stack.add(loadedKey);
|
|
168
|
+
}
|
|
169
|
+
errs.push(...keys.errs);
|
|
151
170
|
}
|
|
152
|
-
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
ancestors.delete(key);
|
|
153
174
|
}
|
|
154
175
|
}
|
|
155
176
|
stack.add(key);
|
|
@@ -178,19 +199,24 @@ export class ConfigProvider {
|
|
|
178
199
|
* right before calling this) — see #4015.
|
|
179
200
|
*
|
|
180
201
|
* @param targetFile - The file being linted
|
|
181
|
-
* @param names - Config file paths or module names to merge
|
|
202
|
+
* @param names - Config file paths or module names to merge. A key of a configuration already
|
|
203
|
+
* stored (by {@link set} or {@link search}) is taken as it is; any other name is resolved as a
|
|
204
|
+
* value of `extends` is, from the working directory: a module name stays, a path becomes
|
|
205
|
+
* absolute
|
|
182
206
|
* @param cache - Whether to reuse already-loaded/cached entries
|
|
183
207
|
* @returns The fully resolved configuration set including plugins and errors
|
|
184
208
|
*/
|
|
185
209
|
async resolve(targetFile, names, cache = true) {
|
|
186
|
-
const keys =
|
|
210
|
+
const keys = await Promise.all(names
|
|
211
|
+
.filter(nonNullableFilter)
|
|
212
|
+
.map(name => (this.#store.has(name) ? name : resolveNameOrAbsPath(process.cwd(), name))));
|
|
187
213
|
const key = keys.join(KEY_SEPARATOR);
|
|
188
214
|
let baseConfigSet = this.#cache.get(key);
|
|
189
215
|
if (!baseConfigSet) {
|
|
190
216
|
baseConfigSet = await this.#resolveBase(keys, cache, targetFile.path);
|
|
191
217
|
this.#cache.set(key, baseConfigSet);
|
|
192
218
|
}
|
|
193
|
-
return this.#applyOverrides(baseConfigSet, targetFile);
|
|
219
|
+
return await this.#applyOverrides(baseConfigSet, targetFile);
|
|
194
220
|
}
|
|
195
221
|
/**
|
|
196
222
|
* The `names`-dependent, `targetFile`-independent part of {@link resolve}:
|
|
@@ -245,8 +271,13 @@ export class ConfigProvider {
|
|
|
245
271
|
* replaces every earlier match, not just the base config. `appliedOverrides`
|
|
246
272
|
* records that match order for `--show-config=details` to surface, since this
|
|
247
273
|
* "last match wins outright" behavior is easy to mistake for a bug (see #4023).
|
|
274
|
+
*
|
|
275
|
+
* The plugins an override declares are loaded here, as `#resolveBase` cannot know which
|
|
276
|
+
* overrides apply. The result keeps every plugin of the base, replaced where the override
|
|
277
|
+
* declares the same one: a `reset` override that declares none still reaches the rules of
|
|
278
|
+
* the plugins the base loaded, as it did before the override's own were loaded at all.
|
|
248
279
|
*/
|
|
249
|
-
#applyOverrides(baseConfigSet, targetFile) {
|
|
280
|
+
async #applyOverrides(baseConfigSet, targetFile) {
|
|
250
281
|
let config = baseConfigSet.config;
|
|
251
282
|
const appliedOverrides = [];
|
|
252
283
|
if (config.overrides) {
|
|
@@ -258,24 +289,53 @@ export class ConfigProvider {
|
|
|
258
289
|
appliedOverrides.push(glob);
|
|
259
290
|
}
|
|
260
291
|
}
|
|
261
|
-
|
|
292
|
+
if (config === baseConfigSet.config) {
|
|
293
|
+
return baseConfigSet;
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
...(await this.#resolveOverridePlugins(baseConfigSet, config)),
|
|
297
|
+
config,
|
|
298
|
+
appliedOverrides,
|
|
299
|
+
};
|
|
262
300
|
}
|
|
263
301
|
return baseConfigSet;
|
|
264
302
|
}
|
|
265
303
|
/**
|
|
266
|
-
*
|
|
304
|
+
* @param baseConfigSet - The base configuration set, which is never mutated
|
|
305
|
+
* @param config - The configuration after the overrides are applied
|
|
306
|
+
* @returns `baseConfigSet` with the plugins of `config` loaded in, or itself (same
|
|
307
|
+
* reference) when `config` declares none
|
|
308
|
+
*/
|
|
309
|
+
async #resolveOverridePlugins(baseConfigSet, config) {
|
|
310
|
+
if (!config.plugins || config.plugins === baseConfigSet.config.plugins) {
|
|
311
|
+
return baseConfigSet;
|
|
312
|
+
}
|
|
313
|
+
const { plugins: declared, errors } = await resolvePlugins(config.plugins);
|
|
314
|
+
const baseNames = new Set(baseConfigSet.plugins.map(plugin => plugin.name));
|
|
315
|
+
const plugins = [
|
|
316
|
+
...baseConfigSet.plugins.map(plugin => declared.find(d => d.name === plugin.name) ?? plugin),
|
|
317
|
+
...declared.filter(plugin => !baseNames.has(plugin.name)),
|
|
318
|
+
];
|
|
319
|
+
// `merge` mode declares the base's plugins again, and a plugin that is not found is cached
|
|
320
|
+
// as the error, so what the base already reported is not reported twice.
|
|
321
|
+
const reported = new Set(baseConfigSet.errs.map(error => error.message));
|
|
322
|
+
const errs = [...baseConfigSet.errs, ...errors.filter(error => !reported.has(error.message))];
|
|
323
|
+
return { ...baseConfigSet, plugins, errs };
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Searches for a markuplint configuration file starting from the target file's directory
|
|
327
|
+
* and going up through its ancestors; the nearest one wins. A `package.json` without a
|
|
328
|
+
* `markuplint` field is not a configuration file, so the search passes over it. A target
|
|
329
|
+
* directory that does not exist yet, such as that of an unsaved editor buffer in a new folder,
|
|
330
|
+
* does not end the search either: the search starts from that directory and goes on through
|
|
331
|
+
* its ancestors, the missing ones yielding nothing.
|
|
267
332
|
*
|
|
268
333
|
* @param targetFile - The file whose directory to search from
|
|
269
334
|
* @returns The file path of the found config, or `null` if none was found
|
|
270
335
|
*/
|
|
271
336
|
async search(targetFile) {
|
|
272
|
-
const isExists = await targetFile.dirExists();
|
|
273
337
|
if (cpLog.enabled) {
|
|
274
338
|
cpLog('search: %s', targetFile.path);
|
|
275
|
-
cpLog('isExists: %s', isExists);
|
|
276
|
-
}
|
|
277
|
-
if (!isExists) {
|
|
278
|
-
return null;
|
|
279
339
|
}
|
|
280
340
|
const res = await search(targetFile.path, false);
|
|
281
341
|
cpLog('searched config: %O', res);
|
|
@@ -356,7 +416,6 @@ export class ConfigProvider {
|
|
|
356
416
|
const resolvedKeys = new Set();
|
|
357
417
|
const errs = [];
|
|
358
418
|
for (const key of keys) {
|
|
359
|
-
this.#recursiveLoadKeyAndDepth.clear();
|
|
360
419
|
const keySet = await this.recursiveLoad(key, cache, referrer);
|
|
361
420
|
for (const k of keySet.stack) {
|
|
362
421
|
resolvedKeys.add(k);
|
|
@@ -381,26 +440,15 @@ export class ConfigProvider {
|
|
|
381
440
|
async #pathResolve(config, filePath) {
|
|
382
441
|
const optimizedConfig = mergeConfig(config);
|
|
383
442
|
const dir = path.dirname(filePath);
|
|
443
|
+
// Only the keys of `overrides` (the globs) are resolved here; each value is a
|
|
444
|
+
// configuration of its own, whose paths are relative to the same directory.
|
|
445
|
+
const overrides = await relPathToNameOrAbsPath(dir, optimizedConfig.overrides, undefined, true);
|
|
384
446
|
return {
|
|
385
|
-
...optimizedConfig,
|
|
447
|
+
...(await resolveConfigPaths(dir, optimizedConfig)),
|
|
386
448
|
extends: await relPathToNameOrAbsPath(dir, optimizedConfig.extends),
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
specs: await relPathToNameOrAbsPath(dir, optimizedConfig.specs),
|
|
390
|
-
excludeFiles: await relPathToNameOrAbsPath(dir, optimizedConfig.excludeFiles),
|
|
391
|
-
pretenders: optimizedConfig.pretenders
|
|
392
|
-
? {
|
|
393
|
-
...optimizedConfig.pretenders,
|
|
394
|
-
files: await relPathToNameOrAbsPath(dir, optimizedConfig.pretenders?.files),
|
|
395
|
-
scan: optimizedConfig.pretenders?.scan?.map(entry => ({
|
|
396
|
-
...entry,
|
|
397
|
-
files: typeof entry.files === 'string'
|
|
398
|
-
? path.resolve(dir, entry.files)
|
|
399
|
-
: entry.files.map(f => path.resolve(dir, f)),
|
|
400
|
-
})),
|
|
401
|
-
}
|
|
449
|
+
overrides: overrides
|
|
450
|
+
? Object.fromEntries(await Promise.all(Object.entries(overrides).map(async ([glob, value]) => [glob, await resolveConfigPaths(dir, value)])))
|
|
402
451
|
: undefined,
|
|
403
|
-
overrides: await relPathToNameOrAbsPath(dir, optimizedConfig.overrides, undefined, true),
|
|
404
452
|
};
|
|
405
453
|
}
|
|
406
454
|
#validateConfig(config, filePath) {
|
|
@@ -412,18 +460,71 @@ export class ConfigProvider {
|
|
|
412
460
|
createSelector(rule.selector);
|
|
413
461
|
}
|
|
414
462
|
catch (error) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
463
|
+
// Anything but an `InvalidSelectorError` is not a fault of the selector string the user wrote
|
|
464
|
+
// (a bug, or an argument that only an extended pseudo-class reads), so it must propagate
|
|
465
|
+
// instead of being reported as a configuration error.
|
|
466
|
+
if (isFatalError(error) || !(error instanceof InvalidSelectorError)) {
|
|
467
|
+
throw error;
|
|
420
468
|
}
|
|
469
|
+
errors.push(new ConfigParserError(error.message, {
|
|
470
|
+
filePath,
|
|
471
|
+
raw: rule.selector,
|
|
472
|
+
}));
|
|
421
473
|
}
|
|
422
474
|
}
|
|
423
475
|
}
|
|
424
476
|
return errors;
|
|
425
477
|
}
|
|
426
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* Resolves the paths a configuration holds against `dir`, the directory of the file that
|
|
481
|
+
* declares it. Once configurations from different directories are merged (`extends`), the
|
|
482
|
+
* directory a relative path belongs to is no longer known, so this runs for the top level and
|
|
483
|
+
* for the value of every `overrides` entry while it still is.
|
|
484
|
+
*
|
|
485
|
+
* Every property of a configuration that holds a path has to be resolved here: one missed is
|
|
486
|
+
* resolved at the top level but left relative inside `overrides` (or the reverse), and what is
|
|
487
|
+
* left relative is read from the working directory later.
|
|
488
|
+
*
|
|
489
|
+
* The result has no `undefined` property for what the configuration does not hold. An
|
|
490
|
+
* `overrides` value is the configuration itself under `reset` mode, so an `undefined` key
|
|
491
|
+
* would show up in it as a property it never had; under `merge` mode it would overwrite the
|
|
492
|
+
* value the base configuration holds. `mergeConfig` leaves none in an `overrides` value for
|
|
493
|
+
* the same reason.
|
|
494
|
+
*
|
|
495
|
+
* @template T - The configuration type: the top level, or the value of an `overrides` entry
|
|
496
|
+
* @param dir - The directory of the configuration file that declares `config`
|
|
497
|
+
* @param config - The configuration whose paths are resolved
|
|
498
|
+
* @returns The configuration with the paths resolved
|
|
499
|
+
*/
|
|
500
|
+
async function resolveConfigPaths(dir, config) {
|
|
501
|
+
return {
|
|
502
|
+
...config,
|
|
503
|
+
...(config.plugins && { plugins: await relPathToNameOrAbsPath(dir, config.plugins, ['name']) }),
|
|
504
|
+
...(config.parser && { parser: await relPathToNameOrAbsPath(dir, config.parser) }),
|
|
505
|
+
...(config.specs && { specs: await relPathToNameOrAbsPath(dir, config.specs) }),
|
|
506
|
+
...(config.excludeFiles && { excludeFiles: await relPathToNameOrAbsPath(dir, config.excludeFiles) }),
|
|
507
|
+
...(config.pretenders && {
|
|
508
|
+
pretenders: {
|
|
509
|
+
...config.pretenders,
|
|
510
|
+
...(config.pretenders.files && {
|
|
511
|
+
files: await relPathToNameOrAbsPath(dir, config.pretenders.files),
|
|
512
|
+
}),
|
|
513
|
+
...(config.pretenders.scan && {
|
|
514
|
+
scan: config.pretenders.scan.map(entry => ({
|
|
515
|
+
...entry,
|
|
516
|
+
files: typeof entry.files === 'string'
|
|
517
|
+
? path.resolve(dir, entry.files)
|
|
518
|
+
: entry.files.map(f => path.resolve(dir, f)),
|
|
519
|
+
})),
|
|
520
|
+
}),
|
|
521
|
+
...(config.pretenders.data && {
|
|
522
|
+
data: config.pretenders.data.map(pretender => rebaseHandWrittenPretenderFilePath(pretender, relPath => path.resolve(dir, relPath))),
|
|
523
|
+
}),
|
|
524
|
+
},
|
|
525
|
+
}),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
427
528
|
async function load(filePath, cache, referrer) {
|
|
428
529
|
if (!fileExists(filePath) && (await moduleExists(filePath))) {
|
|
429
530
|
const config = (await generalImport(filePath)) ?? new ConfigLoadError('Module is not found', filePath, referrer);
|
package/lib/cosmiconfig.d.ts
CHANGED
|
@@ -10,6 +10,30 @@ type CosmiConfig = ReturnType<LoaderSync>;
|
|
|
10
10
|
* content instead of the explorer's stale cache. See #4015.
|
|
11
11
|
*/
|
|
12
12
|
export declare function clearExplorerCache(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Searches for a configuration file in the directory of `filePath` and in each ancestor
|
|
15
|
+
* directory up to the file system root, nearest first.
|
|
16
|
+
*
|
|
17
|
+
* The walk is done here and not by cosmiconfig's `searchStrategy: 'project'`: that strategy
|
|
18
|
+
* stops at the first directory that has a `package.json`, which hides a configuration file at a
|
|
19
|
+
* monorepo root from every file inside a package, while the documented behavior is that the
|
|
20
|
+
* search goes upward from the target file's directory (https://markuplint.dev/docs/configuration).
|
|
21
|
+
* `searchStrategy: 'global'` is no substitute: it also searches the OS-level configuration
|
|
22
|
+
* directory, and that cannot be turned off.
|
|
23
|
+
*
|
|
24
|
+
* A directory that has a `package.json` or `package.yaml` does not end the walk: a `package.json`
|
|
25
|
+
* without a `markuplint` field is not a configuration file, and a `package.yaml` never is, as it
|
|
26
|
+
* is not among the search places. A directory that does not exist, the target's own included,
|
|
27
|
+
* is passed over (see {@link isMissingDir}). An empty configuration is treated like no result
|
|
28
|
+
* and the walk continues. A configuration file that cannot be loaded, at any level, makes the
|
|
29
|
+
* search throw instead of being skipped. There is no upper bound below the file system root, so a
|
|
30
|
+
* configuration file in a directory outside the project applies when nothing nearer exists.
|
|
31
|
+
*
|
|
32
|
+
* @template T - The type of the configuration object
|
|
33
|
+
* @param filePath - The path of the target file; the search starts from its directory
|
|
34
|
+
* @param cacheClear - Whether to clear the explorer's caches before searching
|
|
35
|
+
* @returns The path and content of the nearest configuration file, or `null` if none was found
|
|
36
|
+
*/
|
|
13
37
|
export declare function search<T = CosmiConfig>(filePath: string, cacheClear: boolean): Promise<{
|
|
14
38
|
filePath: string;
|
|
15
39
|
config: T;
|
|
@@ -18,4 +42,29 @@ export declare function load<T = CosmiConfig>(filePath: string, cacheClear: bool
|
|
|
18
42
|
filePath: string;
|
|
19
43
|
config: T;
|
|
20
44
|
}>;
|
|
45
|
+
/**
|
|
46
|
+
* The files whose change can fix `error`, for a caller to watch or to list among the files a
|
|
47
|
+
* configuration depends on: the file it names and, when that file's `$import` target could not
|
|
48
|
+
* be read, the target, which may not exist yet. Nothing when it names a directory that a search
|
|
49
|
+
* could not examine. For a `ConfigLoadError` raised elsewhere (a preset, a module), what it names.
|
|
50
|
+
*
|
|
51
|
+
* @param error - A `ConfigLoadError`
|
|
52
|
+
* @returns The absolute paths of the files, the `$import` target first; or what `error` names
|
|
53
|
+
*/
|
|
54
|
+
export declare function configLoadErrorFiles(error: ConfigLoadError): readonly string[];
|
|
55
|
+
/**
|
|
56
|
+
* Finds the location nearest the top of `stack` that names one of `filePaths`, by its path (a
|
|
57
|
+
* CommonJS module) or by its `file:` URL (an ES module): a frame, `<path>:<line>:<column>`, or
|
|
58
|
+
* the header that Node.js puts above a `SyntaxError` in a CommonJS module, `<path>:<line>`. A
|
|
59
|
+
* `SyntaxError` in an ES module has neither. A Windows path is also looked for with forward
|
|
60
|
+
* slashes (`C:/p/…`), as a module runner such as Vitest names the module it evaluates.
|
|
61
|
+
*
|
|
62
|
+
* A path only matches as a whole, at the start of a line or after whitespace or `(`, so that
|
|
63
|
+
* another file whose path ends with the same characters does not.
|
|
64
|
+
*
|
|
65
|
+
* @param stack - The `stack` of an error
|
|
66
|
+
* @param filePaths - Absolute paths of the same file
|
|
67
|
+
* @returns `:<line>:<column>`, `:<line>` for a header, or an empty string when there is neither
|
|
68
|
+
*/
|
|
69
|
+
export declare function stackLocation(stack: string, filePaths: readonly string[]): string;
|
|
21
70
|
export {};
|