@markuplint/file-resolver 5.0.0 → 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 CHANGED
@@ -3,6 +3,18 @@
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
+
14
+ ## [5.0.1](https://github.com/markuplint/markuplint/compare/v5.0.0...v5.0.1) (2026-09-22)
15
+
16
+ **Note:** Version bump only for package @markuplint/file-resolver
17
+
6
18
  # [5.0.0](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.7...v5.0.0) (2026-09-11)
7
19
 
8
20
  **Note:** Version bump only for package @markuplint/file-resolver
@@ -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 `vscode/src/server/get-module.ts`'s `toImportSpecifier()` —
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 `vscode/src/server/get-module.ts`'s `toImportSpecifier()` —
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 (requires
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 (requires
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 async function resolvePretenders(config, context) {
37
- if (!config) {
38
- return [];
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
- if (config.auto && context) {
87
- const { autoScan } = await import('@markuplint/pretenders');
88
- const scanned = await autoScan(context.filePath, context.sourceCode);
89
- // `autoScan()` reports filePath relative to `process.cwd()`, same as `scan()`.
90
- const rebased = rebasePretenderFilePaths(scanned, process.cwd());
91
- // `scan` and `auto` can both walk into the same file (e.g. a component
92
- // `scan`'s glob already covers that `auto`'s import-graph walk also
93
- // reaches); de-duping on (selector, filePath) keeps that file's entry
94
- // from appearing twice while still letting a same-selector entry from a
95
- // genuinely different file through for `disambiguatePretendersForFile`
96
- // to resolve. `selector` is a markuplint CSS-like selector and can
97
- // legitimately contain spaces (a descendant combinator), so the pair is
98
- // joined via `JSON.stringify` rather than a plain-string delimiter —
99
- // otherwise two distinct (selector, filePath) pairs could concatenate to
100
- // the same string and be mistaken for a duplicate.
101
- const dedupeKey = (p) => JSON.stringify([p.selector, p.filePath]);
102
- const seen = new Set(data.filter(p => p.filePath).map(p => dedupeKey(p)));
103
- for (const pretender of rebased) {
104
- if (pretender.filePath) {
105
- const key = dedupeKey(pretender);
106
- if (seen.has(key)) {
107
- continue;
108
- }
109
- seen.add(key);
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
- data.push(pretender);
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.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.0",
38
- "@markuplint/ml-ast": "5.0.0",
39
- "@markuplint/ml-config": "5.0.0",
40
- "@markuplint/ml-core": "5.0.0",
41
- "@markuplint/ml-spec": "5.0.0",
42
- "@markuplint/parser-utils": "5.0.0",
43
- "@markuplint/pretenders": "5.0.0",
44
- "@markuplint/selector": "5.0.0",
45
- "@markuplint/shared": "5.0.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": "c99706fda4c186aeb87b521eaabc5208ac144bbd"
54
+ "gitHead": "3a061784a8211034ad0c1cbd64798c0213414f65"
55
55
  }