@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 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
@@ -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, depth?: number): Promise<{
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
@@ -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
- * Recursively loads a configuration and all its `extends` dependencies.
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
- * @param key - The config file path or module name to load
115
- * @param cache - Whether to use cached results
116
- * @param referrer - The file path of the config that referenced this key
117
- * @param depth - Current recursion depth (for circular reference detection)
118
- * @returns A set of loaded config keys and any errors encountered
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, depth = 1) {
126
+ async #recursiveLoad(key, cache, referrer, ancestors, visited) {
121
127
  const stack = new Set();
122
128
  const errs = [];
123
- const ancestorDepth = this.#recursiveLoadKeyAndDepth.get(key);
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
- this.#recursiveLoadKeyAndDepth.set(key, depth);
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
- stack.add(config.filePath);
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
- for (const depKey of depKeys) {
148
- const keys = await this.recursiveLoad(depKey, cache, key, depth + 1);
149
- for (const key of keys.stack) {
150
- stack.add(key);
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
- errs.push(...keys.errs);
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 = names.filter(nonNullableFilter);
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
- return config === baseConfigSet.config ? baseConfigSet : { ...baseConfigSet, config, appliedOverrides };
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
- * Searches for a markuplint configuration file starting from the target file's directory.
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
- plugins: await relPathToNameOrAbsPath(dir, optimizedConfig.plugins, ['name']),
388
- parser: await relPathToNameOrAbsPath(dir, optimizedConfig.parser),
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
- if (error instanceof InvalidSelectorError) {
416
- errors.push(new ConfigParserError(error.message, {
417
- filePath,
418
- raw: rule.selector,
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);
@@ -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 {};