@markuplint/file-resolver 5.0.1 → 5.1.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 +8 -0
- package/lib/path-utils.d.ts +1 -1
- package/lib/path-utils.js +1 -1
- package/lib/resolve-pretenders.d.ts +72 -3
- package/lib/resolve-pretenders.js +84 -33
- package/package.json +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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.1.0](https://github.com/markuplint/markuplint/compare/v5.0.1...v5.1.0) (2026-10-08)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- **file-resolver:** add createPretenderResolver for one lint target across edits ([e8e2e55](https://github.com/markuplint/markuplint/commit/e8e2e55c523d38e0bacf54a8229bba6c1b91d6e4)), closes [#4064](https://github.com/markuplint/markuplint/issues/4064)
|
|
11
|
+
- **file-resolver:** pass `pretenders.auto.depth` on to `autoScan` ([dad6334](https://github.com/markuplint/markuplint/commit/dad63348db7336b47b0287246dd29a36de97ee3a)), closes [#4066](https://github.com/markuplint/markuplint/issues/4066)
|
|
12
|
+
- **file-resolver:** report the files a pretenders resolution depends on ([9257322](https://github.com/markuplint/markuplint/commit/92573229d0493387380adab6c7ecba1b902a2869)), closes [#4065](https://github.com/markuplint/markuplint/issues/4065)
|
|
13
|
+
|
|
6
14
|
## [5.0.1](https://github.com/markuplint/markuplint/compare/v5.0.0...v5.0.1) (2026-09-22)
|
|
7
15
|
|
|
8
16
|
**Note:** Version bump only for package @markuplint/file-resolver
|
package/lib/path-utils.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare function fromFileURL(fileUrl: string): string;
|
|
|
16
16
|
* usernames), and URL-reserved characters like `#` / `?` do not get
|
|
17
17
|
* reinterpreted as fragment / query delimiters by Node's URL parser.
|
|
18
18
|
*
|
|
19
|
-
* Mirrors `
|
|
19
|
+
* Mirrors `packages/@markuplint/language-server/src/server/get-module.ts`'s `toImportSpecifier()` —
|
|
20
20
|
* keep the two in sync when adjusting Windows-path handling.
|
|
21
21
|
*
|
|
22
22
|
* Known limitation: UNC paths (`\\server\share\...`) are passed through
|
package/lib/path-utils.js
CHANGED
|
@@ -22,7 +22,7 @@ export function fromFileURL(fileUrl) {
|
|
|
22
22
|
* usernames), and URL-reserved characters like `#` / `?` do not get
|
|
23
23
|
* reinterpreted as fragment / query delimiters by Node's URL parser.
|
|
24
24
|
*
|
|
25
|
-
* Mirrors `
|
|
25
|
+
* Mirrors `packages/@markuplint/language-server/src/server/get-module.ts`'s `toImportSpecifier()` —
|
|
26
26
|
* keep the two in sync when adjusting Windows-path handling.
|
|
27
27
|
*
|
|
28
28
|
* Known limitation: UNC paths (`\\server\share\...`) are passed through
|
|
@@ -11,6 +11,68 @@ export type ResolvePretendersContext = {
|
|
|
11
11
|
/** Full source text of the file being linted (may be unsaved editor content) */
|
|
12
12
|
readonly sourceCode: string;
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Where a resolution reports what it read.
|
|
16
|
+
*/
|
|
17
|
+
export type ResolvePretendersOptions = {
|
|
18
|
+
/**
|
|
19
|
+
* A sink the resolution adds to: the normalized (`/`-delimited) absolute
|
|
20
|
+
* paths of the files whose content its result depends on, which a
|
|
21
|
+
* watch-mode host watches to learn when to resolve again (#4065). It covers
|
|
22
|
+
* what `config.scan` and `config.auto` read, and — for
|
|
23
|
+
* {@link disambiguatePretendersForFile} — the `tsconfig.json` the target's
|
|
24
|
+
* imports were resolved with. The lint target itself is not excluded; the
|
|
25
|
+
* caller already holds it.
|
|
26
|
+
*
|
|
27
|
+
* Not covered, because a change to them is not picked up even by resolving
|
|
28
|
+
* again: `config.files` and `config.imports` (`generalImport` keeps what it
|
|
29
|
+
* loaded for the life of the process). For what else is out of reach, see
|
|
30
|
+
* `PretenderScanOptions#dependencies` in `@markuplint/ml-config`.
|
|
31
|
+
*/
|
|
32
|
+
readonly dependencies?: Set<string>;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Resolves pretenders for one lint target, across edits of that target.
|
|
36
|
+
*
|
|
37
|
+
* The sections of `config.pretenders` split by what their result depends on.
|
|
38
|
+
* `files`, `imports`, `data`, and `scan` depend on the config and the
|
|
39
|
+
* filesystem only; `auto` depends on the target's own source as well — the
|
|
40
|
+
* import graph it walks starts from the target's import statements. (So does
|
|
41
|
+
* the selector disambiguation the caller runs on the result.) A resolver
|
|
42
|
+
* reads the first group once, on the first `resolve()`, and reads the target
|
|
43
|
+
* on every `resolve()`, so a host that re-resolves after each edit of the
|
|
44
|
+
* target (`MLEngine.setCode`) re-does exactly the part an edit can change.
|
|
45
|
+
* The first group is read again only by a new resolver, which a host creates
|
|
46
|
+
* on each config resolution — a change on disk to a scanned component is
|
|
47
|
+
* therefore picked up with the next config resolution, not with the next
|
|
48
|
+
* edit of the target. See #4064.
|
|
49
|
+
*
|
|
50
|
+
* A watch-mode host closes that gap by watching the files `resolve()` reports
|
|
51
|
+
* in {@link ResolvePretendersOptions.dependencies}: a change to one of them
|
|
52
|
+
* makes it resolve the config again, which creates the new resolver. See #4065.
|
|
53
|
+
*/
|
|
54
|
+
export type PretenderResolver = {
|
|
55
|
+
/**
|
|
56
|
+
* @param context - The lint target's path/source, required only for
|
|
57
|
+
* `config.auto`; without it `auto` is skipped, as in {@link resolvePretenders}
|
|
58
|
+
* @param options - Where to report the files the result depends on. The
|
|
59
|
+
* retained target-independent entries report theirs on every call, not
|
|
60
|
+
* only on the call that read them.
|
|
61
|
+
* @returns All resolved pretender definitions — a new array on every call,
|
|
62
|
+
* so the retained target-independent entries are never exposed to mutation
|
|
63
|
+
*/
|
|
64
|
+
resolve(context?: ResolvePretendersContext, options?: ResolvePretendersOptions): Promise<Pretender[]>;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Creates a {@link PretenderResolver} for `config`. One resolver stands for one
|
|
68
|
+
* resolution of the config: its target-independent entries are read on the
|
|
69
|
+
* first `resolve()` and kept for the resolver's lifetime (a failure there is
|
|
70
|
+
* kept as well — the host's next config resolution creates a new resolver).
|
|
71
|
+
*
|
|
72
|
+
* @param config - The pretenders configuration section from the optimized config
|
|
73
|
+
* @returns The resolver; its `resolve()` yields `[]` when `config` is absent
|
|
74
|
+
*/
|
|
75
|
+
export declare function createPretenderResolver(config: PretendersConfig): PretenderResolver;
|
|
14
76
|
/**
|
|
15
77
|
* Resolves pretender definitions from files, imported modules, inline data,
|
|
16
78
|
* dynamic component scanning, and (when `context` is given) the lint
|
|
@@ -24,14 +86,20 @@ export type ResolvePretendersContext = {
|
|
|
24
86
|
* 3. `config.data` — inline pretender definitions
|
|
25
87
|
* 4. `config.scan` — dynamic component scanning via glob patterns
|
|
26
88
|
* (`files` accepts `string | string[]`)
|
|
27
|
-
* 5. `config.auto` — on-demand scan of `context`'s own import graph
|
|
89
|
+
* 5. `config.auto` — on-demand scan of `context`'s own import graph, up to
|
|
90
|
+
* `config.auto.depth` hops when the object form gives one (requires
|
|
28
91
|
* `context`; a no-op without it, e.g. when the caller has no lint target yet)
|
|
29
92
|
*
|
|
93
|
+
* Equivalent to one `resolve()` of a fresh {@link createPretenderResolver};
|
|
94
|
+
* a host that resolves the same config repeatedly for one target keeps the
|
|
95
|
+
* resolver instead.
|
|
96
|
+
*
|
|
30
97
|
* @param config - The pretenders configuration section from the optimized config
|
|
31
98
|
* @param context - The lint target's path/source, required only for `config.auto`
|
|
99
|
+
* @param options - Where to report the files the result depends on
|
|
32
100
|
* @returns An array of all resolved pretender definitions
|
|
33
101
|
*/
|
|
34
|
-
export declare function resolvePretenders(config: PretendersConfig, context?: ResolvePretendersContext): Promise<Pretender[]>;
|
|
102
|
+
export declare function resolvePretenders(config: PretendersConfig, context?: ResolvePretendersContext, options?: ResolvePretendersOptions): Promise<Pretender[]>;
|
|
35
103
|
/**
|
|
36
104
|
* Resolves selector collisions in `pretenders` for the specific file about
|
|
37
105
|
* to be linted, deferring to `@markuplint/pretenders`' `disambiguatePretenders`
|
|
@@ -42,10 +110,11 @@ export declare function resolvePretenders(config: PretendersConfig, context?: Re
|
|
|
42
110
|
* @param filePath - Absolute path of the file being linted
|
|
43
111
|
* @param sourceCode - Full source text of the file being linted
|
|
44
112
|
* @param pretenders - The flat pretender list {@link resolvePretenders} produced
|
|
113
|
+
* @param options - Where to report the files the result depends on
|
|
45
114
|
* @returns The disambiguated pretender list, or `pretenders` itself (same
|
|
46
115
|
* reference) when there was no collision to resolve
|
|
47
116
|
*/
|
|
48
|
-
export declare function disambiguatePretendersForFile(filePath: string, sourceCode: string, pretenders: readonly Pretender[]): Promise<readonly Pretender[]>;
|
|
117
|
+
export declare function disambiguatePretendersForFile(filePath: string, sourceCode: string, pretenders: readonly Pretender[], options?: ResolvePretendersOptions): Promise<readonly Pretender[]>;
|
|
49
118
|
/**
|
|
50
119
|
* Deliberately gates on selector+filePath duplication alone — NOT on the
|
|
51
120
|
* selector name shape `@markuplint/pretenders`' `disambiguatePretenders`
|
|
@@ -13,6 +13,37 @@ import { generalImport } from './general-import.js';
|
|
|
13
13
|
function rebasePretenderFilePaths(pretenders, baseDir) {
|
|
14
14
|
return pretenders.map(pretender => rebasePretenderFilePath(pretender, relPath => path.resolve(baseDir, relPath)));
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Creates a {@link PretenderResolver} for `config`. One resolver stands for one
|
|
18
|
+
* resolution of the config: its target-independent entries are read on the
|
|
19
|
+
* first `resolve()` and kept for the resolver's lifetime (a failure there is
|
|
20
|
+
* kept as well — the host's next config resolution creates a new resolver).
|
|
21
|
+
*
|
|
22
|
+
* @param config - The pretenders configuration section from the optimized config
|
|
23
|
+
* @returns The resolver; its `resolve()` yields `[]` when `config` is absent
|
|
24
|
+
*/
|
|
25
|
+
export function createPretenderResolver(config) {
|
|
26
|
+
// The promise is retained, not its result, so that overlapping first calls
|
|
27
|
+
// share one read, and the files it was read from ride along with the result.
|
|
28
|
+
let targetIndependent = null;
|
|
29
|
+
return {
|
|
30
|
+
async resolve(context, options) {
|
|
31
|
+
if (!config) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
targetIndependent ??= resolveTargetIndependentPretenders(config);
|
|
35
|
+
const retained = await targetIndependent;
|
|
36
|
+
const data = [...retained.pretenders];
|
|
37
|
+
for (const dependency of retained.dependencies) {
|
|
38
|
+
options?.dependencies?.add(dependency);
|
|
39
|
+
}
|
|
40
|
+
if (config.auto && context) {
|
|
41
|
+
await appendAutoPretenders(data, context, config.auto, options?.dependencies);
|
|
42
|
+
}
|
|
43
|
+
return data;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
16
47
|
/**
|
|
17
48
|
* Resolves pretender definitions from files, imported modules, inline data,
|
|
18
49
|
* dynamic component scanning, and (when `context` is given) the lint
|
|
@@ -26,18 +57,29 @@ function rebasePretenderFilePaths(pretenders, baseDir) {
|
|
|
26
57
|
* 3. `config.data` — inline pretender definitions
|
|
27
58
|
* 4. `config.scan` — dynamic component scanning via glob patterns
|
|
28
59
|
* (`files` accepts `string | string[]`)
|
|
29
|
-
* 5. `config.auto` — on-demand scan of `context`'s own import graph
|
|
60
|
+
* 5. `config.auto` — on-demand scan of `context`'s own import graph, up to
|
|
61
|
+
* `config.auto.depth` hops when the object form gives one (requires
|
|
30
62
|
* `context`; a no-op without it, e.g. when the caller has no lint target yet)
|
|
31
63
|
*
|
|
64
|
+
* Equivalent to one `resolve()` of a fresh {@link createPretenderResolver};
|
|
65
|
+
* a host that resolves the same config repeatedly for one target keeps the
|
|
66
|
+
* resolver instead.
|
|
67
|
+
*
|
|
32
68
|
* @param config - The pretenders configuration section from the optimized config
|
|
33
69
|
* @param context - The lint target's path/source, required only for `config.auto`
|
|
70
|
+
* @param options - Where to report the files the result depends on
|
|
34
71
|
* @returns An array of all resolved pretender definitions
|
|
35
72
|
*/
|
|
36
|
-
export
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
73
|
+
export function resolvePretenders(config, context, options) {
|
|
74
|
+
return createPretenderResolver(config).resolve(context, options);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Steps 1–4 of {@link resolvePretenders}: the sections whose result does not
|
|
78
|
+
* depend on the lint target.
|
|
79
|
+
*/
|
|
80
|
+
async function resolveTargetIndependentPretenders(config) {
|
|
40
81
|
const data = [];
|
|
82
|
+
const dependencies = new Set();
|
|
41
83
|
if (config.files) {
|
|
42
84
|
for (const file of config.files) {
|
|
43
85
|
const pretenderFile = await generalImport(file);
|
|
@@ -77,41 +119,49 @@ export async function resolvePretenders(config, context) {
|
|
|
77
119
|
if (resolved.length > 0) {
|
|
78
120
|
const scanned = await scan(resolved, {
|
|
79
121
|
ignoreComponentNames: entry.ignoreComponentNames ? [...entry.ignoreComponentNames] : undefined,
|
|
122
|
+
dependencies,
|
|
80
123
|
});
|
|
81
124
|
// `scan()` (with no `cwd` option) reports filePath relative to `process.cwd()`.
|
|
82
125
|
data.push(...rebasePretenderFilePaths(scanned, process.cwd()));
|
|
83
126
|
}
|
|
84
127
|
}
|
|
85
128
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
return { pretenders: data, dependencies };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Step 5 of {@link resolvePretenders}: appends what `config.auto` finds in
|
|
133
|
+
* `context`'s import graph to `data`, the target-independent entries.
|
|
134
|
+
*/
|
|
135
|
+
async function appendAutoPretenders(data, context, auto, dependencies) {
|
|
136
|
+
const { autoScan } = await import('@markuplint/pretenders');
|
|
137
|
+
const scanned = await autoScan(context.filePath, context.sourceCode, {
|
|
138
|
+
depth: auto === true ? undefined : auto.depth,
|
|
139
|
+
dependencies,
|
|
140
|
+
});
|
|
141
|
+
// `autoScan()` reports filePath relative to `process.cwd()`, same as `scan()`.
|
|
142
|
+
const rebased = rebasePretenderFilePaths(scanned, process.cwd());
|
|
143
|
+
// `scan` and `auto` can both walk into the same file (e.g. a component
|
|
144
|
+
// `scan`'s glob already covers that `auto`'s import-graph walk also
|
|
145
|
+
// reaches); de-duping on (selector, filePath) keeps that file's entry
|
|
146
|
+
// from appearing twice while still letting a same-selector entry from a
|
|
147
|
+
// genuinely different file through for `disambiguatePretendersForFile`
|
|
148
|
+
// to resolve. `selector` is a markuplint CSS-like selector and can
|
|
149
|
+
// legitimately contain spaces (a descendant combinator), so the pair is
|
|
150
|
+
// joined via `JSON.stringify` rather than a plain-string delimiter —
|
|
151
|
+
// otherwise two distinct (selector, filePath) pairs could concatenate to
|
|
152
|
+
// the same string and be mistaken for a duplicate.
|
|
153
|
+
const dedupeKey = (p) => JSON.stringify([p.selector, p.filePath]);
|
|
154
|
+
const seen = new Set(data.filter(p => p.filePath).map(p => dedupeKey(p)));
|
|
155
|
+
for (const pretender of rebased) {
|
|
156
|
+
if (pretender.filePath) {
|
|
157
|
+
const key = dedupeKey(pretender);
|
|
158
|
+
if (seen.has(key)) {
|
|
159
|
+
continue;
|
|
110
160
|
}
|
|
111
|
-
|
|
161
|
+
seen.add(key);
|
|
112
162
|
}
|
|
163
|
+
data.push(pretender);
|
|
113
164
|
}
|
|
114
|
-
return data;
|
|
115
165
|
}
|
|
116
166
|
/**
|
|
117
167
|
* Resolves selector collisions in `pretenders` for the specific file about
|
|
@@ -123,15 +173,16 @@ export async function resolvePretenders(config, context) {
|
|
|
123
173
|
* @param filePath - Absolute path of the file being linted
|
|
124
174
|
* @param sourceCode - Full source text of the file being linted
|
|
125
175
|
* @param pretenders - The flat pretender list {@link resolvePretenders} produced
|
|
176
|
+
* @param options - Where to report the files the result depends on
|
|
126
177
|
* @returns The disambiguated pretender list, or `pretenders` itself (same
|
|
127
178
|
* reference) when there was no collision to resolve
|
|
128
179
|
*/
|
|
129
|
-
export async function disambiguatePretendersForFile(filePath, sourceCode, pretenders) {
|
|
180
|
+
export async function disambiguatePretendersForFile(filePath, sourceCode, pretenders, options) {
|
|
130
181
|
if (!hasResolvableCollision(pretenders)) {
|
|
131
182
|
return pretenders;
|
|
132
183
|
}
|
|
133
184
|
const { disambiguatePretenders } = await import('@markuplint/pretenders');
|
|
134
|
-
return disambiguatePretenders(pretenders, { filePath, sourceCode });
|
|
185
|
+
return disambiguatePretenders(pretenders, { filePath, sourceCode, dependencies: options?.dependencies });
|
|
135
186
|
}
|
|
136
187
|
/**
|
|
137
188
|
* Deliberately gates on selector+filePath duplication alone — NOT on the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/file-resolver",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.0",
|
|
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
|
|
38
|
-
"@markuplint/ml-ast": "5.0
|
|
39
|
-
"@markuplint/ml-config": "5.0
|
|
40
|
-
"@markuplint/ml-core": "5.0
|
|
41
|
-
"@markuplint/ml-spec": "5.0
|
|
42
|
-
"@markuplint/parser-utils": "5.0
|
|
43
|
-
"@markuplint/pretenders": "5.0
|
|
44
|
-
"@markuplint/selector": "5.0
|
|
45
|
-
"@markuplint/shared": "5.0
|
|
37
|
+
"@markuplint/html-parser": "5.1.0",
|
|
38
|
+
"@markuplint/ml-ast": "5.1.0",
|
|
39
|
+
"@markuplint/ml-config": "5.1.0",
|
|
40
|
+
"@markuplint/ml-core": "5.1.0",
|
|
41
|
+
"@markuplint/ml-spec": "5.1.0",
|
|
42
|
+
"@markuplint/parser-utils": "5.1.0",
|
|
43
|
+
"@markuplint/pretenders": "5.1.0",
|
|
44
|
+
"@markuplint/selector": "5.1.0",
|
|
45
|
+
"@markuplint/shared": "5.1.0",
|
|
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": "3a061784a8211034ad0c1cbd64798c0213414f65"
|
|
55
55
|
}
|