@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.
@@ -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
@@ -1,5 +1,6 @@
1
1
  export type { MLFile } from './ml-file/index.js';
2
2
  export * from './config-provider.js';
3
+ export { configLoadErrorFiles } from './cosmiconfig.js';
3
4
  export * from './resolve-files.js';
4
5
  export * from './resolve-parser.js';
5
6
  export * from './resolve-pretenders.js';
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './config-provider.js';
2
+ export { configLoadErrorFiles } from './cosmiconfig.js';
2
3
  export * from './resolve-files.js';
3
4
  export * from './resolve-parser.js';
4
5
  export * from './resolve-pretenders.js';
@@ -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;
@@ -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
  }
@@ -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>;
@@ -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 import(name);
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
@@ -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 absPath = path.resolve(dir, pathname);
13
- return bang + absPath;
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
- * `Pretender.filePath` is written by scanners relative to their own base
7
- * directory, which is meaningless once entries from files/scan results
8
- * scattered across different directories are merged into one flat list.
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.push(...config.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 { scan } = await import('@markuplint/pretenders');
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
- const globResults = await Promise.all(patterns.map(p => glob(p)));
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 { autoScan } = await import('@markuplint/pretenders');
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 { disambiguatePretenders } = await import('@markuplint/pretenders');
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
- const pretendersMod = await import('@markuplint/pretenders').catch(() => null);
232
- pretendersMod?.clearPretenderCaches();
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.0",
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.3.0",
38
- "@markuplint/ml-ast": "5.3.0",
39
- "@markuplint/ml-config": "5.3.0",
40
- "@markuplint/ml-core": "5.3.0",
41
- "@markuplint/ml-spec": "5.3.0",
42
- "@markuplint/parser-utils": "5.3.0",
43
- "@markuplint/pretenders": "5.3.0",
44
- "@markuplint/selector": "5.3.0",
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": "^13.0.6",
49
- "ignore": "^7.0.12",
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": "^10.2.6"
60
+ "minimatch": "^11.0.4"
53
61
  },
54
- "gitHead": "f587cf0206c8426e57369a48d6d5fb22306eafde"
62
+ "gitHead": "fedebcfd56eaec4642e68643b2f9e10750d810e1"
55
63
  }