@markuplint/file-resolver 4.18.4 → 4.18.5
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 +7 -0
- package/lib/config-provider.d.ts +40 -1
- package/lib/config-provider.js +70 -7
- package/lib/cosmiconfig.d.ts +28 -0
- package/lib/cosmiconfig.js +63 -14
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
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
|
+
## [4.18.5](https://github.com/markuplint/markuplint/compare/v4.18.4...v4.18.5) (2026-10-27)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **file-resolver:** add invalidate and runExclusive to ConfigProvider ([e8490a8](https://github.com/markuplint/markuplint/commit/e8490a8d19d37277f170be480cbca6a2a62b155c)), closes [#4415](https://github.com/markuplint/markuplint/issues/4415)
|
|
11
|
+
- **file-resolver:** find the configuration above a nested package.json ([dbd077b](https://github.com/markuplint/markuplint/commit/dbd077bb13c0c3ed5f7febf77105917b411c42dd)), closes [#4405](https://github.com/markuplint/markuplint/issues/4405)
|
|
12
|
+
|
|
6
13
|
## [4.18.4](https://github.com/markuplint/markuplint/compare/v4.18.3...v4.18.4) (2026-10-22)
|
|
7
14
|
|
|
8
15
|
**Note:** Version bump only for package @markuplint/file-resolver
|
package/lib/config-provider.d.ts
CHANGED
|
@@ -10,6 +10,39 @@ import type { Nullable } from '@markuplint/shared';
|
|
|
10
10
|
*/
|
|
11
11
|
export declare class ConfigProvider {
|
|
12
12
|
#private;
|
|
13
|
+
/**
|
|
14
|
+
* Clears every cached and stored configuration: the resolved-config cache, the store of loaded
|
|
15
|
+
* and registered configurations, the plugin-resolution cache and the shared explorer's caches.
|
|
16
|
+
*
|
|
17
|
+
* `resolve()` with `cache` set to `false` calls this as its first step. That is too late for a
|
|
18
|
+
* caller that has just registered a configuration with {@link set}: the clearing discards it.
|
|
19
|
+
* Such a caller that wants a fresh re-read calls this method first, then {@link set} and
|
|
20
|
+
* {@link search}, then `resolve()` with `cache` left `true`: what was registered after the
|
|
21
|
+
* clearing survives to `resolve()` (#4415).
|
|
22
|
+
*
|
|
23
|
+
* The plugin-resolution cache and the explorer are shared by every `ConfigProvider` in the
|
|
24
|
+
* process, so clearing them here affects other instances too. They are only caches, and a
|
|
25
|
+
* later call reads the files again.
|
|
26
|
+
*/
|
|
27
|
+
invalidate(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Runs `fn` after every earlier `runExclusive()` call on this instance has settled. The queue
|
|
30
|
+
* is this provider's own, held in `#queue`.
|
|
31
|
+
*
|
|
32
|
+
* Wrap a whole {@link invalidate} → {@link set} / {@link search} → `resolve()` sequence in it:
|
|
33
|
+
* an overlapping call on the same provider (two watch-triggered re-resolves of one engine close
|
|
34
|
+
* together, or callers that share one provider) can then not clear the configurations this call
|
|
35
|
+
* has just registered and not yet consumed (#4415).
|
|
36
|
+
*
|
|
37
|
+
* Only calls that go through this method are serialized: a direct `set()`, `search()` or
|
|
38
|
+
* `resolve()` call on the same provider is not queued. `fn` must not call `runExclusive()` on
|
|
39
|
+
* the same provider and wait for it: the inner call waits for `fn` to settle, so it never ends.
|
|
40
|
+
*
|
|
41
|
+
* @template T - The type `fn` resolves to
|
|
42
|
+
* @param fn - The sequence to run exclusively
|
|
43
|
+
* @returns What `fn` resolves to; a rejection of `fn` does not block the next call
|
|
44
|
+
*/
|
|
45
|
+
runExclusive<T>(fn: () => Promise<T>): Promise<T>;
|
|
13
46
|
/**
|
|
14
47
|
* Recursively loads a configuration and all its `extends` dependencies.
|
|
15
48
|
*
|
|
@@ -27,6 +60,11 @@ export declare class ConfigProvider {
|
|
|
27
60
|
* Resolves the full configuration for a target file by merging all named configs,
|
|
28
61
|
* resolving plugins, and applying file-specific overrides.
|
|
29
62
|
*
|
|
63
|
+
* With `cache` set to `false` it first clears the store, which also discards any configuration
|
|
64
|
+
* the caller has just registered with {@link set}. A caller that registers inline
|
|
65
|
+
* configurations and wants a fresh re-read calls {@link invalidate} before registering them and
|
|
66
|
+
* passes `cache` as `true` (#4415).
|
|
67
|
+
*
|
|
30
68
|
* @param targetFile - The file being linted
|
|
31
69
|
* @param names - Config file paths or module names to merge
|
|
32
70
|
* @param cache - Whether to use cached results
|
|
@@ -34,7 +72,8 @@ export declare class ConfigProvider {
|
|
|
34
72
|
*/
|
|
35
73
|
resolve(targetFile: Readonly<MLFile>, names: readonly Nullable<string>[], cache?: boolean): Promise<ConfigSet>;
|
|
36
74
|
/**
|
|
37
|
-
* Searches for a markuplint configuration file starting from the target file's directory
|
|
75
|
+
* Searches for a markuplint configuration file starting from the target file's directory
|
|
76
|
+
* and going up through its ancestors; the nearest one wins (see `search` in `cosmiconfig.ts`).
|
|
38
77
|
*
|
|
39
78
|
* @param targetFile - The file whose directory to search from
|
|
40
79
|
* @returns The file path of the found config, or `null` if none was found
|
package/lib/config-provider.js
CHANGED
|
@@ -3,14 +3,20 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
3
3
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
4
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
5
|
};
|
|
6
|
-
var
|
|
6
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
7
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
10
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
11
|
+
};
|
|
12
|
+
var _ConfigProvider_cache, _ConfigProvider_held, _ConfigProvider_recursiveLoadKeyAndDepth, _ConfigProvider_store, _ConfigProvider_queue;
|
|
7
13
|
import path from 'node:path';
|
|
8
14
|
import { mergeConfig } from '@markuplint/ml-config';
|
|
9
15
|
import { ConfigParserError } from '@markuplint/parser-utils';
|
|
10
16
|
import { InvalidSelectorError, createSelector } from '@markuplint/selector';
|
|
11
17
|
import { nonNullableFilter, toNoEmptyStringArrayFromStringOrArray } from '@markuplint/shared';
|
|
12
18
|
import { ConfigLoadError } from './config-load-error.js';
|
|
13
|
-
import { load as loadConfig, search } from './cosmiconfig.js';
|
|
19
|
+
import { clearExplorerCache, load as loadConfig, search } from './cosmiconfig.js';
|
|
14
20
|
import { log } from './debug.js';
|
|
15
21
|
import { generalImport } from './general-import.js';
|
|
16
22
|
import { getPreset } from './get-preset.js';
|
|
@@ -34,6 +40,59 @@ export class ConfigProvider {
|
|
|
34
40
|
_ConfigProvider_held.set(this, new Set());
|
|
35
41
|
_ConfigProvider_recursiveLoadKeyAndDepth.set(this, new Map());
|
|
36
42
|
_ConfigProvider_store.set(this, new Map());
|
|
43
|
+
_ConfigProvider_queue.set(this, Promise.resolve());
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Clears every cached and stored configuration: the resolved-config cache, the store of loaded
|
|
47
|
+
* and registered configurations, the plugin-resolution cache and the shared explorer's caches.
|
|
48
|
+
*
|
|
49
|
+
* `resolve()` with `cache` set to `false` calls this as its first step. That is too late for a
|
|
50
|
+
* caller that has just registered a configuration with {@link set}: the clearing discards it.
|
|
51
|
+
* Such a caller that wants a fresh re-read calls this method first, then {@link set} and
|
|
52
|
+
* {@link search}, then `resolve()` with `cache` left `true`: what was registered after the
|
|
53
|
+
* clearing survives to `resolve()` (#4415).
|
|
54
|
+
*
|
|
55
|
+
* The plugin-resolution cache and the explorer are shared by every `ConfigProvider` in the
|
|
56
|
+
* process, so clearing them here affects other instances too. They are only caches, and a
|
|
57
|
+
* later call reads the files again.
|
|
58
|
+
*/
|
|
59
|
+
invalidate() {
|
|
60
|
+
__classPrivateFieldGet(this, _ConfigProvider_store, "f").clear();
|
|
61
|
+
__classPrivateFieldGet(this, _ConfigProvider_cache, "f").clear();
|
|
62
|
+
__classPrivateFieldGet(this, _ConfigProvider_held, "f").clear();
|
|
63
|
+
cacheClear();
|
|
64
|
+
clearExplorerCache();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Runs `fn` after every earlier `runExclusive()` call on this instance has settled. The queue
|
|
68
|
+
* is this provider's own, held in `#queue`.
|
|
69
|
+
*
|
|
70
|
+
* Wrap a whole {@link invalidate} → {@link set} / {@link search} → `resolve()` sequence in it:
|
|
71
|
+
* an overlapping call on the same provider (two watch-triggered re-resolves of one engine close
|
|
72
|
+
* together, or callers that share one provider) can then not clear the configurations this call
|
|
73
|
+
* has just registered and not yet consumed (#4415).
|
|
74
|
+
*
|
|
75
|
+
* Only calls that go through this method are serialized: a direct `set()`, `search()` or
|
|
76
|
+
* `resolve()` call on the same provider is not queued. `fn` must not call `runExclusive()` on
|
|
77
|
+
* the same provider and wait for it: the inner call waits for `fn` to settle, so it never ends.
|
|
78
|
+
*
|
|
79
|
+
* @template T - The type `fn` resolves to
|
|
80
|
+
* @param fn - The sequence to run exclusively
|
|
81
|
+
* @returns What `fn` resolves to; a rejection of `fn` does not block the next call
|
|
82
|
+
*/
|
|
83
|
+
async runExclusive(fn) {
|
|
84
|
+
const previous = __classPrivateFieldGet(this, _ConfigProvider_queue, "f");
|
|
85
|
+
let release;
|
|
86
|
+
__classPrivateFieldSet(this, _ConfigProvider_queue, new Promise(resolve => {
|
|
87
|
+
release = resolve;
|
|
88
|
+
}), "f");
|
|
89
|
+
await previous;
|
|
90
|
+
try {
|
|
91
|
+
return await fn();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
release();
|
|
95
|
+
}
|
|
37
96
|
}
|
|
38
97
|
/**
|
|
39
98
|
* Recursively loads a configuration and all its `extends` dependencies.
|
|
@@ -86,6 +145,11 @@ export class ConfigProvider {
|
|
|
86
145
|
* Resolves the full configuration for a target file by merging all named configs,
|
|
87
146
|
* resolving plugins, and applying file-specific overrides.
|
|
88
147
|
*
|
|
148
|
+
* With `cache` set to `false` it first clears the store, which also discards any configuration
|
|
149
|
+
* the caller has just registered with {@link set}. A caller that registers inline
|
|
150
|
+
* configurations and wants a fresh re-read calls {@link invalidate} before registering them and
|
|
151
|
+
* passes `cache` as `true` (#4415).
|
|
152
|
+
*
|
|
89
153
|
* @param targetFile - The file being linted
|
|
90
154
|
* @param names - Config file paths or module names to merge
|
|
91
155
|
* @param cache - Whether to use cached results
|
|
@@ -93,9 +157,7 @@ export class ConfigProvider {
|
|
|
93
157
|
*/
|
|
94
158
|
async resolve(targetFile, names, cache = true) {
|
|
95
159
|
if (!cache) {
|
|
96
|
-
|
|
97
|
-
__classPrivateFieldGet(this, _ConfigProvider_cache, "f").clear();
|
|
98
|
-
cacheClear();
|
|
160
|
+
this.invalidate();
|
|
99
161
|
}
|
|
100
162
|
const keys = names.filter(nonNullableFilter);
|
|
101
163
|
const key = keys.join(KEY_SEPARATOR);
|
|
@@ -161,7 +223,8 @@ export class ConfigProvider {
|
|
|
161
223
|
return result;
|
|
162
224
|
}
|
|
163
225
|
/**
|
|
164
|
-
* Searches for a markuplint configuration file starting from the target file's directory
|
|
226
|
+
* Searches for a markuplint configuration file starting from the target file's directory
|
|
227
|
+
* and going up through its ancestors; the nearest one wins (see `search` in `cosmiconfig.ts`).
|
|
165
228
|
*
|
|
166
229
|
* @param targetFile - The file whose directory to search from
|
|
167
230
|
* @returns The file path of the found config, or `null` if none was found
|
|
@@ -289,7 +352,7 @@ export class ConfigProvider {
|
|
|
289
352
|
return errors;
|
|
290
353
|
}
|
|
291
354
|
}
|
|
292
|
-
_ConfigProvider_cache = new WeakMap(), _ConfigProvider_held = new WeakMap(), _ConfigProvider_recursiveLoadKeyAndDepth = new WeakMap(), _ConfigProvider_store = new WeakMap();
|
|
355
|
+
_ConfigProvider_cache = new WeakMap(), _ConfigProvider_held = new WeakMap(), _ConfigProvider_recursiveLoadKeyAndDepth = new WeakMap(), _ConfigProvider_store = new WeakMap(), _ConfigProvider_queue = new WeakMap();
|
|
293
356
|
async function load(filePath, cache, referrer) {
|
|
294
357
|
if (!fileExists(filePath) && (await moduleExists(filePath))) {
|
|
295
358
|
const config = (await generalImport(filePath)) ?? new ConfigLoadError('Module is not found', filePath, referrer);
|
package/lib/cosmiconfig.d.ts
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
import type { LoaderSync } from 'cosmiconfig';
|
|
2
2
|
import { ConfigLoadError } from './config-load-error.js';
|
|
3
3
|
type CosmiConfig = ReturnType<LoaderSync>;
|
|
4
|
+
/**
|
|
5
|
+
* Clears the shared explorer's own search and load caches.
|
|
6
|
+
*
|
|
7
|
+
* `search()` and `load()` clear them only as a side effect of one call; this lets a caller
|
|
8
|
+
* clear them up front, before any `search()` or `load()`, so that the calls that follow read the
|
|
9
|
+
* current file content (see `ConfigProvider#invalidate`, #4415).
|
|
10
|
+
*/
|
|
11
|
+
export declare function clearExplorerCache(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Searches for a configuration file in the directory of `filePath` and then in each ancestor
|
|
14
|
+
* directory up to the file system root, and returns the nearest one.
|
|
15
|
+
*
|
|
16
|
+
* This walk is done here instead of by cosmiconfig's `searchStrategy: 'project'`, because that
|
|
17
|
+
* strategy stops at the first directory that has a `package.json`, which hides a configuration
|
|
18
|
+
* file at a monorepo root from every file inside a package (#4405). The documented behavior is
|
|
19
|
+
* that the search goes upward from the target file's directory
|
|
20
|
+
* (https://markuplint.dev/docs/configuration). `searchStrategy: 'global'` is not a fit either: it
|
|
21
|
+
* also searches the OS-level configuration directory, and that cannot be turned off.
|
|
22
|
+
*
|
|
23
|
+
* A `package.json` that has no `markuplint` key is not a configuration file, so the walk
|
|
24
|
+
* continues past it. There is no upper bound below the file system root, so a configuration file
|
|
25
|
+
* in a directory outside the project applies when nothing nearer exists.
|
|
26
|
+
*
|
|
27
|
+
* @template T - The type of the configuration object
|
|
28
|
+
* @param filePath - The path of the target file; the search starts from its directory
|
|
29
|
+
* @param cacheClear - Whether to clear the explorer's caches before searching
|
|
30
|
+
* @returns The path and content of the nearest configuration file, or `null` if none was found
|
|
31
|
+
*/
|
|
4
32
|
export declare function search<T = CosmiConfig>(filePath: string, cacheClear: boolean): Promise<{
|
|
5
33
|
filePath: string;
|
|
6
34
|
config: T;
|
package/lib/cosmiconfig.js
CHANGED
|
@@ -19,26 +19,75 @@ const explorer = cosmiconfig('markuplint', {
|
|
|
19
19
|
}
|
|
20
20
|
}),
|
|
21
21
|
},
|
|
22
|
-
|
|
22
|
+
// Checks only the directory it is given; see `search()` for the ancestor walk.
|
|
23
|
+
searchStrategy: 'none',
|
|
23
24
|
});
|
|
25
|
+
/**
|
|
26
|
+
* Clears the shared explorer's own search and load caches.
|
|
27
|
+
*
|
|
28
|
+
* `search()` and `load()` clear them only as a side effect of one call; this lets a caller
|
|
29
|
+
* clear them up front, before any `search()` or `load()`, so that the calls that follow read the
|
|
30
|
+
* current file content (see `ConfigProvider#invalidate`, #4415).
|
|
31
|
+
*/
|
|
32
|
+
export function clearExplorerCache() {
|
|
33
|
+
explorer.clearCaches();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Yields `dir` and then each of its ancestors, ending with the file system root.
|
|
37
|
+
*
|
|
38
|
+
* @param dir - The directory to start from
|
|
39
|
+
*/
|
|
40
|
+
function* ancestorDirs(dir) {
|
|
41
|
+
let current = path.resolve(dir);
|
|
42
|
+
while (true) {
|
|
43
|
+
yield current;
|
|
44
|
+
const parent = path.dirname(current);
|
|
45
|
+
if (parent === current) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
current = parent;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Searches for a configuration file in the directory of `filePath` and then in each ancestor
|
|
53
|
+
* directory up to the file system root, and returns the nearest one.
|
|
54
|
+
*
|
|
55
|
+
* This walk is done here instead of by cosmiconfig's `searchStrategy: 'project'`, because that
|
|
56
|
+
* strategy stops at the first directory that has a `package.json`, which hides a configuration
|
|
57
|
+
* file at a monorepo root from every file inside a package (#4405). The documented behavior is
|
|
58
|
+
* that the search goes upward from the target file's directory
|
|
59
|
+
* (https://markuplint.dev/docs/configuration). `searchStrategy: 'global'` is not a fit either: it
|
|
60
|
+
* also searches the OS-level configuration directory, and that cannot be turned off.
|
|
61
|
+
*
|
|
62
|
+
* A `package.json` that has no `markuplint` key is not a configuration file, so the walk
|
|
63
|
+
* continues past it. There is no upper bound below the file system root, so a configuration file
|
|
64
|
+
* in a directory outside the project applies when nothing nearer exists.
|
|
65
|
+
*
|
|
66
|
+
* @template T - The type of the configuration object
|
|
67
|
+
* @param filePath - The path of the target file; the search starts from its directory
|
|
68
|
+
* @param cacheClear - Whether to clear the explorer's caches before searching
|
|
69
|
+
* @returns The path and content of the nearest configuration file, or `null` if none was found
|
|
70
|
+
*/
|
|
24
71
|
export async function search(filePath, cacheClear) {
|
|
25
72
|
if (cacheClear) {
|
|
26
73
|
explorer.clearCaches();
|
|
27
74
|
}
|
|
28
|
-
const dir
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
75
|
+
for (const dir of ancestorDirs(path.dirname(filePath))) {
|
|
76
|
+
searchLog('Search dir: %s', dir);
|
|
77
|
+
const result = await explorer.search(dir).catch(cacheConfigError(dir, filePath));
|
|
78
|
+
searchLog('Search result: %O', result);
|
|
79
|
+
if (!result || result.isEmpty) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const config = result.config;
|
|
83
|
+
return {
|
|
84
|
+
filePath: result.filepath,
|
|
85
|
+
config: config && typeof config === 'object' && 'default' in config && typeof config.default === 'object'
|
|
86
|
+
? config.default
|
|
87
|
+
: config,
|
|
88
|
+
};
|
|
34
89
|
}
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
filePath: result.filepath,
|
|
38
|
-
config: config && typeof config === 'object' && 'default' in config && typeof config.default === 'object'
|
|
39
|
-
? config.default
|
|
40
|
-
: config,
|
|
41
|
-
};
|
|
90
|
+
return null;
|
|
42
91
|
}
|
|
43
92
|
export async function load(filePath, cacheClear, referrer) {
|
|
44
93
|
if (cacheClear) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/file-resolver",
|
|
3
|
-
"version": "4.18.
|
|
3
|
+
"version": "4.18.5",
|
|
4
4
|
"description": "The file resolver of markuplint",
|
|
5
5
|
"repository": "git@github.com:markuplint/markuplint.git",
|
|
6
6
|
"author": "Yusuke Hirao <yusukehirao@me.com>",
|
|
@@ -43,5 +43,5 @@
|
|
|
43
43
|
"jsonc": "2.0.0",
|
|
44
44
|
"minimatch": "10.2.6"
|
|
45
45
|
},
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "e7307cd5bdb771ac91e723c4c8869499190f2465"
|
|
47
47
|
}
|