@markuplint/file-resolver 5.0.0-rc.5 → 5.0.0-rc.7
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 +23 -0
- package/lib/config-provider.d.ts +78 -2
- package/lib/config-provider.js +172 -41
- package/lib/cosmiconfig.d.ts +9 -0
- package/lib/cosmiconfig.js +11 -0
- package/lib/types.d.ts +9 -0
- package/package.json +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
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.0.0-rc.7](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.6...v5.0.0-rc.7) (2026-08-31)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- **file-resolver:** record which overrides globs matched a target file ([c4cba6d](https://github.com/markuplint/markuplint/commit/c4cba6d9444ecb19752204a1655caad0d27f3ff8)), closes [#4023](https://github.com/markuplint/markuplint/issues/4023)
|
|
11
|
+
|
|
12
|
+
# [5.0.0-rc.6](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.5...v5.0.0-rc.6) (2026-08-30)
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- resolveConfig(false) crashes with inline config (not a file path) ([#4018](https://github.com/markuplint/markuplint/issues/4018)) ([7e38b64](https://github.com/markuplint/markuplint/commit/7e38b64caa8cca69009ee765e4aada37fc48c559)), closes [#4015](https://github.com/markuplint/markuplint/issues/4015) [#4015](https://github.com/markuplint/markuplint/issues/4015)
|
|
17
|
+
|
|
18
|
+
### Performance Improvements
|
|
19
|
+
|
|
20
|
+
- share ConfigProvider across a run's files, fix latent overrides caching bug ([#4016](https://github.com/markuplint/markuplint/issues/4016)) ([fcc1875](https://github.com/markuplint/markuplint/commit/fcc1875b1a984a5ef1bb36aa04e7b3522fefc58e)), closes [#3997](https://github.com/markuplint/markuplint/issues/3997) [#3997](https://github.com/markuplint/markuplint/issues/3997)
|
|
21
|
+
|
|
22
|
+
### BREAKING CHANGES
|
|
23
|
+
|
|
24
|
+
- `ConfigProvider#resolve(targetFile, names, false)` no longer
|
|
25
|
+
clears the provider's store/cache/plugin-resolution caches by itself. Callers
|
|
26
|
+
that relied on `cache: false` alone to force a fresh re-read must now call
|
|
27
|
+
the new `ConfigProvider#invalidate()` first.
|
|
28
|
+
|
|
6
29
|
# [5.0.0-rc.5](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.4...v5.0.0-rc.5) (2026-08-28)
|
|
7
30
|
|
|
8
31
|
### Bug Fixes
|
package/lib/config-provider.d.ts
CHANGED
|
@@ -7,9 +7,52 @@ import type { Nullable } from '@markuplint/shared';
|
|
|
7
7
|
*
|
|
8
8
|
* Handles `extends` chains, plugins, presets, overrides, and circular reference detection.
|
|
9
9
|
* Configuration files are searched via cosmiconfig and cached by file path.
|
|
10
|
+
*
|
|
11
|
+
* Designed to be shared across every target file in a run (see
|
|
12
|
+
* `MLEngineOptions.configProvider` in `packages/markuplint/src/api/ml-engine.ts`):
|
|
13
|
+
* {@link resolve}'s cache is keyed by the resolved config's `names`, not by
|
|
14
|
+
* target file, so one instance reused across many files avoids redoing
|
|
15
|
+
* merge/validate/plugin-resolution once per file that shares the same
|
|
16
|
+
* config — see #3997.
|
|
10
17
|
*/
|
|
11
18
|
export declare class ConfigProvider {
|
|
12
19
|
#private;
|
|
20
|
+
/**
|
|
21
|
+
* Clears every cached and stored config entry: the base-config cache
|
|
22
|
+
* (`#cache`), the loaded/registered config store (`#store`), `set()`'s
|
|
23
|
+
* identity→key stabilization (`#autoKeys`), `resolve-plugins.ts`'s own
|
|
24
|
+
* module-level plugin-resolution cache, and the shared `cosmiconfig`
|
|
25
|
+
* explorer's own search/load caches (so {@link search}, called right
|
|
26
|
+
* after this, re-reads the current file content instead of a stale
|
|
27
|
+
* cosmiconfig-level cache).
|
|
28
|
+
*
|
|
29
|
+
* The `cosmiconfig` explorer and the `resolve-plugins.ts` cache are
|
|
30
|
+
* module-level singletons shared by every `ConfigProvider` instance in
|
|
31
|
+
* the process — clearing them here affects other instances too, not just
|
|
32
|
+
* this one. Harmless (they just re-search/re-load), but worth knowing
|
|
33
|
+
* when reasoning about a cache-busting re-resolve's blast radius.
|
|
34
|
+
*
|
|
35
|
+
* Callers doing a cache-busting re-resolve (e.g. watch mode after a file
|
|
36
|
+
* change) must call this **before** registering any inline config via
|
|
37
|
+
* {@link set}, and before {@link search}, for that same resolve —
|
|
38
|
+
* `resolve()` itself no longer clears anything, so a `set()`/`search()`
|
|
39
|
+
* call made after `invalidate()` survives through to `resolve()`. Wrap
|
|
40
|
+
* the whole `invalidate()` → `set()`/`search()` → `resolve()` sequence in
|
|
41
|
+
* {@link runExclusive} so an overlapping call on the same instance can't
|
|
42
|
+
* interleave its own `invalidate()` in the middle of it. See #4015.
|
|
43
|
+
*/
|
|
44
|
+
invalidate(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Runs `fn` exclusively with respect to every other `runExclusive` call on
|
|
47
|
+
* this instance: queued calls wait for earlier ones to settle before
|
|
48
|
+
* starting, so two overlapping cache-busting re-resolves (e.g. two
|
|
49
|
+
* watch-triggered `MLEngine#resolveConfig(false)` calls close together,
|
|
50
|
+
* whether from one engine's own provider or several engines sharing one)
|
|
51
|
+
* can't interleave — one call's {@link invalidate} can no longer wipe the
|
|
52
|
+
* `set()`/`search()` entries another call registered a moment earlier but
|
|
53
|
+
* hasn't yet consumed. See #4015.
|
|
54
|
+
*/
|
|
55
|
+
runExclusive<T>(fn: () => Promise<T>): Promise<T>;
|
|
13
56
|
/**
|
|
14
57
|
* Recursively loads a configuration and all its `extends` dependencies.
|
|
15
58
|
*
|
|
@@ -27,9 +70,27 @@ export declare class ConfigProvider {
|
|
|
27
70
|
* Resolves the full configuration for a target file by merging all named configs,
|
|
28
71
|
* resolving plugins, and applying file-specific overrides.
|
|
29
72
|
*
|
|
73
|
+
* Split into a cacheable "base" phase (`#resolveBase`: merge/validate/plugin
|
|
74
|
+
* resolution/plugin-provided `extends`) and a per-call `overrides` phase, because
|
|
75
|
+
* `overrides` matching depends on `targetFile` while everything else in `names`
|
|
76
|
+
* resolution does not. Only the base result is cached (keyed on `names`, not on
|
|
77
|
+
* `targetFile`) — callers sharing one `ConfigProvider` across many target files
|
|
78
|
+
* (see #3997) get that work done once, while `overrides` are always re-evaluated
|
|
79
|
+
* per call so a `.vue`-only override never leaks into a `.html` file's result (or
|
|
80
|
+
* vice versa) just because they resolve the same `names`.
|
|
81
|
+
*
|
|
82
|
+
* Does NOT clear the provider's store/cache itself — call {@link invalidate}
|
|
83
|
+
* first if a fresh re-read is needed (e.g. a watch-triggered re-resolve).
|
|
84
|
+
* `cache` only controls whether an already-loaded `names` entry (this call's
|
|
85
|
+
* base-config cache, and — deeper still — cosmiconfig's own per-file cache in
|
|
86
|
+
* `#load`) is reused; it used to also wipe the store/cache up front, which
|
|
87
|
+
* discarded any `set()` call the caller had just made for this same resolve
|
|
88
|
+
* (e.g. `MLEngine#resolveConfig()` registering inline `config`/`defaultConfig`
|
|
89
|
+
* right before calling this) — see #4015.
|
|
90
|
+
*
|
|
30
91
|
* @param targetFile - The file being linted
|
|
31
92
|
* @param names - Config file paths or module names to merge
|
|
32
|
-
* @param cache - Whether to
|
|
93
|
+
* @param cache - Whether to reuse already-loaded/cached entries
|
|
33
94
|
* @returns The fully resolved configuration set including plugins and errors
|
|
34
95
|
*/
|
|
35
96
|
resolve(targetFile: Readonly<MLFile>, names: readonly Nullable<string>[], cache?: boolean): Promise<ConfigSet>;
|
|
@@ -45,7 +106,22 @@ export declare class ConfigProvider {
|
|
|
45
106
|
*
|
|
46
107
|
* @param config - The optimized configuration to store
|
|
47
108
|
* @param key - An optional key to store the config under; auto-generated if omitted
|
|
109
|
+
* @param identity - Object identity to auto-key on when `key` is omitted (e.g. the
|
|
110
|
+
* caller's original, pre-merge config object). Repeated calls with the same
|
|
111
|
+
* `identity` reuse the same generated key instead of minting a fresh UUID each
|
|
112
|
+
* time, so `resolve()`'s base cache can hit for inline (non-file-path) config
|
|
113
|
+
* shared across multiple target files — see #3997. Falls back to `config` itself
|
|
114
|
+
* (which is rebuilt fresh by every caller today, so this is a no-op unless a
|
|
115
|
+
* caller passes a stable `identity`).
|
|
116
|
+
*
|
|
117
|
+
* **Invariant**: an `identity` must correspond to `config` content that is
|
|
118
|
+
* effectively immutable for as long as that identity is reused — a second
|
|
119
|
+
* call with the same `identity` but *different* `config` content returns
|
|
120
|
+
* the *first* call's key/content, silently discarding the new content. Not
|
|
121
|
+
* reachable via `MLEngine`'s call sites today (they hold `options.config`/
|
|
122
|
+
* `defaultConfig` as one unchanged reference per run); a future caller
|
|
123
|
+
* passing a reused identity for genuinely different content would hit this.
|
|
48
124
|
* @returns The key under which the config was stored
|
|
49
125
|
*/
|
|
50
|
-
set(config: OptimizedConfig, key?: string): string;
|
|
126
|
+
set(config: OptimizedConfig, key?: string, identity?: object): string;
|
|
51
127
|
}
|
package/lib/config-provider.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mergeConfig } from '@markuplint/ml-config';
|
|
|
3
3
|
import { ConfigParserError } from '@markuplint/parser-utils';
|
|
4
4
|
import { InvalidSelectorError, createSelector } from '@markuplint/selector';
|
|
5
5
|
import { nonNullableFilter, toNoEmptyStringArrayFromStringOrArray, ConfigLoadError } from '@markuplint/shared';
|
|
6
|
-
import { load as loadConfig, search } from './cosmiconfig.js';
|
|
6
|
+
import { load as loadConfig, search, clearExplorerCache } from './cosmiconfig.js';
|
|
7
7
|
import { log } from './debug.js';
|
|
8
8
|
import { generalImport } from './general-import.js';
|
|
9
9
|
import { getPreset } from './get-preset.js';
|
|
@@ -20,12 +20,89 @@ const KEY_SEPARATOR = '__ML_CONFIG_MERGE__';
|
|
|
20
20
|
*
|
|
21
21
|
* Handles `extends` chains, plugins, presets, overrides, and circular reference detection.
|
|
22
22
|
* Configuration files are searched via cosmiconfig and cached by file path.
|
|
23
|
+
*
|
|
24
|
+
* Designed to be shared across every target file in a run (see
|
|
25
|
+
* `MLEngineOptions.configProvider` in `packages/markuplint/src/api/ml-engine.ts`):
|
|
26
|
+
* {@link resolve}'s cache is keyed by the resolved config's `names`, not by
|
|
27
|
+
* target file, so one instance reused across many files avoids redoing
|
|
28
|
+
* merge/validate/plugin-resolution once per file that shares the same
|
|
29
|
+
* config — see #3997.
|
|
23
30
|
*/
|
|
24
31
|
export class ConfigProvider {
|
|
25
32
|
#cache = new Map();
|
|
26
33
|
#held = new Set();
|
|
27
34
|
#recursiveLoadKeyAndDepth = new Map();
|
|
28
35
|
#store = new Map();
|
|
36
|
+
/**
|
|
37
|
+
* Stabilizes {@link set}'s auto-generated key for an inline config object
|
|
38
|
+
* across repeated calls with the *same* object reference (e.g. one caller
|
|
39
|
+
* sharing one `ConfigProvider` — and one `options.config`/`defaultConfig`
|
|
40
|
+
* object — across many target files). Without this, `set()` would mint a
|
|
41
|
+
* fresh UUID per call even for identical content, so `resolve()`'s cache
|
|
42
|
+
* (keyed on that UUID) would never hit for inline (non-file-path) config.
|
|
43
|
+
* Keyed on object identity, not content, so it costs nothing to check and
|
|
44
|
+
* needs no hashing of arbitrary config shapes.
|
|
45
|
+
*/
|
|
46
|
+
#autoKeys = new WeakMap();
|
|
47
|
+
/**
|
|
48
|
+
* Serializes {@link runExclusive} calls on this instance.
|
|
49
|
+
*/
|
|
50
|
+
#queue = Promise.resolve();
|
|
51
|
+
/**
|
|
52
|
+
* Clears every cached and stored config entry: the base-config cache
|
|
53
|
+
* (`#cache`), the loaded/registered config store (`#store`), `set()`'s
|
|
54
|
+
* identity→key stabilization (`#autoKeys`), `resolve-plugins.ts`'s own
|
|
55
|
+
* module-level plugin-resolution cache, and the shared `cosmiconfig`
|
|
56
|
+
* explorer's own search/load caches (so {@link search}, called right
|
|
57
|
+
* after this, re-reads the current file content instead of a stale
|
|
58
|
+
* cosmiconfig-level cache).
|
|
59
|
+
*
|
|
60
|
+
* The `cosmiconfig` explorer and the `resolve-plugins.ts` cache are
|
|
61
|
+
* module-level singletons shared by every `ConfigProvider` instance in
|
|
62
|
+
* the process — clearing them here affects other instances too, not just
|
|
63
|
+
* this one. Harmless (they just re-search/re-load), but worth knowing
|
|
64
|
+
* when reasoning about a cache-busting re-resolve's blast radius.
|
|
65
|
+
*
|
|
66
|
+
* Callers doing a cache-busting re-resolve (e.g. watch mode after a file
|
|
67
|
+
* change) must call this **before** registering any inline config via
|
|
68
|
+
* {@link set}, and before {@link search}, for that same resolve —
|
|
69
|
+
* `resolve()` itself no longer clears anything, so a `set()`/`search()`
|
|
70
|
+
* call made after `invalidate()` survives through to `resolve()`. Wrap
|
|
71
|
+
* the whole `invalidate()` → `set()`/`search()` → `resolve()` sequence in
|
|
72
|
+
* {@link runExclusive} so an overlapping call on the same instance can't
|
|
73
|
+
* interleave its own `invalidate()` in the middle of it. See #4015.
|
|
74
|
+
*/
|
|
75
|
+
invalidate() {
|
|
76
|
+
this.#store.clear();
|
|
77
|
+
this.#cache.clear();
|
|
78
|
+
this.#autoKeys = new WeakMap();
|
|
79
|
+
cacheClear();
|
|
80
|
+
clearExplorerCache();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Runs `fn` exclusively with respect to every other `runExclusive` call on
|
|
84
|
+
* this instance: queued calls wait for earlier ones to settle before
|
|
85
|
+
* starting, so two overlapping cache-busting re-resolves (e.g. two
|
|
86
|
+
* watch-triggered `MLEngine#resolveConfig(false)` calls close together,
|
|
87
|
+
* whether from one engine's own provider or several engines sharing one)
|
|
88
|
+
* can't interleave — one call's {@link invalidate} can no longer wipe the
|
|
89
|
+
* `set()`/`search()` entries another call registered a moment earlier but
|
|
90
|
+
* hasn't yet consumed. See #4015.
|
|
91
|
+
*/
|
|
92
|
+
async runExclusive(fn) {
|
|
93
|
+
const previous = this.#queue;
|
|
94
|
+
let release;
|
|
95
|
+
this.#queue = new Promise(resolve => {
|
|
96
|
+
release = resolve;
|
|
97
|
+
});
|
|
98
|
+
await previous;
|
|
99
|
+
try {
|
|
100
|
+
return await fn();
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
release();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
29
106
|
/**
|
|
30
107
|
* Recursively loads a configuration and all its `extends` dependencies.
|
|
31
108
|
*
|
|
@@ -77,28 +154,50 @@ export class ConfigProvider {
|
|
|
77
154
|
* Resolves the full configuration for a target file by merging all named configs,
|
|
78
155
|
* resolving plugins, and applying file-specific overrides.
|
|
79
156
|
*
|
|
157
|
+
* Split into a cacheable "base" phase (`#resolveBase`: merge/validate/plugin
|
|
158
|
+
* resolution/plugin-provided `extends`) and a per-call `overrides` phase, because
|
|
159
|
+
* `overrides` matching depends on `targetFile` while everything else in `names`
|
|
160
|
+
* resolution does not. Only the base result is cached (keyed on `names`, not on
|
|
161
|
+
* `targetFile`) — callers sharing one `ConfigProvider` across many target files
|
|
162
|
+
* (see #3997) get that work done once, while `overrides` are always re-evaluated
|
|
163
|
+
* per call so a `.vue`-only override never leaks into a `.html` file's result (or
|
|
164
|
+
* vice versa) just because they resolve the same `names`.
|
|
165
|
+
*
|
|
166
|
+
* Does NOT clear the provider's store/cache itself — call {@link invalidate}
|
|
167
|
+
* first if a fresh re-read is needed (e.g. a watch-triggered re-resolve).
|
|
168
|
+
* `cache` only controls whether an already-loaded `names` entry (this call's
|
|
169
|
+
* base-config cache, and — deeper still — cosmiconfig's own per-file cache in
|
|
170
|
+
* `#load`) is reused; it used to also wipe the store/cache up front, which
|
|
171
|
+
* discarded any `set()` call the caller had just made for this same resolve
|
|
172
|
+
* (e.g. `MLEngine#resolveConfig()` registering inline `config`/`defaultConfig`
|
|
173
|
+
* right before calling this) — see #4015.
|
|
174
|
+
*
|
|
80
175
|
* @param targetFile - The file being linted
|
|
81
176
|
* @param names - Config file paths or module names to merge
|
|
82
|
-
* @param cache - Whether to
|
|
177
|
+
* @param cache - Whether to reuse already-loaded/cached entries
|
|
83
178
|
* @returns The fully resolved configuration set including plugins and errors
|
|
84
179
|
*/
|
|
85
180
|
async resolve(targetFile, names, cache = true) {
|
|
86
|
-
if (!cache) {
|
|
87
|
-
this.#store.clear();
|
|
88
|
-
this.#cache.clear();
|
|
89
|
-
cacheClear();
|
|
90
|
-
}
|
|
91
181
|
const keys = names.filter(nonNullableFilter);
|
|
92
182
|
const key = keys.join(KEY_SEPARATOR);
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
183
|
+
let baseConfigSet = this.#cache.get(key);
|
|
184
|
+
if (!baseConfigSet) {
|
|
185
|
+
baseConfigSet = await this.#resolveBase(keys, cache, targetFile.path);
|
|
186
|
+
this.#cache.set(key, baseConfigSet);
|
|
96
187
|
}
|
|
97
|
-
|
|
188
|
+
return this.#applyOverrides(baseConfigSet, targetFile);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The `names`-dependent, `targetFile`-independent part of {@link resolve}:
|
|
192
|
+
* merges all named configs, validates, resolves plugins, and expands
|
|
193
|
+
* plugin-provided `extends`. Safe to cache under a `names`-only key.
|
|
194
|
+
*/
|
|
195
|
+
async #resolveBase(keys, cache, referrer) {
|
|
196
|
+
let configSet = await this.#mergeConfigs(keys, cache, referrer);
|
|
98
197
|
const filePath = [...configSet.files].toReversed()[0];
|
|
99
198
|
if (!filePath) {
|
|
100
199
|
throw new ConfigParserError('Config file not found', {
|
|
101
|
-
filePath:
|
|
200
|
+
filePath: referrer,
|
|
102
201
|
});
|
|
103
202
|
}
|
|
104
203
|
const errors = this.#validateConfig(configSet.config, filePath);
|
|
@@ -120,36 +219,43 @@ export class ConfigProvider {
|
|
|
120
219
|
}
|
|
121
220
|
}
|
|
122
221
|
}
|
|
123
|
-
configSet = await this.#mergeConfigs([...keys, ...extendHelds], cache,
|
|
222
|
+
configSet = await this.#mergeConfigs([...keys, ...extendHelds], cache, referrer);
|
|
124
223
|
this.#held.clear();
|
|
125
224
|
}
|
|
126
|
-
|
|
127
|
-
if (configSet.config.overrides) {
|
|
128
|
-
const overrides = configSet.config.overrides;
|
|
129
|
-
const globs = Object.keys(overrides);
|
|
130
|
-
for (const glob of globs) {
|
|
131
|
-
const isMatched = targetFile.matches(glob);
|
|
132
|
-
const config = overrides[glob];
|
|
133
|
-
if (isMatched && config) {
|
|
134
|
-
switch (configSet.config.overrideMode) {
|
|
135
|
-
case 'merge': {
|
|
136
|
-
configSet.config = mergeConfig(configSet.config, config);
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
default: /* or "reset" */ {
|
|
140
|
-
configSet.config = config;
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const result = {
|
|
225
|
+
return {
|
|
148
226
|
...configSet,
|
|
149
227
|
plugins,
|
|
150
228
|
};
|
|
151
|
-
|
|
152
|
-
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The `targetFile`-dependent part of {@link resolve}: matches `config.overrides`
|
|
232
|
+
* globs against `targetFile` and applies whichever match, per `overrideMode`.
|
|
233
|
+
* Never mutates `baseConfigSet` — returns it unchanged (same reference) when no
|
|
234
|
+
* override matches, or a shallow copy with a freshly computed `config` otherwise,
|
|
235
|
+
* so the cached base entry stays valid for the next target file.
|
|
236
|
+
*
|
|
237
|
+
* Matches are applied in `Object.keys(overrides)` order (config-key insertion
|
|
238
|
+
* order), each round re-assigning `config` rather than accumulating — so under
|
|
239
|
+
* the default `overrideMode: 'reset'`, the last-matching glob's config entirely
|
|
240
|
+
* replaces every earlier match, not just the base config. `appliedOverrides`
|
|
241
|
+
* records that match order for `--show-config=details` to surface, since this
|
|
242
|
+
* "last match wins outright" behavior is easy to mistake for a bug (see #4023).
|
|
243
|
+
*/
|
|
244
|
+
#applyOverrides(baseConfigSet, targetFile) {
|
|
245
|
+
let config = baseConfigSet.config;
|
|
246
|
+
const appliedOverrides = [];
|
|
247
|
+
if (config.overrides) {
|
|
248
|
+
const overrides = config.overrides;
|
|
249
|
+
for (const glob of Object.keys(overrides)) {
|
|
250
|
+
const overrideConfig = overrides[glob];
|
|
251
|
+
if (targetFile.matches(glob) && overrideConfig) {
|
|
252
|
+
config = config.overrideMode === 'merge' ? mergeConfig(config, overrideConfig) : overrideConfig;
|
|
253
|
+
appliedOverrides.push(glob);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return config === baseConfigSet.config ? baseConfigSet : { ...baseConfigSet, config, appliedOverrides };
|
|
257
|
+
}
|
|
258
|
+
return baseConfigSet;
|
|
153
259
|
}
|
|
154
260
|
/**
|
|
155
261
|
* Searches for a markuplint configuration file starting from the target file's directory.
|
|
@@ -180,12 +286,37 @@ export class ConfigProvider {
|
|
|
180
286
|
*
|
|
181
287
|
* @param config - The optimized configuration to store
|
|
182
288
|
* @param key - An optional key to store the config under; auto-generated if omitted
|
|
289
|
+
* @param identity - Object identity to auto-key on when `key` is omitted (e.g. the
|
|
290
|
+
* caller's original, pre-merge config object). Repeated calls with the same
|
|
291
|
+
* `identity` reuse the same generated key instead of minting a fresh UUID each
|
|
292
|
+
* time, so `resolve()`'s base cache can hit for inline (non-file-path) config
|
|
293
|
+
* shared across multiple target files — see #3997. Falls back to `config` itself
|
|
294
|
+
* (which is rebuilt fresh by every caller today, so this is a no-op unless a
|
|
295
|
+
* caller passes a stable `identity`).
|
|
296
|
+
*
|
|
297
|
+
* **Invariant**: an `identity` must correspond to `config` content that is
|
|
298
|
+
* effectively immutable for as long as that identity is reused — a second
|
|
299
|
+
* call with the same `identity` but *different* `config` content returns
|
|
300
|
+
* the *first* call's key/content, silently discarding the new content. Not
|
|
301
|
+
* reachable via `MLEngine`'s call sites today (they hold `options.config`/
|
|
302
|
+
* `defaultConfig` as one unchanged reference per run); a future caller
|
|
303
|
+
* passing a reused identity for genuinely different content would hit this.
|
|
183
304
|
* @returns The key under which the config was stored
|
|
184
305
|
*/
|
|
185
|
-
set(config, key) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
306
|
+
set(config, key, identity) {
|
|
307
|
+
if (key != null) {
|
|
308
|
+
this.#store.set(key, config);
|
|
309
|
+
return key;
|
|
310
|
+
}
|
|
311
|
+
const identityKey = identity ?? config;
|
|
312
|
+
const existingKey = this.#autoKeys.get(identityKey);
|
|
313
|
+
if (existingKey != null) {
|
|
314
|
+
return existingKey;
|
|
315
|
+
}
|
|
316
|
+
const newKey = uuid();
|
|
317
|
+
this.#store.set(newKey, config);
|
|
318
|
+
this.#autoKeys.set(identityKey, newKey);
|
|
319
|
+
return newKey;
|
|
189
320
|
}
|
|
190
321
|
async #load(filePath, cache, referrer) {
|
|
191
322
|
const entity = this.#store.get(filePath);
|
package/lib/cosmiconfig.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { LoaderSync } from 'cosmiconfig';
|
|
2
2
|
import { ConfigLoadError } from '@markuplint/shared';
|
|
3
3
|
type CosmiConfig = ReturnType<LoaderSync>;
|
|
4
|
+
/**
|
|
5
|
+
* Clears the shared `cosmiconfig` explorer's own internal search/load caches.
|
|
6
|
+
* Distinct from this module's `cacheClear` parameters (which clear the same
|
|
7
|
+
* caches but only as a side effect of one `search`/`load` call) — this lets a
|
|
8
|
+
* caller (see `ConfigProvider#invalidate`) clear them up front, before any
|
|
9
|
+
* `search`/`load` call, so a subsequent `search` reads the current file
|
|
10
|
+
* content instead of the explorer's stale cache. See #4015.
|
|
11
|
+
*/
|
|
12
|
+
export declare function clearExplorerCache(): void;
|
|
4
13
|
export declare function search<T = CosmiConfig>(filePath: string, cacheClear: boolean): Promise<{
|
|
5
14
|
filePath: string;
|
|
6
15
|
config: T;
|
package/lib/cosmiconfig.js
CHANGED
|
@@ -40,6 +40,17 @@ const explorer = cosmiconfig('markuplint', {
|
|
|
40
40
|
},
|
|
41
41
|
searchStrategy: 'project',
|
|
42
42
|
});
|
|
43
|
+
/**
|
|
44
|
+
* Clears the shared `cosmiconfig` explorer's own internal search/load caches.
|
|
45
|
+
* Distinct from this module's `cacheClear` parameters (which clear the same
|
|
46
|
+
* caches but only as a side effect of one `search`/`load` call) — this lets a
|
|
47
|
+
* caller (see `ConfigProvider#invalidate`) clear them up front, before any
|
|
48
|
+
* `search`/`load` call, so a subsequent `search` reads the current file
|
|
49
|
+
* content instead of the explorer's stale cache. See #4015.
|
|
50
|
+
*/
|
|
51
|
+
export function clearExplorerCache() {
|
|
52
|
+
explorer.clearCaches();
|
|
53
|
+
}
|
|
43
54
|
export async function search(filePath, cacheClear) {
|
|
44
55
|
if (cacheClear) {
|
|
45
56
|
explorer.clearCaches();
|
package/lib/types.d.ts
CHANGED
|
@@ -13,6 +13,15 @@ export interface ConfigSet {
|
|
|
13
13
|
readonly files: ReadonlySet<string>;
|
|
14
14
|
/** Errors encountered during config loading or resolution */
|
|
15
15
|
readonly errs: readonly Readonly<Error>[];
|
|
16
|
+
/**
|
|
17
|
+
* `overrides` keys (resolved to absolute globs by `OptimizedConfig`) that
|
|
18
|
+
* matched the target file, in the order they were applied — config-key
|
|
19
|
+
* order, so later entries win under `overrideMode: 'reset'`, the default.
|
|
20
|
+
* Absent — never an empty array — when no override matched this target
|
|
21
|
+
* file, whether because the config has no `overrides` at all or none of
|
|
22
|
+
* its globs matched.
|
|
23
|
+
*/
|
|
24
|
+
readonly appliedOverrides?: readonly string[];
|
|
16
25
|
}
|
|
17
26
|
/**
|
|
18
27
|
* A lint target: either a file path/glob string, or an inline source code object.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/file-resolver",
|
|
3
|
-
"version": "5.0.0-rc.
|
|
3
|
+
"version": "5.0.0-rc.7",
|
|
4
4
|
"description": "The file resolver of markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,15 +34,15 @@
|
|
|
34
34
|
"@types/node": "24.13.3"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@markuplint/html-parser": "5.0.0-rc.
|
|
38
|
-
"@markuplint/ml-ast": "5.0.0-rc.
|
|
39
|
-
"@markuplint/ml-config": "5.0.0-rc.
|
|
40
|
-
"@markuplint/ml-core": "5.0.0-rc.
|
|
41
|
-
"@markuplint/ml-spec": "5.0.0-rc.
|
|
42
|
-
"@markuplint/parser-utils": "5.0.0-rc.
|
|
43
|
-
"@markuplint/pretenders": "5.0.0-rc.
|
|
44
|
-
"@markuplint/selector": "5.0.0-rc.
|
|
45
|
-
"@markuplint/shared": "5.0.0-rc.
|
|
37
|
+
"@markuplint/html-parser": "5.0.0-rc.7",
|
|
38
|
+
"@markuplint/ml-ast": "5.0.0-rc.7",
|
|
39
|
+
"@markuplint/ml-config": "5.0.0-rc.7",
|
|
40
|
+
"@markuplint/ml-core": "5.0.0-rc.7",
|
|
41
|
+
"@markuplint/ml-spec": "5.0.0-rc.7",
|
|
42
|
+
"@markuplint/parser-utils": "5.0.0-rc.7",
|
|
43
|
+
"@markuplint/pretenders": "5.0.0-rc.7",
|
|
44
|
+
"@markuplint/selector": "5.0.0-rc.7",
|
|
45
|
+
"@markuplint/shared": "5.0.0-rc.7",
|
|
46
46
|
"cosmiconfig": "9.0.1",
|
|
47
47
|
"debug": "4.4.3",
|
|
48
48
|
"glob": "13.0.6",
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
"jsonc": "2.0.0",
|
|
52
52
|
"minimatch": "10.2.5"
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "8d7a3b801e0f5f88438c003e7eee2bd45e6aedd0"
|
|
55
55
|
}
|