@markuplint/file-resolver 5.3.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 +24 -0
- package/lib/config-provider.d.ts +26 -4
- package/lib/config-provider.js +155 -54
- package/lib/cosmiconfig.d.ts +49 -0
- package/lib/cosmiconfig.js +455 -32
- 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/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/ml-file/ml-file.d.ts +4 -0
- package/lib/ml-file/ml-file.js +4 -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 +73 -13
- package/package.json +22 -14
|
@@ -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/index.d.ts
CHANGED
package/lib/index.js
CHANGED
package/lib/ml-file/ml-file.d.ts
CHANGED
|
@@ -12,6 +12,10 @@ export declare class MLFile {
|
|
|
12
12
|
*/
|
|
13
13
|
get nPath(): string;
|
|
14
14
|
get path(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the file's directory exists. Only ENOENT counts as missing; any other `stat` failure
|
|
17
|
+
* throws. Kept as part of the public `MLFile` type, though nothing in this repository calls it.
|
|
18
|
+
*/
|
|
15
19
|
dirExists(): Promise<boolean>;
|
|
16
20
|
getCode(): Promise<string>;
|
|
17
21
|
ignored(globPath: string | readonly string[]): boolean;
|
package/lib/ml-file/ml-file.js
CHANGED
|
@@ -51,6 +51,10 @@ export class MLFile {
|
|
|
51
51
|
get path() {
|
|
52
52
|
return path.resolve(this.#dirname, this.#basename);
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether the file's directory exists. Only ENOENT counts as missing; any other `stat` failure
|
|
56
|
+
* throws. Kept as part of the public `MLFile` type, though nothing in this repository calls it.
|
|
57
|
+
*/
|
|
54
58
|
async dirExists() {
|
|
55
59
|
return !!(await stat(this.#dirname));
|
|
56
60
|
}
|
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,11 +1,43 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { rebasePretenderFilePath } from '@markuplint/ml-config';
|
|
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';
|
|
7
|
+
import { normalizeForGlob } from './path-utils.js';
|
|
8
|
+
const PRETENDERS_PACKAGE = '@markuplint/pretenders';
|
|
5
9
|
/**
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `Pretender.filePath` is written by scanners (and by hand, in `data`) relative to
|
|
38
|
+
* their own base directory, which is meaningless once entries from
|
|
39
|
+
* files/scan results/configurations scattered across different directories are
|
|
40
|
+
* merged into one flat list.
|
|
9
41
|
* Rebasing to an absolute path immediately after each source is read is
|
|
10
42
|
* what lets {@link disambiguatePretendersForFile} later compare a
|
|
11
43
|
* pretender's origin file against the lint target's resolved imports.
|
|
@@ -20,7 +52,8 @@ function rebasePretenderFilePaths(pretenders, baseDir) {
|
|
|
20
52
|
* kept as well — the host's next config resolution creates a new resolver).
|
|
21
53
|
*
|
|
22
54
|
* @param config - The pretenders configuration section from the optimized config
|
|
23
|
-
* @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
|
|
24
57
|
*/
|
|
25
58
|
export function createPretenderResolver(config) {
|
|
26
59
|
// The promise is retained, not its result, so that overlapping first calls
|
|
@@ -69,6 +102,10 @@ export function createPretenderResolver(config) {
|
|
|
69
102
|
* @param context - The lint target's path/source, required only for `config.auto`
|
|
70
103
|
* @param options - Where to report the files the result depends on
|
|
71
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.
|
|
72
109
|
*/
|
|
73
110
|
export function resolvePretenders(config, context, options) {
|
|
74
111
|
return createPretenderResolver(config).resolve(context, options);
|
|
@@ -108,13 +145,21 @@ async function resolveTargetIndependentPretenders(config) {
|
|
|
108
145
|
}
|
|
109
146
|
}
|
|
110
147
|
if (config.data) {
|
|
111
|
-
data
|
|
148
|
+
// A configuration file's `data` is already absolute (resolved by the config provider
|
|
149
|
+
// from the file's own directory). What is relative here comes from a configuration with
|
|
150
|
+
// no file behind it (`MLEngine`'s `config` / `defaultConfig` option), which has no
|
|
151
|
+
// directory of its own, so it is read from `process.cwd()` — as its `scan` globs are.
|
|
152
|
+
const cwd = process.cwd();
|
|
153
|
+
data.push(...config.data.map(pretender => rebaseHandWrittenPretenderFilePath(pretender, p => path.resolve(cwd, p))));
|
|
112
154
|
}
|
|
113
155
|
if (config.scan) {
|
|
114
|
-
const
|
|
156
|
+
const scan = await importFromPretenders('scan', 'The "pretenders.scan" option');
|
|
115
157
|
for (const entry of config.scan) {
|
|
116
158
|
const patterns = typeof entry.files === 'string' ? [entry.files] : [...entry.files];
|
|
117
|
-
|
|
159
|
+
// The patterns are absolute paths of the platform (`#pathResolve`), which Windows writes
|
|
160
|
+
// with backslashes, and a glob reads one as an escape unless it is a slash. Only there:
|
|
161
|
+
// elsewhere a backslash is the escape a pattern may mean (`\[id\]`, a directory of brackets).
|
|
162
|
+
const globResults = await Promise.all(patterns.map(p => glob(process.platform === 'win32' ? normalizeForGlob(p) : p)));
|
|
118
163
|
const resolved = globResults.flat().map(f => path.resolve(f));
|
|
119
164
|
if (resolved.length > 0) {
|
|
120
165
|
const scanned = await scan(resolved, {
|
|
@@ -133,7 +178,7 @@ async function resolveTargetIndependentPretenders(config) {
|
|
|
133
178
|
* `context`'s import graph to `data`, the target-independent entries.
|
|
134
179
|
*/
|
|
135
180
|
async function appendAutoPretenders(data, context, auto, dependencies) {
|
|
136
|
-
const
|
|
181
|
+
const autoScan = await importFromPretenders('autoScan', 'The "pretenders.auto" option');
|
|
137
182
|
const scanned = await autoScan(context.filePath, context.sourceCode, {
|
|
138
183
|
depth: auto === true ? undefined : auto.depth,
|
|
139
184
|
dependencies,
|
|
@@ -179,12 +224,14 @@ async function appendAutoPretenders(data, context, auto, dependencies) {
|
|
|
179
224
|
* @param options - Where to report the files the result depends on
|
|
180
225
|
* @returns The disambiguated pretender list, or `pretenders` itself (same
|
|
181
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`
|
|
182
229
|
*/
|
|
183
230
|
export async function disambiguatePretendersForFile(filePath, sourceCode, pretenders, options) {
|
|
184
231
|
if (!pretenders.some(pretender => pretender.filePath)) {
|
|
185
232
|
return pretenders;
|
|
186
233
|
}
|
|
187
|
-
const
|
|
234
|
+
const disambiguatePretenders = await importFromPretenders('disambiguatePretenders', 'A pretender that has a `filePath`');
|
|
188
235
|
// The same base the scanned pretenders' paths were made absolute from (see `rebasePretenderFilePaths`).
|
|
189
236
|
return disambiguatePretenders(pretenders, {
|
|
190
237
|
filePath,
|
|
@@ -223,11 +270,24 @@ export function hasResolvableCollision(pretenders) {
|
|
|
223
270
|
* newly valid tsconfig `paths` alias keeps resolving as it did before the
|
|
224
271
|
* change for the rest of the process's lifetime. A no-op (not an error) when
|
|
225
272
|
* `@markuplint/pretenders` isn't installed, since nothing has populated its
|
|
226
|
-
* 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.
|
|
227
275
|
*
|
|
228
276
|
* @returns A promise that resolves once the caches have been cleared
|
|
229
277
|
*/
|
|
230
278
|
export async function invalidatePretenderResolutionCaches() {
|
|
231
|
-
|
|
232
|
-
|
|
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();
|
|
233
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.3.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
|
}
|