@williamthorsen/nmr 0.48.0 → 0.50.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/.meta/changelog.json +218 -0
- package/.readyup/kits/default.js +1527 -1460
- package/.readyup/manifest.json +96 -11
- package/CHANGELOG.md +65 -0
- package/README.md +19 -8
- package/agents/guidance/rulebooks/nmr.md +2 -2
- package/dist/esm/check-cache-build-output.d.ts +24 -0
- package/dist/esm/check-cache-build-output.js +49 -0
- package/dist/esm/check-cache-delegation.d.ts +27 -0
- package/dist/esm/check-cache-delegation.js +92 -0
- package/dist/esm/check-cache-entries.d.ts +109 -0
- package/dist/esm/check-cache-entries.js +87 -0
- package/dist/esm/check-cache-gate.d.ts +64 -0
- package/dist/esm/check-cache-gate.js +226 -0
- package/dist/esm/check-cache-key.d.ts +47 -0
- package/dist/esm/check-cache-key.js +77 -0
- package/dist/esm/check-cache-record.d.ts +69 -0
- package/dist/esm/check-cache-record.js +167 -0
- package/dist/esm/check-cache.d.ts +8 -176
- package/dist/esm/check-cache.js +9 -214
- package/dist/esm/child-env.d.ts +57 -0
- package/dist/esm/child-env.js +59 -0
- package/dist/esm/child-process.d.ts +55 -0
- package/dist/esm/child-process.js +112 -0
- package/dist/esm/commands/build.d.ts +0 -16
- package/dist/esm/commands/build.js +9 -214
- package/dist/esm/commands/clean.js +3 -2
- package/dist/esm/commands/compiler.d.ts +18 -0
- package/dist/esm/commands/compiler.js +49 -0
- package/dist/esm/commands/fmt.d.ts +0 -39
- package/dist/esm/commands/fmt.js +4 -81
- package/dist/esm/commands/format-targets.d.ts +39 -0
- package/dist/esm/commands/format-targets.js +83 -0
- package/dist/esm/commands/rewrite-specifiers.d.ts +17 -0
- package/dist/esm/commands/rewrite-specifiers.js +122 -0
- package/dist/esm/commands/tsconfig-chain.d.ts +8 -0
- package/dist/esm/commands/tsconfig-chain.js +63 -0
- package/dist/esm/config-check-cache.d.ts +17 -0
- package/dist/esm/config-check-cache.js +63 -0
- package/dist/esm/config-validation.d.ts +20 -0
- package/dist/esm/config-validation.js +236 -0
- package/dist/esm/config.js +4 -286
- package/dist/esm/crossing-diagnostics.d.ts +39 -0
- package/dist/esm/crossing-diagnostics.js +132 -0
- package/dist/esm/default-scripts.js +1 -1
- package/dist/esm/delegate-host.d.ts +43 -0
- package/dist/esm/delegate-host.js +191 -0
- package/dist/esm/delegation-target.d.ts +52 -0
- package/dist/esm/delegation-target.js +71 -0
- package/dist/esm/fan-out.d.ts +57 -0
- package/dist/esm/fan-out.js +146 -0
- package/dist/esm/glyphs.d.ts +1 -1
- package/dist/esm/help.js +10 -3
- package/dist/esm/helpers/deriveExcerpt.d.ts +9 -0
- package/dist/esm/helpers/deriveExcerpt.js +1 -1
- package/dist/esm/helpers/renderQuotedList.d.ts +2 -0
- package/dist/esm/helpers/renderQuotedList.js +9 -0
- package/dist/esm/hooks.d.ts +17 -0
- package/dist/esm/hooks.js +23 -0
- package/dist/esm/invocation-args.d.ts +40 -0
- package/dist/esm/invocation-args.js +178 -0
- package/dist/esm/invocation.d.ts +67 -0
- package/dist/esm/invocation.js +3 -0
- package/dist/esm/machine-lock.d.ts +2 -0
- package/dist/esm/machine-lock.js +1 -1
- package/dist/esm/output-channel.d.ts +23 -0
- package/dist/esm/output-channel.js +14 -0
- package/dist/esm/pass-manifest.d.ts +53 -0
- package/dist/esm/pass-manifest.js +89 -0
- package/dist/esm/presentation.d.ts +47 -0
- package/dist/esm/presentation.js +32 -0
- package/dist/esm/recording.d.ts +1 -1
- package/dist/esm/recording.js +2 -2
- package/dist/esm/remote-host-session.d.ts +62 -0
- package/dist/esm/remote-host-session.js +277 -0
- package/dist/esm/remote-probe-host.d.ts +22 -0
- package/dist/esm/remote-probe-host.js +50 -0
- package/dist/esm/remote-probe.d.ts +63 -0
- package/dist/esm/remote-probe.js +88 -0
- package/dist/esm/remote-run.d.ts +45 -65
- package/dist/esm/remote-run.js +128 -162
- package/dist/esm/remote-session.d.ts +40 -0
- package/dist/esm/remote-session.js +151 -0
- package/dist/esm/remote-snapshot.d.ts +21 -0
- package/dist/esm/remote-snapshot.js +80 -0
- package/dist/esm/replay-assembly.d.ts +2 -2
- package/dist/esm/replay-assembly.js +1 -1
- package/dist/esm/run-chain.d.ts +21 -0
- package/dist/esm/run-chain.js +108 -0
- package/dist/esm/runCli.d.ts +1 -9
- package/dist/esm/runCli.js +39 -1002
- package/dist/esm/runner.d.ts +1 -54
- package/dist/esm/runner.js +2 -125
- package/dist/esm/shell-syntax.d.ts +30 -0
- package/dist/esm/shell-syntax.js +120 -0
- package/dist/esm/steps.d.ts +0 -2
- package/dist/esm/steps.js +3 -119
- package/dist/esm/types.d.ts +5 -0
- package/dist/esm/user-config.d.ts +34 -0
- package/dist/esm/user-config.js +93 -0
- package/dist/esm/verbosity.d.ts +6 -0
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +1 -1
- package/dist/esm/verdict.js +1 -1
- package/docs/check-cache.md +28 -12
- package/docs/delegate.md +103 -17
- package/docs/reporting.md +6 -1
- package/docs/scripts.md +2 -1
- package/package.json +4 -4
- /package/dist/esm/{prettier-default-config.d.ts → prettier-default.config.d.ts} +0 -0
- /package/dist/esm/{prettier-default-config.js → prettier-default.config.js} +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { hasErrnoCode } from '@williamthorsen/nmr-core';
|
|
2
|
+
const MINIMUM_TYPESCRIPT_MAJOR = 5;
|
|
3
|
+
const MINIMUM_TYPESCRIPT_MINOR = 7;
|
|
4
|
+
const UNSUPPORTED_TYPESCRIPT_MAJOR = 7;
|
|
5
|
+
const SUPPORTED_TYPESCRIPT_RANGE = `>=${MINIMUM_TYPESCRIPT_MAJOR}.${MINIMUM_TYPESCRIPT_MINOR} <${UNSUPPORTED_TYPESCRIPT_MAJOR}`;
|
|
6
|
+
const INSTALL_TYPESCRIPT_GUIDANCE = "Install a version in that range as the 'typescript' peer dependency.";
|
|
7
|
+
let ts;
|
|
8
|
+
export function assertSupportedTypeScript() {
|
|
9
|
+
const compiler = getCompiler();
|
|
10
|
+
const versionMajorMinor = compiler.versionMajorMinor;
|
|
11
|
+
const version = compiler.version;
|
|
12
|
+
const [majorPart, minorPart] = typeof versionMajorMinor === 'string' ? versionMajorMinor.split('.', 2) : [];
|
|
13
|
+
const major = majorPart === undefined ? 0 : Number(majorPart);
|
|
14
|
+
const minor = minorPart === undefined ? 0 : Number(minorPart);
|
|
15
|
+
const tooOld = major < MINIMUM_TYPESCRIPT_MAJOR || (major === MINIMUM_TYPESCRIPT_MAJOR && minor < MINIMUM_TYPESCRIPT_MINOR);
|
|
16
|
+
if (tooOld || major >= UNSUPPORTED_TYPESCRIPT_MAJOR) {
|
|
17
|
+
throw new Error(`nmr-compile requires TypeScript ${SUPPORTED_TYPESCRIPT_RANGE}, but found ` +
|
|
18
|
+
`${typeof version === 'string' ? version : 'a version without the compiler API'}. ${INSTALL_TYPESCRIPT_GUIDANCE}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function formatDiagnostics(diagnostics) {
|
|
22
|
+
const compiler = getCompiler();
|
|
23
|
+
return compiler.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
24
|
+
getCurrentDirectory: () => process.cwd(),
|
|
25
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
26
|
+
getNewLine: () => compiler.sys.newLine,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function getCompiler() {
|
|
30
|
+
if (ts === undefined) {
|
|
31
|
+
throw new Error('nmr-compile: the compiler is not loaded; call loadCompiler first.');
|
|
32
|
+
}
|
|
33
|
+
return ts;
|
|
34
|
+
}
|
|
35
|
+
export async function loadCompiler(loadTypeScript = importTypeScript) {
|
|
36
|
+
try {
|
|
37
|
+
ts = await loadTypeScript();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (hasErrnoCode(error, 'ERR_MODULE_NOT_FOUND') && error.message.includes("'typescript'")) {
|
|
41
|
+
throw new Error(`nmr-compile requires TypeScript ${SUPPORTED_TYPESCRIPT_RANGE}, but 'typescript' could not be resolved. ` +
|
|
42
|
+
INSTALL_TYPESCRIPT_GUIDANCE, { cause: error });
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function importTypeScript() {
|
|
48
|
+
return await import('typescript');
|
|
49
|
+
}
|
|
@@ -1,27 +1,4 @@
|
|
|
1
1
|
export type FormatMode = 'check' | 'write';
|
|
2
|
-
export interface FormatTargets {
|
|
3
|
-
/** Listed paths that Prettier cannot be handed, such as symlinks and submodule gitlinks. */
|
|
4
|
-
excludedFiles: string[];
|
|
5
|
-
/** Paths relative to the working directory, as `git ls-files` emits them. */
|
|
6
|
-
files: string[];
|
|
7
|
-
/** Absolute paths to every `.prettierignore` governing the repository, root-most first. */
|
|
8
|
-
ignorePaths: string[];
|
|
9
|
-
/** The repository's top-level directory, as git reports it. */
|
|
10
|
-
repositoryRoot: string;
|
|
11
|
-
/** Listed paths that this process is denied access to, each with the errno code that its stat raised. */
|
|
12
|
-
unreadableFiles: UnreadableFile[];
|
|
13
|
-
}
|
|
14
|
-
export interface UnreadableFile {
|
|
15
|
-
file: string;
|
|
16
|
-
code: string;
|
|
17
|
-
}
|
|
18
|
-
export type ResolveTargetsResult = {
|
|
19
|
-
ok: true;
|
|
20
|
-
targets: FormatTargets;
|
|
21
|
-
} | {
|
|
22
|
-
ok: false;
|
|
23
|
-
error: string;
|
|
24
|
-
};
|
|
25
2
|
/**
|
|
26
3
|
* Formats or checks the files that git reports for `cwd`, and returns the exit code to report. A repository
|
|
27
4
|
* without a Prettier config of its own is formatted with the house config. The run uses Prettier's content-keyed
|
|
@@ -32,22 +9,6 @@ export type ResolveTargetsResult = {
|
|
|
32
9
|
* success over an empty selection.
|
|
33
10
|
*/
|
|
34
11
|
export declare function runFmt(argv: string[], cwd?: string): Promise<number>;
|
|
35
|
-
/**
|
|
36
|
-
* Resolves what Prettier should format and which ignore files govern it, using git rather than Prettier's
|
|
37
|
-
* own discovery. Prettier reads `.gitignore` and `.prettierignore` from the working directory only, so a
|
|
38
|
-
* pattern in `packages/<pkg>/.gitignore` is invisible to a root-level run; git knows the whole hierarchy,
|
|
39
|
-
* along with `.git/info/exclude` and `core.excludesFile`, which Prettier cannot read under any flag.
|
|
40
|
-
*
|
|
41
|
-
* Because file selection is anchored at `cwd` and ignore discovery at the repository root, scoping follows
|
|
42
|
-
* `cwd` while the ignore rules applied to a given file are the same from every directory.
|
|
43
|
-
*
|
|
44
|
-
* `pathspecs` narrow the selection and are passed to git verbatim: They have git pathspec semantics rather
|
|
45
|
-
* than shell glob semantics.
|
|
46
|
-
*
|
|
47
|
-
* A listed path that Prettier cannot be handed is set aside in `excludedFiles`, and one that this process is denied
|
|
48
|
-
* access to in `unreadableFiles`; any other stat failure fails the resolution.
|
|
49
|
-
*/
|
|
50
|
-
export declare function resolveFormatTargets(cwd: string, pathspecs?: string[]): ResolveTargetsResult;
|
|
51
12
|
/** @internal */
|
|
52
13
|
export interface RunPrettierOptions {
|
|
53
14
|
cliPath: string;
|
package/dist/esm/commands/fmt.js
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import {
|
|
8
|
+
import { reportError } from '@williamthorsen/nmr-core';
|
|
9
9
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
10
10
|
import { isObject } from "../helpers/type-guards.js";
|
|
11
11
|
import { prepareFmtCache } from "./fmt-cache.js";
|
|
12
|
-
|
|
13
|
-
const LIST_FILES_ARGS = ['ls-files', '-z', '--cached', '--others', '--exclude-standard'];
|
|
14
|
-
const IGNORE_FILE_PATHSPEC = `*${IGNORE_FILENAME}`;
|
|
15
|
-
const UNREADABLE_CODES = ['EACCES', 'EPERM'];
|
|
12
|
+
import { resolveFormatTargets } from "./format-targets.js";
|
|
16
13
|
const PRETTIER_PACKAGE = 'prettier';
|
|
17
|
-
const DEFAULT_CONFIG_PATH = fileURLToPath(new URL(`../prettier-default
|
|
14
|
+
const DEFAULT_CONFIG_PATH = fileURLToPath(new URL(`../prettier-default.config${path.extname(fileURLToPath(import.meta.url))}`, import.meta.url));
|
|
18
15
|
const ARGUMENT_BUDGET_BYTES = 100_000;
|
|
19
16
|
const USAGE = 'Usage: nmr-fmt (--check | --write) [--no-cache] [pathspec...]';
|
|
20
17
|
const MODE_ARGS = {
|
|
@@ -78,63 +75,6 @@ export async function runFmt(argv, cwd = process.cwd()) {
|
|
|
78
75
|
cwd,
|
|
79
76
|
});
|
|
80
77
|
}
|
|
81
|
-
export function resolveFormatTargets(cwd, pathspecs = []) {
|
|
82
|
-
const toplevel = runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
83
|
-
if (!toplevel.ok)
|
|
84
|
-
return toplevel;
|
|
85
|
-
const repositoryRoot = toplevel.stdout.trim();
|
|
86
|
-
const listedFiles = runGit([...LIST_FILES_ARGS, '--', ...pathspecs], cwd);
|
|
87
|
-
if (!listedFiles.ok)
|
|
88
|
-
return listedFiles;
|
|
89
|
-
const ignoreFiles = runGit([...LIST_FILES_ARGS, '--', IGNORE_FILE_PATHSPEC], repositoryRoot);
|
|
90
|
-
if (!ignoreFiles.ok)
|
|
91
|
-
return ignoreFiles;
|
|
92
|
-
const excludedFiles = [];
|
|
93
|
-
const files = [];
|
|
94
|
-
const unreadableFiles = [];
|
|
95
|
-
const listedPaths = dedupe(splitNulSeparated(listedFiles.stdout));
|
|
96
|
-
for (const file of listedPaths) {
|
|
97
|
-
const status = classifyListedFile(cwd, file);
|
|
98
|
-
switch (status.kind) {
|
|
99
|
-
case 'excluded':
|
|
100
|
-
excludedFiles.push(file);
|
|
101
|
-
break;
|
|
102
|
-
case 'failed':
|
|
103
|
-
return { ok: false, error: status.error };
|
|
104
|
-
case 'formattable':
|
|
105
|
-
files.push(file);
|
|
106
|
-
break;
|
|
107
|
-
case 'unreadable':
|
|
108
|
-
unreadableFiles.push({ file, code: status.code });
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
const discoveredIgnorePaths = splitNulSeparated(ignoreFiles.stdout)
|
|
113
|
-
.filter((file) => path.basename(file) === IGNORE_FILENAME)
|
|
114
|
-
.map((file) => path.resolve(repositoryRoot, file));
|
|
115
|
-
return {
|
|
116
|
-
ok: true,
|
|
117
|
-
targets: {
|
|
118
|
-
excludedFiles: excludedFiles.toSorted(),
|
|
119
|
-
files: files.toSorted(),
|
|
120
|
-
ignorePaths: dedupe([path.join(repositoryRoot, IGNORE_FILENAME), ...discoveredIgnorePaths]),
|
|
121
|
-
repositoryRoot,
|
|
122
|
-
unreadableFiles,
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
function classifyListedFile(cwd, file) {
|
|
127
|
-
try {
|
|
128
|
-
const stats = lstatSync(path.resolve(cwd, file), { throwIfNoEntry: false });
|
|
129
|
-
return { kind: stats?.isFile() === true ? 'formattable' : 'excluded' };
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
const code = UNREADABLE_CODES.find((candidate) => hasErrnoCode(error, candidate));
|
|
133
|
-
if (code !== undefined)
|
|
134
|
-
return { kind: 'unreadable', code };
|
|
135
|
-
return { kind: 'failed', error: `could not stat ${file}: ${describeError(error)}` };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
78
|
function parseFmtArgs(argv) {
|
|
139
79
|
let mode;
|
|
140
80
|
let shouldBypassCache = false;
|
|
@@ -248,20 +188,3 @@ function resolveFrom(parent, specifier) {
|
|
|
248
188
|
return undefined;
|
|
249
189
|
}
|
|
250
190
|
}
|
|
251
|
-
function runGit(args, cwd) {
|
|
252
|
-
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
253
|
-
if (result.error) {
|
|
254
|
-
return { ok: false, error: result.error.message };
|
|
255
|
-
}
|
|
256
|
-
if (result.status !== 0) {
|
|
257
|
-
const stderr = result.stderr.trim();
|
|
258
|
-
return { ok: false, error: stderr || `\`git ${args.join(' ')}\` failed with exit code ${result.status}.` };
|
|
259
|
-
}
|
|
260
|
-
return { ok: true, stdout: result.stdout };
|
|
261
|
-
}
|
|
262
|
-
function splitNulSeparated(output) {
|
|
263
|
-
return output.split('\0').filter((entry) => entry !== '');
|
|
264
|
-
}
|
|
265
|
-
function dedupe(items) {
|
|
266
|
-
return [...new Set(items)];
|
|
267
|
-
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface FormatTargets {
|
|
2
|
+
/** Listed paths that Prettier cannot be handed, such as symlinks and submodule gitlinks. */
|
|
3
|
+
excludedFiles: string[];
|
|
4
|
+
/** Paths relative to the working directory, as `git ls-files` emits them. */
|
|
5
|
+
files: string[];
|
|
6
|
+
/** Absolute paths to every `.prettierignore` governing the repository, root-most first. */
|
|
7
|
+
ignorePaths: string[];
|
|
8
|
+
/** The repository's top-level directory, as git reports it. */
|
|
9
|
+
repositoryRoot: string;
|
|
10
|
+
/** Listed paths that this process is denied access to, each with the errno code that its stat raised. */
|
|
11
|
+
unreadableFiles: UnreadableFile[];
|
|
12
|
+
}
|
|
13
|
+
export interface UnreadableFile {
|
|
14
|
+
file: string;
|
|
15
|
+
code: string;
|
|
16
|
+
}
|
|
17
|
+
export type ResolveTargetsResult = {
|
|
18
|
+
ok: true;
|
|
19
|
+
targets: FormatTargets;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Resolves what Prettier should format and which ignore files govern it, using git rather than Prettier's
|
|
26
|
+
* own discovery. Prettier reads `.gitignore` and `.prettierignore` from the working directory only, so a
|
|
27
|
+
* pattern in `packages/<pkg>/.gitignore` is invisible to a root-level run; git knows the whole hierarchy,
|
|
28
|
+
* along with `.git/info/exclude` and `core.excludesFile`, which Prettier cannot read under any flag.
|
|
29
|
+
*
|
|
30
|
+
* Because file selection is anchored at `cwd` and ignore discovery at the repository root, scoping follows
|
|
31
|
+
* `cwd` while the ignore rules applied to a given file are the same from every directory.
|
|
32
|
+
*
|
|
33
|
+
* `pathspecs` narrow the selection and are passed to git verbatim: They have git pathspec semantics rather
|
|
34
|
+
* than shell glob semantics.
|
|
35
|
+
*
|
|
36
|
+
* A listed path that Prettier cannot be handed is set aside in `excludedFiles`, and one that this process is denied
|
|
37
|
+
* access to in `unreadableFiles`; any other stat failure fails the resolution.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveFormatTargets(cwd: string, pathspecs?: string[]): ResolveTargetsResult;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { lstatSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { hasErrnoCode } from '@williamthorsen/nmr-core';
|
|
5
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
6
|
+
const IGNORE_FILENAME = '.prettierignore';
|
|
7
|
+
const LIST_FILES_ARGS = ['ls-files', '-z', '--cached', '--others', '--exclude-standard'];
|
|
8
|
+
const IGNORE_FILE_PATHSPEC = `*${IGNORE_FILENAME}`;
|
|
9
|
+
const UNREADABLE_CODES = ['EACCES', 'EPERM'];
|
|
10
|
+
export function resolveFormatTargets(cwd, pathspecs = []) {
|
|
11
|
+
const toplevel = runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
12
|
+
if (!toplevel.ok)
|
|
13
|
+
return toplevel;
|
|
14
|
+
const repositoryRoot = toplevel.stdout.trim();
|
|
15
|
+
const listedFiles = runGit([...LIST_FILES_ARGS, '--', ...pathspecs], cwd);
|
|
16
|
+
if (!listedFiles.ok)
|
|
17
|
+
return listedFiles;
|
|
18
|
+
const ignoreFiles = runGit([...LIST_FILES_ARGS, '--', IGNORE_FILE_PATHSPEC], repositoryRoot);
|
|
19
|
+
if (!ignoreFiles.ok)
|
|
20
|
+
return ignoreFiles;
|
|
21
|
+
const excludedFiles = [];
|
|
22
|
+
const files = [];
|
|
23
|
+
const unreadableFiles = [];
|
|
24
|
+
const listedPaths = dedupe(splitNulSeparated(listedFiles.stdout));
|
|
25
|
+
for (const file of listedPaths) {
|
|
26
|
+
const status = classifyListedFile(cwd, file);
|
|
27
|
+
switch (status.kind) {
|
|
28
|
+
case 'excluded':
|
|
29
|
+
excludedFiles.push(file);
|
|
30
|
+
break;
|
|
31
|
+
case 'failed':
|
|
32
|
+
return { ok: false, error: status.error };
|
|
33
|
+
case 'formattable':
|
|
34
|
+
files.push(file);
|
|
35
|
+
break;
|
|
36
|
+
case 'unreadable':
|
|
37
|
+
unreadableFiles.push({ file, code: status.code });
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const discoveredIgnorePaths = splitNulSeparated(ignoreFiles.stdout)
|
|
42
|
+
.filter((file) => path.basename(file) === IGNORE_FILENAME)
|
|
43
|
+
.map((file) => path.resolve(repositoryRoot, file));
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
targets: {
|
|
47
|
+
excludedFiles: excludedFiles.toSorted(),
|
|
48
|
+
files: files.toSorted(),
|
|
49
|
+
ignorePaths: dedupe([path.join(repositoryRoot, IGNORE_FILENAME), ...discoveredIgnorePaths]),
|
|
50
|
+
repositoryRoot,
|
|
51
|
+
unreadableFiles,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function classifyListedFile(cwd, file) {
|
|
56
|
+
try {
|
|
57
|
+
const stats = lstatSync(path.resolve(cwd, file), { throwIfNoEntry: false });
|
|
58
|
+
return { kind: stats?.isFile() === true ? 'formattable' : 'excluded' };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const code = UNREADABLE_CODES.find((candidate) => hasErrnoCode(error, candidate));
|
|
62
|
+
if (code !== undefined)
|
|
63
|
+
return { kind: 'unreadable', code };
|
|
64
|
+
return { kind: 'failed', error: `could not stat ${file}: ${describeError(error)}` };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function runGit(args, cwd) {
|
|
68
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
69
|
+
if (result.error) {
|
|
70
|
+
return { ok: false, error: result.error.message };
|
|
71
|
+
}
|
|
72
|
+
if (result.status !== 0) {
|
|
73
|
+
const stderr = result.stderr.trim();
|
|
74
|
+
return { ok: false, error: stderr || `\`git ${args.join(' ')}\` failed with exit code ${result.status}.` };
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, stdout: result.stdout };
|
|
77
|
+
}
|
|
78
|
+
function splitNulSeparated(output) {
|
|
79
|
+
return output.split('\0').filter((entry) => entry !== '');
|
|
80
|
+
}
|
|
81
|
+
function dedupe(items) {
|
|
82
|
+
return [...new Set(items)];
|
|
83
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type * as TypeScript from 'typescript';
|
|
2
|
+
/** Reports whether a specifier starts with `./` or `../`. */
|
|
3
|
+
export declare function isRelativeSpecifier(specifier: string): boolean;
|
|
4
|
+
/** Reports whether `child` lies strictly inside `parent`. */
|
|
5
|
+
export declare function isWithin(parent: string, child: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Rewrites module specifiers in a single emitted `.js` or `.d.ts` file's text: relative imports ending
|
|
8
|
+
* in a TypeScript extension become their `.js` equivalent, and tsconfig `paths` aliases resolve to
|
|
9
|
+
* runnable relative `.js` specifiers. Parsing the text means only real import/export specifiers are
|
|
10
|
+
* touched -- text inside strings and comments is never altered. Returns the text unchanged when nothing
|
|
11
|
+
* needs rewriting.
|
|
12
|
+
*
|
|
13
|
+
* `outputFile` names where the emit will be published, not where the text is now: `mapOutputToSource`
|
|
14
|
+
* reconstructs the originating source file by swapping the `outDir` prefix, and aliases resolve from
|
|
15
|
+
* that source location. A staging path here would resolve them from the wrong directory.
|
|
16
|
+
*/
|
|
17
|
+
export declare function rewriteSpecifiers(outputFile: string, text: string, compilerOptions: TypeScript.CompilerOptions, sourceRoot: string): string;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { getCompiler } from "./compiler.js";
|
|
3
|
+
const TS_EXTENSIONS = ['.ts', '.tsx'];
|
|
4
|
+
const JS_EXTENSION = '.js';
|
|
5
|
+
export function isRelativeSpecifier(specifier) {
|
|
6
|
+
return specifier.startsWith('./') || specifier.startsWith('../');
|
|
7
|
+
}
|
|
8
|
+
export function isWithin(parent, child) {
|
|
9
|
+
const relativePath = path.relative(parent, child);
|
|
10
|
+
return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
|
11
|
+
}
|
|
12
|
+
export function rewriteSpecifiers(outputFile, text, compilerOptions, sourceRoot) {
|
|
13
|
+
if (!isRewritableOutput(outputFile)) {
|
|
14
|
+
return text;
|
|
15
|
+
}
|
|
16
|
+
const ts = getCompiler();
|
|
17
|
+
const sourceFile = ts.createSourceFile(outputFile, text, ts.ScriptTarget.Latest, false, resolveScriptKind(outputFile));
|
|
18
|
+
const aliasPrefixes = collectAliasPrefixes(compilerOptions);
|
|
19
|
+
const sourceContainingFile = mapOutputToSource(outputFile, compilerOptions, sourceRoot);
|
|
20
|
+
const edits = [];
|
|
21
|
+
visitModuleSpecifiers(sourceFile, (literal) => {
|
|
22
|
+
const replacement = resolveSpecifierReplacement(literal.text, sourceContainingFile, compilerOptions, sourceRoot, aliasPrefixes);
|
|
23
|
+
if (replacement === undefined) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const startOffset = literal.getStart(sourceFile);
|
|
27
|
+
const quote = text[startOffset] ?? '"';
|
|
28
|
+
edits.push({ startOffset, endOffset: literal.getEnd(), text: `${quote}${replacement}${quote}` });
|
|
29
|
+
});
|
|
30
|
+
if (edits.length === 0) {
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
const orderedEdits = edits.toSorted((a, b) => b.startOffset - a.startOffset);
|
|
34
|
+
let updatedText = text;
|
|
35
|
+
for (const edit of orderedEdits) {
|
|
36
|
+
updatedText = updatedText.slice(0, edit.startOffset) + edit.text + updatedText.slice(edit.endOffset);
|
|
37
|
+
}
|
|
38
|
+
return updatedText;
|
|
39
|
+
}
|
|
40
|
+
function buildRelativeSpecifier(fromDir, targetFile) {
|
|
41
|
+
const relativePath = path.relative(fromDir, targetFile).split(path.sep).join('/');
|
|
42
|
+
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
|
|
43
|
+
}
|
|
44
|
+
function collectAliasPrefixes(compilerOptions) {
|
|
45
|
+
if (compilerOptions.paths === undefined) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return Object.keys(compilerOptions.paths).map((pattern) => pattern.replace(/\*$/, ''));
|
|
49
|
+
}
|
|
50
|
+
function getModuleSpecifier(node) {
|
|
51
|
+
const ts = getCompiler();
|
|
52
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier !== undefined) {
|
|
53
|
+
return ts.isStringLiteralLike(node.moduleSpecifier) ? node.moduleSpecifier : undefined;
|
|
54
|
+
}
|
|
55
|
+
if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
|
|
56
|
+
return ts.isStringLiteralLike(node.moduleReference.expression) ? node.moduleReference.expression : undefined;
|
|
57
|
+
}
|
|
58
|
+
if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
|
59
|
+
return ts.isStringLiteralLike(node.argument.literal) ? node.argument.literal : undefined;
|
|
60
|
+
}
|
|
61
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
62
|
+
const [first] = node.arguments;
|
|
63
|
+
return first !== undefined && ts.isStringLiteralLike(first) ? first : undefined;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
function isRewritableOutput(file) {
|
|
68
|
+
return file.endsWith('.d.ts') || file.endsWith('.js');
|
|
69
|
+
}
|
|
70
|
+
function mapOutputToSource(outputFile, compilerOptions, sourceRoot) {
|
|
71
|
+
const outDir = compilerOptions.outDir ?? path.dirname(outputFile);
|
|
72
|
+
const relativeFromOut = path.relative(outDir, outputFile);
|
|
73
|
+
const withoutExtension = relativeFromOut.replace(/\.d\.ts$|\.js$/, '');
|
|
74
|
+
return path.join(sourceRoot, `${withoutExtension}.ts`);
|
|
75
|
+
}
|
|
76
|
+
function resolveScriptKind(file) {
|
|
77
|
+
const ts = getCompiler();
|
|
78
|
+
return file.endsWith('.d.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS;
|
|
79
|
+
}
|
|
80
|
+
function resolveSpecifierReplacement(specifier, sourceContainingFile, compilerOptions, sourceRoot, aliasPrefixes) {
|
|
81
|
+
if (isRelativeSpecifier(specifier)) {
|
|
82
|
+
const rewrittenSpecifier = swapTypeScriptExtension(specifier);
|
|
83
|
+
return rewrittenSpecifier === specifier ? undefined : rewrittenSpecifier;
|
|
84
|
+
}
|
|
85
|
+
if (aliasPrefixes.every((prefix) => !(specifier === prefix || specifier.startsWith(prefix)))) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const ts = getCompiler();
|
|
89
|
+
const resolvedModule = ts.resolveModuleName(specifier, sourceContainingFile, compilerOptions, ts.sys).resolvedModule;
|
|
90
|
+
if (!resolvedModule) {
|
|
91
|
+
throw new Error(`nmr-compile: could not resolve aliased import '${specifier}' from ${sourceContainingFile}. ` +
|
|
92
|
+
`Verify the tsconfig 'paths' mapping and that the target file exists.`);
|
|
93
|
+
}
|
|
94
|
+
if (!isWithin(sourceRoot, resolvedModule.resolvedFileName)) {
|
|
95
|
+
const { paths: _paths, baseUrl: _baseUrl, rootDirs: _rootDirs, ...nodeResolutionOptions } = compilerOptions;
|
|
96
|
+
const bareResolvedModule = ts.resolveModuleName(specifier, sourceContainingFile, nodeResolutionOptions, ts.sys).resolvedModule;
|
|
97
|
+
if (bareResolvedModule) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`nmr-compile: aliased import '${specifier}' from ${sourceContainingFile} resolves to ` +
|
|
101
|
+
`${resolvedModule.resolvedFileName}, outside the package source root ${sourceRoot}, and does not resolve ` +
|
|
102
|
+
`the way Node will at runtime, which ignores tsconfig 'paths', 'baseUrl', and 'rootDirs'. The ` +
|
|
103
|
+
`emitted specifier would fail at runtime; re-anchor the alias inside the package.`);
|
|
104
|
+
}
|
|
105
|
+
const relativeSpecifier = buildRelativeSpecifier(path.dirname(sourceContainingFile), resolvedModule.resolvedFileName);
|
|
106
|
+
return swapTypeScriptExtension(relativeSpecifier);
|
|
107
|
+
}
|
|
108
|
+
function swapTypeScriptExtension(specifier) {
|
|
109
|
+
const extension = TS_EXTENSIONS.find((candidate) => specifier.endsWith(candidate));
|
|
110
|
+
return extension === undefined ? specifier : `${specifier.slice(0, -extension.length)}${JS_EXTENSION}`;
|
|
111
|
+
}
|
|
112
|
+
function visitModuleSpecifiers(sourceFile, visit) {
|
|
113
|
+
const ts = getCompiler();
|
|
114
|
+
function walk(node) {
|
|
115
|
+
const specifier = getModuleSpecifier(node);
|
|
116
|
+
if (specifier !== undefined) {
|
|
117
|
+
visit(specifier);
|
|
118
|
+
}
|
|
119
|
+
ts.forEachChild(node, walk);
|
|
120
|
+
}
|
|
121
|
+
walk(sourceFile);
|
|
122
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a package's full tsconfig `extends` chain, returning every config file in it (the leaf `tsconfig.json` and
|
|
3
|
+
* each base that it transitively extends, up to the repo root) as paths relative to `packageDir`.
|
|
4
|
+
* Emit is driven by the fully-resolved compiler options, so the base configs must be in the cache's hashed input
|
|
5
|
+
* set; otherwise a change to a base config would not bust the cache and stale output could be released. The
|
|
6
|
+
* relative paths give `computeBuildHash` a location-independent path string to fold into the digest.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveTsconfigChain(packageDir: string, configFileName?: string): string[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { formatDiagnostics, getCompiler } from "./compiler.js";
|
|
3
|
+
import { isRelativeSpecifier } from "./rewrite-specifiers.js";
|
|
4
|
+
export function resolveTsconfigChain(packageDir, configFileName = 'tsconfig.json') {
|
|
5
|
+
const ts = getCompiler();
|
|
6
|
+
const resolvedChain = [];
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
function walk(configPath) {
|
|
9
|
+
const normalizedPath = path.resolve(configPath);
|
|
10
|
+
if (seen.has(normalizedPath)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
seen.add(normalizedPath);
|
|
14
|
+
resolvedChain.push(normalizedPath);
|
|
15
|
+
const configFile = ts.readConfigFile(normalizedPath, (fileName) => ts.sys.readFile(fileName));
|
|
16
|
+
if (configFile.error) {
|
|
17
|
+
throw new Error(`nmr-compile: failed to read ${normalizedPath}.\n${formatDiagnostics([configFile.error])}`);
|
|
18
|
+
}
|
|
19
|
+
for (const entry of normalizeExtendsField(configFile.config)) {
|
|
20
|
+
walk(resolveExtendsTarget(entry, normalizedPath));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
walk(path.resolve(packageDir, configFileName));
|
|
24
|
+
return resolvedChain.map((absolute) => path.relative(packageDir, absolute));
|
|
25
|
+
}
|
|
26
|
+
function normalizeExtendsField(config) {
|
|
27
|
+
if (config === null || typeof config !== 'object' || !('extends' in config)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const extendsField = config.extends;
|
|
31
|
+
if (typeof extendsField === 'string') {
|
|
32
|
+
return [extendsField];
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(extendsField)) {
|
|
35
|
+
return extendsField.filter((entry) => typeof entry === 'string');
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
function resolveExtendsTarget(extendsEntry, fromConfigPath) {
|
|
40
|
+
const ts = getCompiler();
|
|
41
|
+
if (isRelativeSpecifier(extendsEntry) || path.isAbsolute(extendsEntry)) {
|
|
42
|
+
const base = path.resolve(path.dirname(fromConfigPath), extendsEntry);
|
|
43
|
+
if (ts.sys.fileExists(base)) {
|
|
44
|
+
return base;
|
|
45
|
+
}
|
|
46
|
+
const withJsonExtension = `${base}.json`;
|
|
47
|
+
if (ts.sys.fileExists(withJsonExtension)) {
|
|
48
|
+
return withJsonExtension;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not exist.`);
|
|
51
|
+
}
|
|
52
|
+
const resolvedPath = resolvePackageSpecifier(extendsEntry, fromConfigPath) ??
|
|
53
|
+
resolvePackageSpecifier(`${extendsEntry}/tsconfig.json`, fromConfigPath);
|
|
54
|
+
if (resolvedPath === undefined) {
|
|
55
|
+
throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not resolve to a file.`);
|
|
56
|
+
}
|
|
57
|
+
return resolvedPath;
|
|
58
|
+
}
|
|
59
|
+
function resolvePackageSpecifier(specifier, fromConfigPath) {
|
|
60
|
+
const ts = getCompiler();
|
|
61
|
+
const { resolvedModule } = ts.resolveModuleName(specifier, fromConfigPath, { moduleResolution: ts.ModuleResolutionKind.NodeNext, resolveJsonModule: true }, ts.sys);
|
|
62
|
+
return resolvedModule?.resolvedFileName;
|
|
63
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { NmrConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Rejects a `checkCache` name that does not resolve to any command. Both fields are read by name alone, so a
|
|
4
|
+
* misspelt entry is inert: The command that it meant to name goes on running, indistinguishable from one that
|
|
5
|
+
* cannot be cached at all.
|
|
6
|
+
*
|
|
7
|
+
* Because the test is resolvability rather than membership of the cacheable set, an `excludeCommands` entry
|
|
8
|
+
* written against a name that a later release moves out of the defaults remains valid.
|
|
9
|
+
*
|
|
10
|
+
* Reads nothing while every name resolves in the merged registries, and reads a package's own `scripts` only
|
|
11
|
+
* once a name is missing from them: Those are a resolution tier of their own; a name declared only there is
|
|
12
|
+
* valid.
|
|
13
|
+
*
|
|
14
|
+
* Does nothing outside a monorepo root, where `checkCache` is a key that this tier does not honor. `assertTierKeys`
|
|
15
|
+
* reports that, and of the two messages it is the one that names the real mistake.
|
|
16
|
+
*/
|
|
17
|
+
export declare function assertResolvableCheckCacheCommands(config: NmrConfig, configPath: string, baseDir: string): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { findClosestName } from "./helpers/findClosestName.js";
|
|
2
|
+
import { isHookName } from "./helpers/hook-name.js";
|
|
3
|
+
import { buildRootRegistry, buildWorkspaceRegistry, readPackageJsonScripts } from "./resolver.js";
|
|
4
|
+
import { UserError } from "./UserError.js";
|
|
5
|
+
import { getWorkspacePackageDirs, isMonorepoRoot } from "./workspace.js";
|
|
6
|
+
export function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
|
|
7
|
+
const { checkCache } = config;
|
|
8
|
+
if (checkCache === undefined || !isMonorepoRoot(baseDir)) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const fields = ['excludeCommands', 'extraCommands'];
|
|
12
|
+
const entries = fields.flatMap((field) => (checkCache[field] ?? []).map((command) => ({ command, field })));
|
|
13
|
+
const hook = entries.find(({ command }) => isHookName(command));
|
|
14
|
+
if (hook !== undefined) {
|
|
15
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${hook.field}\` names the hook \`${hook.command}\`, ` +
|
|
16
|
+
'which is never gated on its own: It runs as part of the chain of the command that it wraps.');
|
|
17
|
+
}
|
|
18
|
+
const registeredNames = new Set([
|
|
19
|
+
...Object.keys(buildRootRegistry(config, baseDir)),
|
|
20
|
+
...Object.keys(buildWorkspaceRegistry(config)),
|
|
21
|
+
]);
|
|
22
|
+
const missingEntries = entries.filter(({ command }) => !registeredNames.has(command));
|
|
23
|
+
if (missingEntries.length === 0) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const declaredNames = readDeclaredScriptNames(baseDir);
|
|
27
|
+
const unresolvableEntry = missingEntries.find(({ command }) => !declaredNames.has(command));
|
|
28
|
+
if (unresolvableEntry === undefined) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const closestName = findClosestName(unresolvableEntry.command, [...registeredNames, ...declaredNames]);
|
|
32
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${unresolvableEntry.field}\` does not name any command: ` +
|
|
33
|
+
`\`${unresolvableEntry.command}\`.${closestName === undefined ? '' : ` Did you mean \`${closestName}\`?`}`);
|
|
34
|
+
}
|
|
35
|
+
function readDeclaredScriptNames(monorepoRoot) {
|
|
36
|
+
let packageDirs;
|
|
37
|
+
try {
|
|
38
|
+
packageDirs = getWorkspacePackageDirs(monorepoRoot);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
packageDirs = [];
|
|
42
|
+
}
|
|
43
|
+
const names = new Set();
|
|
44
|
+
const dirs = [monorepoRoot, ...packageDirs];
|
|
45
|
+
for (const dir of dirs) {
|
|
46
|
+
const scripts = readPackageScripts(dir);
|
|
47
|
+
for (const name of Object.keys(scripts)) {
|
|
48
|
+
names.add(name);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return names;
|
|
52
|
+
}
|
|
53
|
+
function readPackageScripts(packageDir) {
|
|
54
|
+
try {
|
|
55
|
+
return readPackageJsonScripts(packageDir) ?? {};
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof UserError) {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NmrConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The keys that each tier honors, and where the other tier's belong. Every recognized key belongs to exactly one
|
|
4
|
+
* tier, and `RECOGNIZED_KEYS` is their union.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CONFIG_TIERS: Record<'root' | 'workspace', ConfigTier>;
|
|
7
|
+
export interface ConfigTier {
|
|
8
|
+
/** Names the tier in an error message. */
|
|
9
|
+
label: string;
|
|
10
|
+
honoredKeys: string[];
|
|
11
|
+
/** Names where a key that this tier does not honor belongs. */
|
|
12
|
+
elsewhere: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Throws on any key outside the recognized set, naming the offenders and the file. An unrecognized key is a
|
|
16
|
+
* typo or a stale spelling, and the setting that it appears to make is one that nothing reads.
|
|
17
|
+
*/
|
|
18
|
+
export declare function assertRecognizedKeys(value: object, recognizedKeys: string[], configPath: string, prefix?: string): void;
|
|
19
|
+
/** Validates that a loaded value conforms to the expected `NmrConfig` shape. */
|
|
20
|
+
export declare function validateConfig(value: unknown, configPath: string, baseDir: string): NmrConfig;
|