@markuplint/file-resolver 5.4.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 +12 -0
- package/lib/config-provider.d.ts +20 -3
- package/lib/config-provider.js +47 -25
- package/lib/general-import.d.ts +18 -0
- package/lib/general-import.js +31 -49
- package/lib/import-module.d.ts +28 -0
- package/lib/import-module.js +64 -0
- package/lib/module-exists.d.ts +22 -0
- package/lib/module-exists.js +28 -10
- package/lib/path-to-abs-or-name.d.ts +18 -0
- package/lib/path-to-abs-or-name.js +18 -0
- package/lib/path-utils.d.ts +9 -0
- package/lib/path-utils.js +11 -0
- package/lib/resolve-name-or-abs-path.d.ts +18 -0
- package/lib/resolve-name-or-abs-path.js +24 -5
- package/lib/resolve-pretenders.d.ts +12 -2
- package/lib/resolve-pretenders.js +57 -7
- package/package.json +22 -14
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.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
|
+
|
|
6
18
|
# [5.4.0](https://github.com/markuplint/markuplint/compare/v5.3.0...v5.4.0) (2026-10-29)
|
|
7
19
|
|
|
8
20
|
### Bug Fixes
|
package/lib/config-provider.d.ts
CHANGED
|
@@ -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
|
|
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,7 +106,10 @@ 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
|
*/
|
package/lib/config-provider.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
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
7
|
import { load as loadConfig, search, clearExplorerCache, configLoadErrorFiles } from './cosmiconfig.js';
|
|
8
8
|
import { log } from './debug.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
|
-
*
|
|
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
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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,
|
|
126
|
+
async #recursiveLoad(key, cache, referrer, ancestors, visited) {
|
|
121
127
|
const stack = new Set();
|
|
122
128
|
const errs = [];
|
|
123
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -151,12 +159,18 @@ export class ConfigProvider {
|
|
|
151
159
|
}
|
|
152
160
|
const depKeys = config.extends === null ? null : toNoEmptyStringArrayFromStringOrArray(config.extends);
|
|
153
161
|
if (depKeys) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
for (const
|
|
157
|
-
|
|
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);
|
|
158
170
|
}
|
|
159
|
-
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
ancestors.delete(key);
|
|
160
174
|
}
|
|
161
175
|
}
|
|
162
176
|
stack.add(key);
|
|
@@ -185,12 +199,17 @@ export class ConfigProvider {
|
|
|
185
199
|
* right before calling this) — see #4015.
|
|
186
200
|
*
|
|
187
201
|
* @param targetFile - The file being linted
|
|
188
|
-
* @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
|
|
189
206
|
* @param cache - Whether to reuse already-loaded/cached entries
|
|
190
207
|
* @returns The fully resolved configuration set including plugins and errors
|
|
191
208
|
*/
|
|
192
209
|
async resolve(targetFile, names, cache = true) {
|
|
193
|
-
const keys =
|
|
210
|
+
const keys = await Promise.all(names
|
|
211
|
+
.filter(nonNullableFilter)
|
|
212
|
+
.map(name => (this.#store.has(name) ? name : resolveNameOrAbsPath(process.cwd(), name))));
|
|
194
213
|
const key = keys.join(KEY_SEPARATOR);
|
|
195
214
|
let baseConfigSet = this.#cache.get(key);
|
|
196
215
|
if (!baseConfigSet) {
|
|
@@ -397,7 +416,6 @@ export class ConfigProvider {
|
|
|
397
416
|
const resolvedKeys = new Set();
|
|
398
417
|
const errs = [];
|
|
399
418
|
for (const key of keys) {
|
|
400
|
-
this.#recursiveLoadKeyAndDepth.clear();
|
|
401
419
|
const keySet = await this.recursiveLoad(key, cache, referrer);
|
|
402
420
|
for (const k of keySet.stack) {
|
|
403
421
|
resolvedKeys.add(k);
|
|
@@ -442,12 +460,16 @@ export class ConfigProvider {
|
|
|
442
460
|
createSelector(rule.selector);
|
|
443
461
|
}
|
|
444
462
|
catch (error) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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;
|
|
450
468
|
}
|
|
469
|
+
errors.push(new ConfigParserError(error.message, {
|
|
470
|
+
filePath,
|
|
471
|
+
raw: rule.selector,
|
|
472
|
+
}));
|
|
451
473
|
}
|
|
452
474
|
}
|
|
453
475
|
}
|
package/lib/general-import.d.ts
CHANGED
|
@@ -1 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imports the module that a configuration names (a configuration to `extends`, a parser, a
|
|
3
|
+
* plugin, the data of `pretenders`) and returns what it exports: the default export when there is
|
|
4
|
+
* one, the namespace otherwise. Whatever cannot be imported, for whatever reason, yields `null`,
|
|
5
|
+
* as the callers report a missing module in their own terms; the reason goes to the debug log.
|
|
6
|
+
*
|
|
7
|
+
* A subpath of a package that its `exports` map does not expose is turned into the absolute path
|
|
8
|
+
* of the file first (see {@link resolvePackageSubpath}), as `import()` refuses it. A JSON module
|
|
9
|
+
* is taken by {@link importModule} through `require()`, so no `.json` file is read by hand: what
|
|
10
|
+
* `require()` takes as JSON, a byte order mark included, is what is accepted.
|
|
11
|
+
*
|
|
12
|
+
* The result is cached by specifier for the life of the process, `null` included; `import()`
|
|
13
|
+
* itself keeps a module for as long, so a fresh read is not to be had either way.
|
|
14
|
+
*
|
|
15
|
+
* @template T - What the module exports
|
|
16
|
+
* @param name - A bare specifier, with a subpath or not, or an absolute path
|
|
17
|
+
* @returns What the module exports, or `null` when it cannot be imported
|
|
18
|
+
*/
|
|
1
19
|
export declare function generalImport<T>(name: string): Promise<T | null>;
|
package/lib/general-import.js
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
1
|
import { createRequire } from 'node:module';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { isFatalError } from '@markuplint/shared';
|
|
5
4
|
import { resolve } from 'import-meta-resolve';
|
|
6
5
|
import { log } from './debug.js';
|
|
7
|
-
import {
|
|
6
|
+
import { importModule } from './import-module.js';
|
|
7
|
+
import { fromFileURL, isRelativePath, toFileURL } from './path-utils.js';
|
|
8
8
|
const gLog = log.extend('general-import');
|
|
9
9
|
const gLogSuccess = gLog.extend('success');
|
|
10
10
|
const gLogError = gLog.extend('error');
|
|
11
11
|
const cache = new Map();
|
|
12
12
|
const require = createRequire(import.meta.url);
|
|
13
|
+
/**
|
|
14
|
+
* Imports the module that a configuration names (a configuration to `extends`, a parser, a
|
|
15
|
+
* plugin, the data of `pretenders`) and returns what it exports: the default export when there is
|
|
16
|
+
* one, the namespace otherwise. Whatever cannot be imported, for whatever reason, yields `null`,
|
|
17
|
+
* as the callers report a missing module in their own terms; the reason goes to the debug log.
|
|
18
|
+
*
|
|
19
|
+
* A subpath of a package that its `exports` map does not expose is turned into the absolute path
|
|
20
|
+
* of the file first (see {@link resolvePackageSubpath}), as `import()` refuses it. A JSON module
|
|
21
|
+
* is taken by {@link importModule} through `require()`, so no `.json` file is read by hand: what
|
|
22
|
+
* `require()` takes as JSON, a byte order mark included, is what is accepted.
|
|
23
|
+
*
|
|
24
|
+
* The result is cached by specifier for the life of the process, `null` included; `import()`
|
|
25
|
+
* itself keeps a module for as long, so a fresh read is not to be had either way.
|
|
26
|
+
*
|
|
27
|
+
* @template T - What the module exports
|
|
28
|
+
* @param name - A bare specifier, with a subpath or not, or an absolute path
|
|
29
|
+
* @returns What the module exports, or `null` when it cannot be imported
|
|
30
|
+
*/
|
|
13
31
|
export async function generalImport(name) {
|
|
14
32
|
if (cache.has(name)) {
|
|
15
33
|
return cache.get(name);
|
|
@@ -33,45 +51,21 @@ export async function generalImport(name) {
|
|
|
33
51
|
if (importPath !== name) {
|
|
34
52
|
gLog('Converted to file URL: %s', importPath);
|
|
35
53
|
}
|
|
36
|
-
const imported = await
|
|
37
|
-
|
|
54
|
+
const imported = await importModule(importPath);
|
|
55
|
+
// A JSON module whose value is `null` is `null`, as a file with nothing in it; `??` would
|
|
56
|
+
// hand the namespace over instead.
|
|
57
|
+
const mod = (('default' in imported ? imported.default : imported) ?? null);
|
|
38
58
|
gLogSuccess('Success by import("%s"): %O', importPath, mod);
|
|
39
59
|
cache.set(name, mod);
|
|
40
60
|
return mod;
|
|
41
61
|
}
|
|
42
62
|
catch (error) {
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// `@markuplint/shared`) only covers errors raised by markuplint's
|
|
50
|
-
// own code, which excludes third-party import failures (e.g. Node 22+ removing
|
|
51
|
-
// import assertion syntax, or bun's stricter ESM parser). Treating
|
|
52
|
-
// every error as a recoverable null-return is the correct policy
|
|
53
|
-
// here; callers (config / parser / plugin loaders) decide how to
|
|
54
|
-
// surface the missing module.
|
|
55
|
-
if (
|
|
56
|
-
// @ts-ignore
|
|
57
|
-
'code' in error &&
|
|
58
|
-
// @ts-ignore
|
|
59
|
-
error.code === 'ERR_IMPORT_ASSERTION_TYPE_MISSING') {
|
|
60
|
-
try {
|
|
61
|
-
const mod = require(name) ?? null;
|
|
62
|
-
gLogSuccess('Success by require("%s"): %O', name, mod);
|
|
63
|
-
cache.set(name, mod);
|
|
64
|
-
return mod;
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
if (error instanceof Error && /^parse failure/i.test(error.message)) {
|
|
68
|
-
gLogError('Error in `createRequire(import.meta.url)()`: %O', error);
|
|
69
|
-
cache.set(name, null);
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
gLogError('Error in generalImport: %O', error);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
63
|
+
// `isFatalError()` is intentionally NOT applied at this catch: `importModule()` runs
|
|
64
|
+
// `import()`, which evaluates third-party module code, so a Tier-1-shaped error here
|
|
65
|
+
// (TypeError / SyntaxError / etc.) may come from the imported module or from the runtime's
|
|
66
|
+
// loader rather than from markuplint's own code, and the two cannot be told apart. Every
|
|
67
|
+
// error is a recoverable `null`; the callers (config / parser / plugin loaders) decide how
|
|
68
|
+
// to surface the missing module.
|
|
75
69
|
if (error instanceof Error) {
|
|
76
70
|
const { filePath, packageName } = /Missing\s"(?<filePath>[^"]+)"\sspecifier\sin\s"(?<packageName>[^"]+)"\spackage/.exec(error.message)
|
|
77
71
|
?.groups ?? {};
|
|
@@ -86,18 +80,6 @@ export async function generalImport(name) {
|
|
|
86
80
|
return result;
|
|
87
81
|
}
|
|
88
82
|
}
|
|
89
|
-
if (path.isAbsolute(name) && name.endsWith('.json')) {
|
|
90
|
-
try {
|
|
91
|
-
const file = await fs.readFile(name, 'utf8');
|
|
92
|
-
const json = JSON.parse(file);
|
|
93
|
-
gLogSuccess('Success by readFile("%s") and JSON.parse: %O', name, json);
|
|
94
|
-
cache.set(name, json);
|
|
95
|
-
return json;
|
|
96
|
-
}
|
|
97
|
-
catch (readError) {
|
|
98
|
-
gLogError('Error in readFile("%s"): %O', name, readError);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
83
|
gLogError('Error in `import()`: %O', error);
|
|
102
84
|
cache.set(name, null);
|
|
103
85
|
return null;
|
|
@@ -111,7 +93,7 @@ export async function generalImport(name) {
|
|
|
111
93
|
* - The package cannot be found at all
|
|
112
94
|
*/
|
|
113
95
|
function resolvePackageSubpath(name) {
|
|
114
|
-
if (path.isAbsolute(name) || name
|
|
96
|
+
if (path.isAbsolute(name) || isRelativePath(name)) {
|
|
115
97
|
return null;
|
|
116
98
|
}
|
|
117
99
|
const packageName = name.startsWith('@') ? name.split('/').slice(0, 2).join('/') : name.split('/')[0];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `import()` that takes a JSON module as one. Node.js and Deno load a JSON module through
|
|
3
|
+
* `import()` only with the import attribute `type: 'json'` (Bun takes it either way), and a
|
|
4
|
+
* module that a configuration names can be one, a configuration to `extends` or the data of
|
|
5
|
+
* `pretenders`, with or without the `.json` extension, as the `exports` map of a package can
|
|
6
|
+
* redirect a specifier. The attribute cannot be given up front, as a `.json` specifier can be
|
|
7
|
+
* redirected to a JavaScript module the same way, and the attribute would then be rejected; and
|
|
8
|
+
* Deno rejects a dynamic `import()` with the attribute as it rejects one without. So the
|
|
9
|
+
* specifier is imported as it is, and when the runtime reports a JSON module without the
|
|
10
|
+
* attribute ({@link isJsonWithoutAttribute}), or the `.json` extension says so, it is
|
|
11
|
+
* `require()`d instead: CommonJS takes a JSON module as it is on every runtime, a bare specifier
|
|
12
|
+
* and a path alike, and what it returns is handed over as the default export, the shape
|
|
13
|
+
* `import()` gives a JSON module. Any other failure of `import()` is thrown as it is, as that is
|
|
14
|
+
* what the callers read (a package that is not installed, a subpath that `exports` does not
|
|
15
|
+
* expose), and `require()` would only run the module's code again. The report is read before the
|
|
16
|
+
* code of a missing module, as Deno attaches that code to the report as well; the code decides
|
|
17
|
+
* only for a `.json` specifier that the runtime did not report as JSON.
|
|
18
|
+
*
|
|
19
|
+
* The `catch` is not guarded by `isFatalError()`: the runtimes report the missing attribute with
|
|
20
|
+
* a `TypeError`, which the guard would re-throw before the retry.
|
|
21
|
+
*
|
|
22
|
+
* @param specifier - What `import()` takes: a bare specifier, a `file:` URL, or an absolute path
|
|
23
|
+
* as `moduleExists()` passes it (a Windows drive path fails here and is left to its
|
|
24
|
+
* `require.resolve()`)
|
|
25
|
+
* @returns The module namespace that `import()` resolves to, or `{ default }` of what
|
|
26
|
+
* `require()` returns for a JSON module
|
|
27
|
+
*/
|
|
28
|
+
export declare function importModule(specifier: string): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { isModuleNotFoundError } from '@markuplint/shared';
|
|
3
|
+
import { log } from './debug.js';
|
|
4
|
+
import { fromFileURL } from './path-utils.js';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const iLog = log.extend('import-module');
|
|
7
|
+
/**
|
|
8
|
+
* `import()` that takes a JSON module as one. Node.js and Deno load a JSON module through
|
|
9
|
+
* `import()` only with the import attribute `type: 'json'` (Bun takes it either way), and a
|
|
10
|
+
* module that a configuration names can be one, a configuration to `extends` or the data of
|
|
11
|
+
* `pretenders`, with or without the `.json` extension, as the `exports` map of a package can
|
|
12
|
+
* redirect a specifier. The attribute cannot be given up front, as a `.json` specifier can be
|
|
13
|
+
* redirected to a JavaScript module the same way, and the attribute would then be rejected; and
|
|
14
|
+
* Deno rejects a dynamic `import()` with the attribute as it rejects one without. So the
|
|
15
|
+
* specifier is imported as it is, and when the runtime reports a JSON module without the
|
|
16
|
+
* attribute ({@link isJsonWithoutAttribute}), or the `.json` extension says so, it is
|
|
17
|
+
* `require()`d instead: CommonJS takes a JSON module as it is on every runtime, a bare specifier
|
|
18
|
+
* and a path alike, and what it returns is handed over as the default export, the shape
|
|
19
|
+
* `import()` gives a JSON module. Any other failure of `import()` is thrown as it is, as that is
|
|
20
|
+
* what the callers read (a package that is not installed, a subpath that `exports` does not
|
|
21
|
+
* expose), and `require()` would only run the module's code again. The report is read before the
|
|
22
|
+
* code of a missing module, as Deno attaches that code to the report as well; the code decides
|
|
23
|
+
* only for a `.json` specifier that the runtime did not report as JSON.
|
|
24
|
+
*
|
|
25
|
+
* The `catch` is not guarded by `isFatalError()`: the runtimes report the missing attribute with
|
|
26
|
+
* a `TypeError`, which the guard would re-throw before the retry.
|
|
27
|
+
*
|
|
28
|
+
* @param specifier - What `import()` takes: a bare specifier, a `file:` URL, or an absolute path
|
|
29
|
+
* as `moduleExists()` passes it (a Windows drive path fails here and is left to its
|
|
30
|
+
* `require.resolve()`)
|
|
31
|
+
* @returns The module namespace that `import()` resolves to, or `{ default }` of what
|
|
32
|
+
* `require()` returns for a JSON module
|
|
33
|
+
*/
|
|
34
|
+
export async function importModule(specifier) {
|
|
35
|
+
try {
|
|
36
|
+
return await import(specifier);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const isJson = isJsonWithoutAttribute(error) || (specifier.endsWith('.json') && !isModuleNotFoundError(error));
|
|
40
|
+
if (!isJson) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
iLog('`import()` refused %s as a JSON module (%O); taking `require()`', specifier, error);
|
|
44
|
+
return { default: require(specifier.startsWith('file:') ? fromFileURL(specifier) : specifier) };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether `import()` reported that the module is JSON and the attribute was not given. The
|
|
49
|
+
* wording is each runtime's own, observed in `general-import.runtime.spec.ts` in a fresh process
|
|
50
|
+
* of the runtime that runs the specs:
|
|
51
|
+
*
|
|
52
|
+
* - Node.js: the code `ERR_IMPORT_ATTRIBUTE_MISSING`
|
|
53
|
+
* - Deno: `Attempted to load JSON module without specifying "type": "json" attribute in the
|
|
54
|
+
* import statement.`
|
|
55
|
+
* - Bun: never, as it takes a JSON module without the attribute
|
|
56
|
+
*
|
|
57
|
+
* @param error - What `import()` threw
|
|
58
|
+
* @returns `true` for that report
|
|
59
|
+
*/
|
|
60
|
+
function isJsonWithoutAttribute(error) {
|
|
61
|
+
return (error instanceof Error &&
|
|
62
|
+
(('code' in error && error.code === 'ERR_IMPORT_ATTRIBUTE_MISSING') ||
|
|
63
|
+
error.message.startsWith('Attempted to load JSON module without specifying "type": "json" attribute')));
|
|
64
|
+
}
|
package/lib/module-exists.d.ts
CHANGED
|
@@ -1 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `name` resolves to a module from where markuplint is installed: a package, or a file of
|
|
3
|
+
* one, as `extends`, `plugins`, `parser` and `specs` take a module.
|
|
4
|
+
*
|
|
5
|
+
* The resolution is that of this file's own location, as `import()` and `createRequire()` do it,
|
|
6
|
+
* never that of the configuration file that names the module, so a relative path is not a module
|
|
7
|
+
* here and is answered `false` without asking the runtime: resolved from this file, it would reach
|
|
8
|
+
* this package's own files or whatever lies above them. `resolveNameOrAbsPath` keeps a value
|
|
9
|
+
* spelled as a path away in the first place; `ConfigProvider` passes the names it is given as they
|
|
10
|
+
* are, so a relative one is answered `false` here and rejected as not absolute by its `#load`.
|
|
11
|
+
*
|
|
12
|
+
* The `catch` is not guarded by `isFatalError()`: what `import()` throws comes from the runtime's
|
|
13
|
+
* loader, which reports a file it cannot take as a module with a `SyntaxError` or a `TypeError`.
|
|
14
|
+
* Vite's module runner, which Vitest runs the specs in, reports such a file as a `Parse failure`,
|
|
15
|
+
* and that alone proves the file resolves; after any other failure the specifier is asked of
|
|
16
|
+
* `require.resolve()`, and what that throws, besides the module being absent or unexposed, is
|
|
17
|
+
* re-thrown as it is.
|
|
18
|
+
*
|
|
19
|
+
* @param name - A bare specifier, or an absolute path
|
|
20
|
+
* @returns `true` when the module resolves, including a file that the package's `exports` do not
|
|
21
|
+
* expose, and a file that resolves but cannot be imported; `false` for a relative path
|
|
22
|
+
*/
|
|
1
23
|
export declare function moduleExists(name: string): Promise<boolean>;
|
package/lib/module-exists.js
CHANGED
|
@@ -1,21 +1,39 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { log } from './debug.js';
|
|
3
|
+
import { importModule } from './import-module.js';
|
|
4
|
+
import { isRelativePath } from './path-utils.js';
|
|
3
5
|
const require = createRequire(import.meta.url);
|
|
4
6
|
const mLog = log.extend('module-exists');
|
|
7
|
+
/**
|
|
8
|
+
* Whether `name` resolves to a module from where markuplint is installed: a package, or a file of
|
|
9
|
+
* one, as `extends`, `plugins`, `parser` and `specs` take a module.
|
|
10
|
+
*
|
|
11
|
+
* The resolution is that of this file's own location, as `import()` and `createRequire()` do it,
|
|
12
|
+
* never that of the configuration file that names the module, so a relative path is not a module
|
|
13
|
+
* here and is answered `false` without asking the runtime: resolved from this file, it would reach
|
|
14
|
+
* this package's own files or whatever lies above them. `resolveNameOrAbsPath` keeps a value
|
|
15
|
+
* spelled as a path away in the first place; `ConfigProvider` passes the names it is given as they
|
|
16
|
+
* are, so a relative one is answered `false` here and rejected as not absolute by its `#load`.
|
|
17
|
+
*
|
|
18
|
+
* The `catch` is not guarded by `isFatalError()`: what `import()` throws comes from the runtime's
|
|
19
|
+
* loader, which reports a file it cannot take as a module with a `SyntaxError` or a `TypeError`.
|
|
20
|
+
* Vite's module runner, which Vitest runs the specs in, reports such a file as a `Parse failure`,
|
|
21
|
+
* and that alone proves the file resolves; after any other failure the specifier is asked of
|
|
22
|
+
* `require.resolve()`, and what that throws, besides the module being absent or unexposed, is
|
|
23
|
+
* re-thrown as it is.
|
|
24
|
+
*
|
|
25
|
+
* @param name - A bare specifier, or an absolute path
|
|
26
|
+
* @returns `true` when the module resolves, including a file that the package's `exports` do not
|
|
27
|
+
* expose, and a file that resolves but cannot be imported; `false` for a relative path
|
|
28
|
+
*/
|
|
5
29
|
export async function moduleExists(name) {
|
|
30
|
+
if (isRelativePath(name)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
6
33
|
try {
|
|
7
|
-
await
|
|
34
|
+
await importModule(name);
|
|
8
35
|
}
|
|
9
36
|
catch (error) {
|
|
10
|
-
if (
|
|
11
|
-
// @ts-ignore
|
|
12
|
-
'code' in error &&
|
|
13
|
-
// @ts-ignore
|
|
14
|
-
error.code === 'ERR_IMPORT_ASSERTION_TYPE_MISSING') {
|
|
15
|
-
// It exists, but it is may be a JSON file.
|
|
16
|
-
mLog('Return true, but it caught Error in `import()`: %O', error);
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
37
|
if (error instanceof Error && /^parse failure/i.test(error.message)) {
|
|
20
38
|
// It exists, but it failed to parse.
|
|
21
39
|
mLog('Return true, but it caught Error in `import()`: %O', error);
|
|
@@ -1 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies {@link resolveNameOrAbsPath} to a property of a configuration in whichever shape it
|
|
3
|
+
* takes: one value, a list of values, or a map such as `parser` (an extension to a module or a
|
|
4
|
+
* file) and `overrides` (a glob to a configuration).
|
|
5
|
+
*
|
|
6
|
+
* In a map, or in an entry of a list, every string value is resolved unless `resolveProps` names
|
|
7
|
+
* the properties that hold a path, as `name` is the one of a `plugins` entry. The keys are left
|
|
8
|
+
* alone unless `resolveKey` says they are paths, as the globs of `overrides` are; the values of
|
|
9
|
+
* `overrides` are configurations in turn, which the caller resolves one by one.
|
|
10
|
+
*
|
|
11
|
+
* @template T - The shape of the property
|
|
12
|
+
* @param dir - The directory of the configuration file that declares the property
|
|
13
|
+
* @param filePath - The property, or `undefined` when the configuration does not hold it
|
|
14
|
+
* @param resolveProps - The properties of a map, or of an entry of a list, whose values are paths;
|
|
15
|
+
* every string value is one when omitted
|
|
16
|
+
* @param resolveKey - Whether the keys of a map are paths
|
|
17
|
+
* @returns The property with its paths resolved, in the same shape, or `undefined` as given
|
|
18
|
+
*/
|
|
1
19
|
export declare function relPathToNameOrAbsPath<T extends string | readonly (string | Record<string, unknown>)[] | Readonly<Record<string, unknown>> | undefined>(dir: string, filePath?: T, resolveProps?: readonly string[], resolveKey?: boolean): Promise<T>;
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { resolveNameOrAbsPath } from './resolve-name-or-abs-path.js';
|
|
2
|
+
/**
|
|
3
|
+
* Applies {@link resolveNameOrAbsPath} to a property of a configuration in whichever shape it
|
|
4
|
+
* takes: one value, a list of values, or a map such as `parser` (an extension to a module or a
|
|
5
|
+
* file) and `overrides` (a glob to a configuration).
|
|
6
|
+
*
|
|
7
|
+
* In a map, or in an entry of a list, every string value is resolved unless `resolveProps` names
|
|
8
|
+
* the properties that hold a path, as `name` is the one of a `plugins` entry. The keys are left
|
|
9
|
+
* alone unless `resolveKey` says they are paths, as the globs of `overrides` are; the values of
|
|
10
|
+
* `overrides` are configurations in turn, which the caller resolves one by one.
|
|
11
|
+
*
|
|
12
|
+
* @template T - The shape of the property
|
|
13
|
+
* @param dir - The directory of the configuration file that declares the property
|
|
14
|
+
* @param filePath - The property, or `undefined` when the configuration does not hold it
|
|
15
|
+
* @param resolveProps - The properties of a map, or of an entry of a list, whose values are paths;
|
|
16
|
+
* every string value is one when omitted
|
|
17
|
+
* @param resolveKey - Whether the keys of a map are paths
|
|
18
|
+
* @returns The property with its paths resolved, in the same shape, or `undefined` as given
|
|
19
|
+
*/
|
|
2
20
|
export async function relPathToNameOrAbsPath(dir, filePath, resolveProps, resolveKey = false) {
|
|
3
21
|
if (filePath == null) {
|
|
4
22
|
// @ts-ignore
|
package/lib/path-utils.d.ts
CHANGED
|
@@ -31,3 +31,12 @@ export declare function toFileURL(filePath: string): string;
|
|
|
31
31
|
export declare function normalizeForIgnore(filePath: string, relative?: boolean): string;
|
|
32
32
|
/** Glob libraries require forward slashes. */
|
|
33
33
|
export declare function normalizeForGlob(filePath: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Whether `filePath` is written as a relative path: a `.` or `..` segment first, followed by
|
|
36
|
+
* either separator (`./x`, `../x`, `.\x`, `..\x`) or by nothing (`.`, `..`). A dot-file name such
|
|
37
|
+
* as `.markuplintrc` is not one.
|
|
38
|
+
*
|
|
39
|
+
* @param filePath - A value of a configuration that names a file or a module
|
|
40
|
+
* @returns `true` for a relative path
|
|
41
|
+
*/
|
|
42
|
+
export declare function isRelativePath(filePath: string): boolean;
|
package/lib/path-utils.js
CHANGED
|
@@ -76,3 +76,14 @@ export function normalizeForIgnore(filePath, relative = false) {
|
|
|
76
76
|
export function normalizeForGlob(filePath) {
|
|
77
77
|
return toSlash(filePath);
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether `filePath` is written as a relative path: a `.` or `..` segment first, followed by
|
|
81
|
+
* either separator (`./x`, `../x`, `.\x`, `..\x`) or by nothing (`.`, `..`). A dot-file name such
|
|
82
|
+
* as `.markuplintrc` is not one.
|
|
83
|
+
*
|
|
84
|
+
* @param filePath - A value of a configuration that names a file or a module
|
|
85
|
+
* @returns `true` for a relative path
|
|
86
|
+
*/
|
|
87
|
+
export function isRelativePath(filePath) {
|
|
88
|
+
return /^\.\.?(?:[/\\]|$)/.test(filePath);
|
|
89
|
+
}
|
|
@@ -1 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a value of a configuration that names a module or a file: a module name stays as it
|
|
3
|
+
* is, and a path becomes absolute against `dir`.
|
|
4
|
+
*
|
|
5
|
+
* A value spelled as a path (`./`, `../`, `.`, `..`, with either separator, or absolute) and a
|
|
6
|
+
* value with the `!` prefix of an exclusion are resolved without asking `moduleExists()`, which
|
|
7
|
+
* resolves a relative specifier from where markuplint is installed, not from the configuration: a
|
|
8
|
+
* file by that name there would make the value a module, keep it relative, and have it read from
|
|
9
|
+
* the working directory later. Installed in a project's `node_modules`, this package's `lib/` lies
|
|
10
|
+
* four directories below the project root (`node_modules/@markuplint/file-resolver/lib/`), so a
|
|
11
|
+
* configuration four directories below the root collided with the root's own `.markuplintrc`
|
|
12
|
+
* through `../../../../.markuplintrc`, and `../package.json` names this package's own. Any other
|
|
13
|
+
* value is tried as a module first, as the documentation of `extends` and its kin describes.
|
|
14
|
+
*
|
|
15
|
+
* @param dir - The directory of the configuration file that declares the value
|
|
16
|
+
* @param pathOrModName - The value, possibly with the `!` prefix of an exclusion
|
|
17
|
+
* @returns The module name as it is, or the absolute path, with the `!` prefix kept
|
|
18
|
+
*/
|
|
1
19
|
export declare function resolveNameOrAbsPath(dir: string, pathOrModName: string): Promise<string>;
|
|
@@ -2,13 +2,32 @@ import path from 'node:path';
|
|
|
2
2
|
import { isPluginModuleName } from './is-plugin-module-name.js';
|
|
3
3
|
import { isPresetModuleName } from './is-preset-module-name.js';
|
|
4
4
|
import { moduleExists } from './module-exists.js';
|
|
5
|
+
import { isRelativePath } from './path-utils.js';
|
|
6
|
+
/**
|
|
7
|
+
* Resolves a value of a configuration that names a module or a file: a module name stays as it
|
|
8
|
+
* is, and a path becomes absolute against `dir`.
|
|
9
|
+
*
|
|
10
|
+
* A value spelled as a path (`./`, `../`, `.`, `..`, with either separator, or absolute) and a
|
|
11
|
+
* value with the `!` prefix of an exclusion are resolved without asking `moduleExists()`, which
|
|
12
|
+
* resolves a relative specifier from where markuplint is installed, not from the configuration: a
|
|
13
|
+
* file by that name there would make the value a module, keep it relative, and have it read from
|
|
14
|
+
* the working directory later. Installed in a project's `node_modules`, this package's `lib/` lies
|
|
15
|
+
* four directories below the project root (`node_modules/@markuplint/file-resolver/lib/`), so a
|
|
16
|
+
* configuration four directories below the root collided with the root's own `.markuplintrc`
|
|
17
|
+
* through `../../../../.markuplintrc`, and `../package.json` names this package's own. Any other
|
|
18
|
+
* value is tried as a module first, as the documentation of `extends` and its kin describes.
|
|
19
|
+
*
|
|
20
|
+
* @param dir - The directory of the configuration file that declares the value
|
|
21
|
+
* @param pathOrModName - The value, possibly with the `!` prefix of an exclusion
|
|
22
|
+
* @returns The module name as it is, or the absolute path, with the `!` prefix kept
|
|
23
|
+
*/
|
|
5
24
|
export async function resolveNameOrAbsPath(dir, pathOrModName) {
|
|
6
|
-
if ((await moduleExists(pathOrModName)) || isPresetModuleName(pathOrModName) || isPluginModuleName(pathOrModName)) {
|
|
7
|
-
return pathOrModName;
|
|
8
|
-
}
|
|
9
25
|
const bangAndPath = /^(!)(.*)/.exec(pathOrModName) ?? [];
|
|
10
26
|
const bang = bangAndPath[1] ?? '';
|
|
11
27
|
const pathname = bangAndPath[2] ?? pathOrModName;
|
|
12
|
-
const
|
|
13
|
-
|
|
28
|
+
const isPath = bang !== '' || path.isAbsolute(pathname) || isRelativePath(pathname);
|
|
29
|
+
if (!isPath && (isPresetModuleName(pathname) || isPluginModuleName(pathname) || (await moduleExists(pathname)))) {
|
|
30
|
+
return pathOrModName;
|
|
31
|
+
}
|
|
32
|
+
return bang + path.resolve(dir, pathname);
|
|
14
33
|
}
|
|
@@ -60,6 +60,8 @@ export type PretenderResolver = {
|
|
|
60
60
|
* only on the call that read them.
|
|
61
61
|
* @returns All resolved pretender definitions — a new array on every call,
|
|
62
62
|
* so the retained target-independent entries are never exposed to mutation
|
|
63
|
+
* @throws An `Error` that says how to install `@markuplint/pretenders` when `config.scan`
|
|
64
|
+
* or `config.auto` needs it and it is not installed, with the error of the import as its `cause`
|
|
63
65
|
*/
|
|
64
66
|
resolve(context?: ResolvePretendersContext, options?: ResolvePretendersOptions): Promise<Pretender[]>;
|
|
65
67
|
};
|
|
@@ -70,7 +72,8 @@ export type PretenderResolver = {
|
|
|
70
72
|
* kept as well — the host's next config resolution creates a new resolver).
|
|
71
73
|
*
|
|
72
74
|
* @param config - The pretenders configuration section from the optimized config
|
|
73
|
-
* @returns The resolver; its `resolve()` yields `[]` when `config` is absent
|
|
75
|
+
* @returns The resolver; its `resolve()` yields `[]` when `config` is absent, and rejects as
|
|
76
|
+
* {@link resolvePretenders} does when `@markuplint/pretenders` is needed and not installed
|
|
74
77
|
*/
|
|
75
78
|
export declare function createPretenderResolver(config: PretendersConfig): PretenderResolver;
|
|
76
79
|
/**
|
|
@@ -98,6 +101,10 @@ export declare function createPretenderResolver(config: PretendersConfig): Prete
|
|
|
98
101
|
* @param context - The lint target's path/source, required only for `config.auto`
|
|
99
102
|
* @param options - Where to report the files the result depends on
|
|
100
103
|
* @returns An array of all resolved pretender definitions
|
|
104
|
+
* @throws An `Error` that says how to install `@markuplint/pretenders` when `config.scan` or
|
|
105
|
+
* `config.auto` needs it and it is not installed (an optional peer of this package), with the
|
|
106
|
+
* error of the import as its `cause`. A module that an installed `@markuplint/pretenders`
|
|
107
|
+
* needs being missing is thrown as it is.
|
|
101
108
|
*/
|
|
102
109
|
export declare function resolvePretenders(config: PretendersConfig, context?: ResolvePretendersContext, options?: ResolvePretendersOptions): Promise<Pretender[]>;
|
|
103
110
|
/**
|
|
@@ -116,6 +123,8 @@ export declare function resolvePretenders(config: PretendersConfig, context?: Re
|
|
|
116
123
|
* @param options - Where to report the files the result depends on
|
|
117
124
|
* @returns The disambiguated pretender list, or `pretenders` itself (same
|
|
118
125
|
* reference) when there was nothing to resolve
|
|
126
|
+
* @throws An `Error` that says how to install `@markuplint/pretenders` when an entry has a
|
|
127
|
+
* `filePath` and it is not installed, with the error of the import as its `cause`
|
|
119
128
|
*/
|
|
120
129
|
export declare function disambiguatePretendersForFile(filePath: string, sourceCode: string, pretenders: readonly Pretender[], options?: ResolvePretendersOptions): Promise<readonly Pretender[]>;
|
|
121
130
|
/**
|
|
@@ -136,7 +145,8 @@ export declare function hasResolvableCollision(pretenders: readonly Pretender[])
|
|
|
136
145
|
* newly valid tsconfig `paths` alias keeps resolving as it did before the
|
|
137
146
|
* change for the rest of the process's lifetime. A no-op (not an error) when
|
|
138
147
|
* `@markuplint/pretenders` isn't installed, since nothing has populated its
|
|
139
|
-
* caches in that case either
|
|
148
|
+
* caches in that case either, and when it is installed but fails to load, which
|
|
149
|
+
* is logged under the `ml-fr` debug namespace: what needs it fails on its own, with the reason.
|
|
140
150
|
*
|
|
141
151
|
* @returns A promise that resolves once the caches have been cleared
|
|
142
152
|
*/
|
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { rebaseHandWrittenPretenderFilePath, rebasePretenderFilePath } from '@markuplint/ml-config';
|
|
3
|
+
import { isFatalError, missingPackageOf } from '@markuplint/shared';
|
|
3
4
|
import { glob } from 'glob';
|
|
5
|
+
import { log } from './debug.js';
|
|
4
6
|
import { generalImport } from './general-import.js';
|
|
5
7
|
import { normalizeForGlob } from './path-utils.js';
|
|
8
|
+
const PRETENDERS_PACKAGE = '@markuplint/pretenders';
|
|
9
|
+
/**
|
|
10
|
+
* `@markuplint/pretenders` is an optional peer of this package (`peerDependenciesMeta`): installing markuplint
|
|
11
|
+
* does not install it, nor the parsers and the frameworks behind them, so it is loaded only for a
|
|
12
|
+
* configuration that needs it, and one that does is told how to install it rather than given the bare
|
|
13
|
+
* `ERR_MODULE_NOT_FOUND` of Node.
|
|
14
|
+
*
|
|
15
|
+
* Only the package itself missing gets that message. A module that an installed `@markuplint/pretenders`
|
|
16
|
+
* needs being missing is a fault of the installation, and is thrown as it is (see `missingPackageOf` in
|
|
17
|
+
* `@markuplint/shared`).
|
|
18
|
+
*
|
|
19
|
+
* @param name - The export to load
|
|
20
|
+
* @param needs - What needs it, as the start of the sentence of the error: an option of the configuration
|
|
21
|
+
* @returns The export
|
|
22
|
+
* @throws An `Error` that says how to install the package, with the error of the import as its `cause`
|
|
23
|
+
*/
|
|
24
|
+
async function importFromPretenders(name, needs) {
|
|
25
|
+
try {
|
|
26
|
+
const mod = await import('@markuplint/pretenders');
|
|
27
|
+
return mod[name];
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (isFatalError(error) || missingPackageOf(error) !== PRETENDERS_PACKAGE) {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`${needs} needs ${PRETENDERS_PACKAGE}, which is not installed. Add it to your project as a development dependency (for example, npm install --save-dev ${PRETENDERS_PACKAGE}), together with the parser package of each language you scan: @markuplint/vue-parser for .vue, @markuplint/svelte-parser for .svelte, @markuplint/astro-parser for .astro (and @markuplint/jsx-parser, @markuplint/mdx-parser for JSX and MDX).`, { cause: error });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
6
36
|
/**
|
|
7
37
|
* `Pretender.filePath` is written by scanners (and by hand, in `data`) relative to
|
|
8
38
|
* their own base directory, which is meaningless once entries from
|
|
@@ -22,7 +52,8 @@ function rebasePretenderFilePaths(pretenders, baseDir) {
|
|
|
22
52
|
* kept as well — the host's next config resolution creates a new resolver).
|
|
23
53
|
*
|
|
24
54
|
* @param config - The pretenders configuration section from the optimized config
|
|
25
|
-
* @returns The resolver; its `resolve()` yields `[]` when `config` is absent
|
|
55
|
+
* @returns The resolver; its `resolve()` yields `[]` when `config` is absent, and rejects as
|
|
56
|
+
* {@link resolvePretenders} does when `@markuplint/pretenders` is needed and not installed
|
|
26
57
|
*/
|
|
27
58
|
export function createPretenderResolver(config) {
|
|
28
59
|
// The promise is retained, not its result, so that overlapping first calls
|
|
@@ -71,6 +102,10 @@ export function createPretenderResolver(config) {
|
|
|
71
102
|
* @param context - The lint target's path/source, required only for `config.auto`
|
|
72
103
|
* @param options - Where to report the files the result depends on
|
|
73
104
|
* @returns An array of all resolved pretender definitions
|
|
105
|
+
* @throws An `Error` that says how to install `@markuplint/pretenders` when `config.scan` or
|
|
106
|
+
* `config.auto` needs it and it is not installed (an optional peer of this package), with the
|
|
107
|
+
* error of the import as its `cause`. A module that an installed `@markuplint/pretenders`
|
|
108
|
+
* needs being missing is thrown as it is.
|
|
74
109
|
*/
|
|
75
110
|
export function resolvePretenders(config, context, options) {
|
|
76
111
|
return createPretenderResolver(config).resolve(context, options);
|
|
@@ -118,7 +153,7 @@ async function resolveTargetIndependentPretenders(config) {
|
|
|
118
153
|
data.push(...config.data.map(pretender => rebaseHandWrittenPretenderFilePath(pretender, p => path.resolve(cwd, p))));
|
|
119
154
|
}
|
|
120
155
|
if (config.scan) {
|
|
121
|
-
const
|
|
156
|
+
const scan = await importFromPretenders('scan', 'The "pretenders.scan" option');
|
|
122
157
|
for (const entry of config.scan) {
|
|
123
158
|
const patterns = typeof entry.files === 'string' ? [entry.files] : [...entry.files];
|
|
124
159
|
// The patterns are absolute paths of the platform (`#pathResolve`), which Windows writes
|
|
@@ -143,7 +178,7 @@ async function resolveTargetIndependentPretenders(config) {
|
|
|
143
178
|
* `context`'s import graph to `data`, the target-independent entries.
|
|
144
179
|
*/
|
|
145
180
|
async function appendAutoPretenders(data, context, auto, dependencies) {
|
|
146
|
-
const
|
|
181
|
+
const autoScan = await importFromPretenders('autoScan', 'The "pretenders.auto" option');
|
|
147
182
|
const scanned = await autoScan(context.filePath, context.sourceCode, {
|
|
148
183
|
depth: auto === true ? undefined : auto.depth,
|
|
149
184
|
dependencies,
|
|
@@ -189,12 +224,14 @@ async function appendAutoPretenders(data, context, auto, dependencies) {
|
|
|
189
224
|
* @param options - Where to report the files the result depends on
|
|
190
225
|
* @returns The disambiguated pretender list, or `pretenders` itself (same
|
|
191
226
|
* reference) when there was nothing to resolve
|
|
227
|
+
* @throws An `Error` that says how to install `@markuplint/pretenders` when an entry has a
|
|
228
|
+
* `filePath` and it is not installed, with the error of the import as its `cause`
|
|
192
229
|
*/
|
|
193
230
|
export async function disambiguatePretendersForFile(filePath, sourceCode, pretenders, options) {
|
|
194
231
|
if (!pretenders.some(pretender => pretender.filePath)) {
|
|
195
232
|
return pretenders;
|
|
196
233
|
}
|
|
197
|
-
const
|
|
234
|
+
const disambiguatePretenders = await importFromPretenders('disambiguatePretenders', 'A pretender that has a `filePath`');
|
|
198
235
|
// The same base the scanned pretenders' paths were made absolute from (see `rebasePretenderFilePaths`).
|
|
199
236
|
return disambiguatePretenders(pretenders, {
|
|
200
237
|
filePath,
|
|
@@ -233,11 +270,24 @@ export function hasResolvableCollision(pretenders) {
|
|
|
233
270
|
* newly valid tsconfig `paths` alias keeps resolving as it did before the
|
|
234
271
|
* change for the rest of the process's lifetime. A no-op (not an error) when
|
|
235
272
|
* `@markuplint/pretenders` isn't installed, since nothing has populated its
|
|
236
|
-
* caches in that case either
|
|
273
|
+
* caches in that case either, and when it is installed but fails to load, which
|
|
274
|
+
* is logged under the `ml-fr` debug namespace: what needs it fails on its own, with the reason.
|
|
237
275
|
*
|
|
238
276
|
* @returns A promise that resolves once the caches have been cleared
|
|
239
277
|
*/
|
|
240
278
|
export async function invalidatePretenderResolutionCaches() {
|
|
241
|
-
|
|
242
|
-
|
|
279
|
+
let clearPretenderCaches;
|
|
280
|
+
try {
|
|
281
|
+
({ clearPretenderCaches } = await import('@markuplint/pretenders'));
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
// Clearing is best-effort, and what needs the package fails on its own with the reason when it is
|
|
285
|
+
// loaded, so a package that fails to load is logged and left to that. Tier-1-shaped errors are not
|
|
286
|
+
// told apart here either: a module the package needs runs as it loads, as in `generalImport`.
|
|
287
|
+
if (missingPackageOf(error) !== PRETENDERS_PACKAGE) {
|
|
288
|
+
log('Failed to load %s to clear its caches: %O', PRETENDERS_PACKAGE, error);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
clearPretenderCaches();
|
|
243
293
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/file-resolver",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"description": "The file resolver of markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,25 +31,33 @@
|
|
|
31
31
|
"clean": "tsc --build --clean"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
+
"@markuplint/pretenders": "5.5.0",
|
|
34
35
|
"@types/node": "24.19.2"
|
|
35
36
|
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@markuplint/pretenders": "^5.4.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"@markuplint/pretenders": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
36
45
|
"dependencies": {
|
|
37
|
-
"@markuplint/html-parser": "5.
|
|
38
|
-
"@markuplint/ml-ast": "5.
|
|
39
|
-
"@markuplint/ml-config": "5.
|
|
40
|
-
"@markuplint/ml-core": "5.
|
|
41
|
-
"@markuplint/ml-spec": "5.
|
|
42
|
-
"@markuplint/parser-utils": "5.
|
|
43
|
-
"@markuplint/
|
|
44
|
-
"@markuplint/
|
|
45
|
-
"@markuplint/shared": "5.4.0",
|
|
46
|
+
"@markuplint/html-parser": "5.5.0",
|
|
47
|
+
"@markuplint/ml-ast": "5.5.0",
|
|
48
|
+
"@markuplint/ml-config": "5.5.0",
|
|
49
|
+
"@markuplint/ml-core": "5.5.0",
|
|
50
|
+
"@markuplint/ml-spec": "5.5.0",
|
|
51
|
+
"@markuplint/parser-utils": "5.5.0",
|
|
52
|
+
"@markuplint/selector": "5.5.0",
|
|
53
|
+
"@markuplint/shared": "5.5.0",
|
|
46
54
|
"cosmiconfig": "^10.0.1",
|
|
47
55
|
"debug": "^4.4.3",
|
|
48
|
-
"glob": "^
|
|
49
|
-
"ignore": "^7.0.
|
|
56
|
+
"glob": "^14.0.3",
|
|
57
|
+
"ignore": "^7.0.14",
|
|
50
58
|
"import-meta-resolve": "^4.2.0",
|
|
51
59
|
"jsonc": "^3.0.0",
|
|
52
|
-
"minimatch": "^
|
|
60
|
+
"minimatch": "^11.0.4"
|
|
53
61
|
},
|
|
54
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "fedebcfd56eaec4642e68643b2f9e10750d810e1"
|
|
55
63
|
}
|