@williamthorsen/nmr 0.38.0 → 0.40.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 +212 -4
- package/.readyup/kits/default.js +124 -104
- package/.readyup/manifest.json +12 -7
- package/CHANGELOG.md +108 -0
- package/README.md +41 -47
- package/agents/README.md +8 -8
- package/agents/guidance/rulebooks/nmr.md +8 -8
- package/bin/ensure-prepublish-hooks.js +1 -1
- package/bin/nmr-clean.js +1 -1
- package/bin/nmr-compile.js +1 -1
- package/bin/nmr-fmt.js +1 -1
- package/bin/nmr-report-catalog.js +1 -1
- package/bin/nmr-report-overrides.js +1 -1
- package/bin/nmr-taze.js +1 -1
- package/bin/nmr.js +1 -1
- package/dist/esm/UserError.d.ts +1 -2
- package/dist/esm/check-cache.d.ts +61 -63
- package/dist/esm/check-cache.js +24 -24
- package/dist/esm/cli-ensure-prepublish-hooks.js +4 -4
- package/dist/esm/commands/build-output.d.ts +37 -38
- package/dist/esm/commands/build-output.js +9 -9
- package/dist/esm/commands/build.d.ts +13 -14
- package/dist/esm/commands/build.js +67 -65
- package/dist/esm/commands/clean.d.ts +6 -6
- package/dist/esm/commands/clean.js +11 -11
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +7 -7
- package/dist/esm/commands/ensure-prepublish-hooks.js +41 -41
- package/dist/esm/commands/fmt.d.ts +8 -8
- package/dist/esm/commands/fmt.js +33 -33
- package/dist/esm/commands/report-catalog.d.ts +5 -7
- package/dist/esm/commands/report-catalog.js +10 -8
- package/dist/esm/commands/report-overrides.d.ts +0 -2
- package/dist/esm/commands/report-overrides.js +8 -8
- package/dist/esm/commands/taze.d.ts +9 -8
- package/dist/esm/config.d.ts +5 -5
- package/dist/esm/config.js +27 -27
- package/dist/esm/context.d.ts +1 -2
- package/dist/esm/context.js +8 -8
- package/dist/esm/default-scripts.d.ts +5 -6
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/git-ignored-paths.d.ts +9 -0
- package/dist/esm/git-ignored-paths.js +10 -0
- package/dist/esm/glyphs.d.ts +3 -3
- package/dist/esm/help.d.ts +4 -4
- package/dist/esm/help.js +21 -21
- package/dist/esm/helpers/clampToBytes.d.ts +4 -5
- package/dist/esm/helpers/clampToBytes.js +3 -3
- package/dist/esm/helpers/config-path.d.ts +0 -2
- package/dist/esm/helpers/createBoundedBuffer.d.ts +3 -3
- package/dist/esm/helpers/createBoundedBuffer.js +2 -2
- package/dist/esm/helpers/deriveExcerpt.d.ts +4 -5
- package/dist/esm/helpers/duration.d.ts +10 -11
- package/dist/esm/helpers/duration.js +7 -7
- package/dist/esm/helpers/filter-selection.d.ts +19 -19
- package/dist/esm/helpers/filter-selection.js +12 -12
- package/dist/esm/helpers/findClosestName.d.ts +1 -1
- package/dist/esm/helpers/findClosestName.js +15 -15
- package/dist/esm/helpers/hook-name.d.ts +2 -5
- package/dist/esm/helpers/package-json.d.ts +11 -10
- package/dist/esm/helpers/package-json.js +26 -25
- package/dist/esm/helpers/readStringValues.d.ts +2 -3
- package/dist/esm/helpers/reportClosing.d.ts +8 -9
- package/dist/esm/helpers/transcript.d.ts +15 -15
- package/dist/esm/helpers/transcript.js +14 -14
- package/dist/esm/helpers/type-guards.d.ts +1 -0
- package/dist/esm/output-style.d.ts +12 -13
- package/dist/esm/prettier.d.ts +2 -2
- package/dist/esm/prettier.js +5 -5
- package/dist/esm/recording.d.ts +25 -20
- package/dist/esm/recording.js +28 -21
- package/dist/esm/replay-assembly.d.ts +10 -10
- package/dist/esm/report-format.d.ts +15 -20
- package/dist/esm/report-format.js +7 -7
- package/dist/esm/reportCliFailure.d.ts +2 -3
- package/dist/esm/resolve-scripts.d.ts +4 -5
- package/dist/esm/resolveBinStyles.d.ts +4 -5
- package/dist/esm/resolveBinStyles.js +3 -3
- package/dist/esm/resolver.d.ts +16 -20
- package/dist/esm/resolver.js +15 -15
- package/dist/esm/runCli.d.ts +6 -6
- package/dist/esm/runCli.js +261 -195
- package/dist/esm/runner.d.ts +20 -20
- package/dist/esm/runner.js +18 -18
- package/dist/esm/steps.d.ts +51 -38
- package/dist/esm/steps.js +32 -27
- package/dist/esm/taze.d.ts +1 -1
- package/dist/esm/tests.d.ts +23 -18
- package/dist/esm/tests.js +21 -9
- package/dist/esm/tiers.d.ts +28 -24
- package/dist/esm/tiers.js +13 -9
- package/dist/esm/types.d.ts +5 -5
- package/dist/esm/verbosity.d.ts +23 -21
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +39 -32
- package/dist/esm/verdict.js +63 -50
- package/dist/esm/vitest-source-resolution.d.ts +11 -11
- package/dist/esm/vitest-source-resolution.js +49 -47
- package/dist/esm/vitest.d.ts +27 -26
- package/dist/esm/vitest.js +90 -33
- package/dist/esm/workspace.d.ts +6 -22
- package/dist/esm/workspace.js +9 -67
- package/docs/check-cache.md +57 -57
- package/docs/prettier.md +10 -10
- package/docs/reporting.md +48 -44
- package/docs/scripts.md +29 -29
- package/docs/upgrades.md +18 -18
- package/docs/utilities.md +13 -13
- package/docs/vitest.md +68 -66
- package/docs/workspace.md +27 -4
- package/package.json +6 -6
- package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
- package/dist/esm/helpers/workspace-patterns.js +0 -29
package/dist/esm/commands/fmt.js
CHANGED
|
@@ -18,21 +18,21 @@ const MODE_ARGS = {
|
|
|
18
18
|
write: ['--list-different', '--write'],
|
|
19
19
|
};
|
|
20
20
|
export function runFmt(argv, cwd = process.cwd()) {
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
23
|
-
reportError(`nmr-fmt: ${
|
|
21
|
+
const parsedArgs = parseFmtArgs(argv);
|
|
22
|
+
if (!parsedArgs.ok) {
|
|
23
|
+
reportError(`nmr-fmt: ${parsedArgs.error}`);
|
|
24
24
|
process.stderr.write(`${USAGE}\n`);
|
|
25
25
|
return 1;
|
|
26
26
|
}
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
reportError(`nmr-fmt: ${
|
|
27
|
+
const resolvedTargets = resolveFormatTargets(cwd, parsedArgs.pathspecs);
|
|
28
|
+
if (!resolvedTargets.ok) {
|
|
29
|
+
reportError(`nmr-fmt: ${resolvedTargets.error}`);
|
|
30
30
|
return 1;
|
|
31
31
|
}
|
|
32
|
-
const { files, ignorePaths } =
|
|
32
|
+
const { files, ignorePaths } = resolvedTargets.targets;
|
|
33
33
|
if (files.length === 0) {
|
|
34
|
-
if (
|
|
35
|
-
reportError(`nmr-fmt:
|
|
34
|
+
if (parsedArgs.pathspecs.length > 0) {
|
|
35
|
+
reportError(`nmr-fmt: the pathspecs did not match any formattable file: ${parsedArgs.pathspecs.join(', ')}`);
|
|
36
36
|
return 1;
|
|
37
37
|
}
|
|
38
38
|
return 0;
|
|
@@ -42,29 +42,29 @@ export function runFmt(argv, cwd = process.cwd()) {
|
|
|
42
42
|
reportError(`nmr-fmt: ${cli.error}`);
|
|
43
43
|
return 1;
|
|
44
44
|
}
|
|
45
|
-
return runPrettier({ cliPath: cli.cliPath, mode:
|
|
45
|
+
return runPrettier({ cliPath: cli.cliPath, mode: parsedArgs.mode, files, ignorePaths, cwd });
|
|
46
46
|
}
|
|
47
47
|
export function resolveFormatTargets(cwd, pathspecs = []) {
|
|
48
48
|
const toplevel = runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
49
49
|
if (!toplevel.ok)
|
|
50
50
|
return toplevel;
|
|
51
51
|
const repositoryRoot = toplevel.stdout.trim();
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
54
|
-
return
|
|
52
|
+
const listedFiles = runGit([...LIST_FILES_ARGS, '--', ...pathspecs], cwd);
|
|
53
|
+
if (!listedFiles.ok)
|
|
54
|
+
return listedFiles;
|
|
55
55
|
const ignoreFiles = runGit([...LIST_FILES_ARGS, '--', IGNORE_FILE_PATHSPEC], repositoryRoot);
|
|
56
56
|
if (!ignoreFiles.ok)
|
|
57
57
|
return ignoreFiles;
|
|
58
|
-
const
|
|
58
|
+
const discoveredIgnorePaths = splitNulSeparated(ignoreFiles.stdout)
|
|
59
59
|
.filter((file) => path.basename(file) === IGNORE_FILENAME)
|
|
60
60
|
.map((file) => path.resolve(repositoryRoot, file));
|
|
61
61
|
return {
|
|
62
62
|
ok: true,
|
|
63
63
|
targets: {
|
|
64
|
-
files: dedupe(splitNulSeparated(
|
|
64
|
+
files: dedupe(splitNulSeparated(listedFiles.stdout))
|
|
65
65
|
.filter((file) => isFormattableFile(cwd, file))
|
|
66
66
|
.toSorted(),
|
|
67
|
-
ignorePaths: dedupe([path.join(repositoryRoot, IGNORE_FILENAME), ...
|
|
67
|
+
ignorePaths: dedupe([path.join(repositoryRoot, IGNORE_FILENAME), ...discoveredIgnorePaths]),
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
70
|
}
|
|
@@ -76,11 +76,11 @@ function parseFmtArgs(argv) {
|
|
|
76
76
|
const pathspecs = [];
|
|
77
77
|
for (const arg of argv) {
|
|
78
78
|
if (arg === '--check' || arg === '--write') {
|
|
79
|
-
const
|
|
80
|
-
if (mode !== undefined && mode !==
|
|
79
|
+
const requestedMode = arg === '--check' ? 'check' : 'write';
|
|
80
|
+
if (mode !== undefined && mode !== requestedMode) {
|
|
81
81
|
return { ok: false, error: 'Pass --check or --write, not both.' };
|
|
82
82
|
}
|
|
83
|
-
mode =
|
|
83
|
+
mode = requestedMode;
|
|
84
84
|
continue;
|
|
85
85
|
}
|
|
86
86
|
if (arg.startsWith('-')) {
|
|
@@ -94,7 +94,7 @@ function parseFmtArgs(argv) {
|
|
|
94
94
|
return { ok: true, mode, pathspecs };
|
|
95
95
|
}
|
|
96
96
|
function resolvePrettierCli() {
|
|
97
|
-
const
|
|
97
|
+
const missingResult = {
|
|
98
98
|
ok: false,
|
|
99
99
|
error: `\`${PRETTIER_PACKAGE}\` (${PRETTIER_RANGE}) could not be resolved. Install it in this repository.`,
|
|
100
100
|
};
|
|
@@ -103,14 +103,14 @@ function resolvePrettierCli() {
|
|
|
103
103
|
manifestPath = createRequire(import.meta.url).resolve(`${PRETTIER_PACKAGE}/package.json`);
|
|
104
104
|
}
|
|
105
105
|
catch {
|
|
106
|
-
return
|
|
106
|
+
return missingResult;
|
|
107
107
|
}
|
|
108
108
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
109
109
|
if (!isObject(manifest))
|
|
110
|
-
return
|
|
110
|
+
return missingResult;
|
|
111
111
|
const bin = isObject(manifest['bin']) ? manifest['bin'][PRETTIER_PACKAGE] : manifest['bin'];
|
|
112
112
|
if (typeof bin !== 'string')
|
|
113
|
-
return
|
|
113
|
+
return missingResult;
|
|
114
114
|
return { ok: true, cliPath: path.resolve(path.dirname(manifestPath), bin) };
|
|
115
115
|
}
|
|
116
116
|
export function runPrettier(options) {
|
|
@@ -121,7 +121,7 @@ export function runPrettier(options) {
|
|
|
121
121
|
...ignorePaths.flatMap((ignorePath) => ['--ignore-path', ignorePath]),
|
|
122
122
|
...MODE_ARGS[mode],
|
|
123
123
|
];
|
|
124
|
-
let
|
|
124
|
+
let firstFailureStatus = 0;
|
|
125
125
|
for (const batch of batchWithinBudget(files, budgetBytes)) {
|
|
126
126
|
const result = spawnSync(process.execPath, [...prettierArgs, ...batch], { cwd, stdio: 'inherit' });
|
|
127
127
|
if (result.error) {
|
|
@@ -129,25 +129,25 @@ export function runPrettier(options) {
|
|
|
129
129
|
return 1;
|
|
130
130
|
}
|
|
131
131
|
const status = result.status ?? 1;
|
|
132
|
-
if (status !== 0 &&
|
|
133
|
-
|
|
132
|
+
if (status !== 0 && firstFailureStatus === 0) {
|
|
133
|
+
firstFailureStatus = status;
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
-
return
|
|
136
|
+
return firstFailureStatus;
|
|
137
137
|
}
|
|
138
|
-
function batchWithinBudget(files,
|
|
138
|
+
function batchWithinBudget(files, budgetBytes) {
|
|
139
139
|
const batches = [];
|
|
140
140
|
let batch = [];
|
|
141
|
-
let
|
|
141
|
+
let sizeBytes = 0;
|
|
142
142
|
for (const file of files) {
|
|
143
|
-
const
|
|
144
|
-
if (batch.length > 0 &&
|
|
143
|
+
const costBytes = Buffer.byteLength(file) + 1;
|
|
144
|
+
if (batch.length > 0 && sizeBytes + costBytes > budgetBytes) {
|
|
145
145
|
batches.push(batch);
|
|
146
146
|
batch = [];
|
|
147
|
-
|
|
147
|
+
sizeBytes = 0;
|
|
148
148
|
}
|
|
149
149
|
batch.push(file);
|
|
150
|
-
|
|
150
|
+
sizeBytes += costBytes;
|
|
151
151
|
}
|
|
152
152
|
if (batch.length > 0)
|
|
153
153
|
batches.push(batch);
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
2
2
|
/**
|
|
3
|
-
* Reports the catalogued dependencies a package-scoped upgrade pass
|
|
3
|
+
* Reports the catalogued dependencies that a package-scoped upgrade pass does not read.
|
|
4
4
|
*
|
|
5
|
-
* The upgrade tool reads `pnpm-workspace.yaml` only when
|
|
6
|
-
* inside a package drops every dependency a catalog
|
|
7
|
-
* catalogued
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Runs ahead of the report produced by the workspace `upgrade` script.
|
|
5
|
+
* The upgrade tool reads `pnpm-workspace.yaml` only when the file is in the working directory, so a pass run
|
|
6
|
+
* inside a package drops every dependency declared by a catalog -- and the tool reports a package whose
|
|
7
|
+
* dependencies are all catalogued as up to date. This report names them, and the root from which a covering
|
|
8
|
+
* pass runs, to keep the reader from taking that result to mean that every dependency is up to date.
|
|
11
9
|
*/
|
|
12
10
|
export declare function reportCatalog(cwd: string, style: OutputStyle): void;
|
|
@@ -15,7 +15,7 @@ export function reportCatalog(cwd, style) {
|
|
|
15
15
|
if (dependencies.length === 0) {
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
|
-
console.warn(formatGlyphLine(NMR_GLYPHS, style, 'catalog', 'WARN: A package-scoped upgrade does not read the catalogs these come
|
|
18
|
+
console.warn(formatGlyphLine(NMR_GLYPHS, style, 'catalog', 'WARN: A package-scoped upgrade does not read the catalogs from which these come:'));
|
|
19
19
|
for (const { name, specifier } of dependencies) {
|
|
20
20
|
console.warn(`- ${name} → ${specifier}`);
|
|
21
21
|
}
|
|
@@ -27,15 +27,17 @@ function describeCatalog(count, monorepoRoot) {
|
|
|
27
27
|
return `${subject} unread. Run \`nmr upgrade\` from ${monorepoRoot} to include ${object}.`;
|
|
28
28
|
}
|
|
29
29
|
function findCataloguedDependencies(packageDir) {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
30
|
+
const packageJson = readPackageJson(packageDir);
|
|
31
|
+
const foundDependencies = new Map();
|
|
32
32
|
for (const field of DEPENDENCY_FIELDS) {
|
|
33
|
-
const
|
|
34
|
-
for (const [name, specifier] of
|
|
35
|
-
if (specifier.startsWith(CATALOG_PROTOCOL) && !
|
|
36
|
-
|
|
33
|
+
const declaredEntries = Object.entries(packageJson[field] ?? {});
|
|
34
|
+
for (const [name, specifier] of declaredEntries) {
|
|
35
|
+
if (specifier.startsWith(CATALOG_PROTOCOL) && !foundDependencies.has(name)) {
|
|
36
|
+
foundDependencies.set(name, specifier);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
return [...
|
|
40
|
+
return [...foundDependencies]
|
|
41
|
+
.map(([name, specifier]) => ({ name, specifier }))
|
|
42
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
41
43
|
}
|
|
@@ -2,7 +2,5 @@ import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
|
2
2
|
/**
|
|
3
3
|
* Reports the pnpm dependency overrides declared in the monorepo root's `pnpm-workspace.yaml`, and rejects a
|
|
4
4
|
* `pnpm.overrides` block left behind in the root `package.json`.
|
|
5
|
-
*
|
|
6
|
-
* Runs ahead of the report produced by the root `upgrade` script.
|
|
7
5
|
*/
|
|
8
6
|
export declare function reportOverrides(monorepoRoot: string, style: OutputStyle): void;
|
|
@@ -5,13 +5,13 @@ import { reportClosing } from "../helpers/reportClosing.js";
|
|
|
5
5
|
import { UserError } from "../UserError.js";
|
|
6
6
|
import { readWorkspaceOverrides } from "../workspace.js";
|
|
7
7
|
export function reportOverrides(monorepoRoot, style) {
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
8
|
+
const declaredOverrides = listEntries(readWorkspaceOverrides(monorepoRoot));
|
|
9
|
+
if (declaredOverrides.length > 0) {
|
|
10
10
|
console.warn(formatGlyphLine(NMR_GLYPHS, style, 'overrides', 'WARN: pnpm overrides are active:'));
|
|
11
|
-
for (const [name, version] of
|
|
11
|
+
for (const [name, version] of declaredOverrides) {
|
|
12
12
|
console.warn(`- ${name} → ${version}`);
|
|
13
13
|
}
|
|
14
|
-
reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'overrides', describeOverrides(
|
|
14
|
+
reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'overrides', describeOverrides(declaredOverrides.length)), console.warn);
|
|
15
15
|
}
|
|
16
16
|
rejectLegacyOverrides(monorepoRoot);
|
|
17
17
|
}
|
|
@@ -24,13 +24,13 @@ function listEntries(overrides) {
|
|
|
24
24
|
return Object.entries(overrides ?? {}).toSorted(([a], [b]) => a.localeCompare(b));
|
|
25
25
|
}
|
|
26
26
|
function rejectLegacyOverrides(monorepoRoot) {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
27
|
+
const legacyOverrides = listEntries(getPnpmOverrides(readPackageJson(monorepoRoot)));
|
|
28
|
+
if (legacyOverrides.length === 0) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
throw new UserError([
|
|
32
|
-
'pnpm 11
|
|
33
|
-
...
|
|
32
|
+
'pnpm 11 does not read `pnpm.overrides` from package.json, so these pin nothing while an upgrade run with `--write` goes on rewriting them:',
|
|
33
|
+
...legacyOverrides.map(([name, version]) => `- ${name} → ${String(version)}`),
|
|
34
34
|
'Move them to the `overrides` block in pnpm-workspace.yaml, quoting each version, or run `pnpx codemod run pnpm-v10-to-v11`.',
|
|
35
35
|
].join('\n'));
|
|
36
36
|
}
|
|
@@ -15,15 +15,16 @@ export interface RunTazeOptions {
|
|
|
15
15
|
* Runs taze from nmr's own dependency tree, forwarding `args`, and returns its exit code.
|
|
16
16
|
*
|
|
17
17
|
* Consumers depend on nmr, not on taze, so taze is transitive and its bin is absent from the consumer's
|
|
18
|
-
* root `node_modules/.bin`. This launcher
|
|
19
|
-
* consumer root, and nmr resolves taze from the tree it does control.
|
|
18
|
+
* root `node_modules/.bin`. This launcher runs taze without that bin: pnpm links nmr's own bins into the
|
|
19
|
+
* consumer root, and nmr resolves taze from the tree that it does control.
|
|
20
20
|
*
|
|
21
|
-
* A request timeout is the one argument added, and only
|
|
22
|
-
* it: an invocation's trailing arguments are appended to the resolved command string,
|
|
23
|
-
* `upgrade` script would be doubled by `nmr upgrade --request-timeout ...`, and taze's parser
|
|
24
|
-
* into an array it reads as a near-zero deadline; a `taze.config.ts` setting
|
|
25
|
-
* defaults overwrite.
|
|
26
|
-
* the script registry, visible in `nmr` help output and overridable
|
|
21
|
+
* A request timeout is the one argument added, and only when `args` does not set one. Neither other site can
|
|
22
|
+
* declare it: Because an invocation's trailing arguments are appended to the resolved command string, the same
|
|
23
|
+
* flag in the `upgrade` script would be doubled by `nmr upgrade --request-timeout ...`, and taze's parser
|
|
24
|
+
* collects the pair into an array that it reads as a near-zero deadline; a `taze.config.ts` setting goes into
|
|
25
|
+
* the object that taze's own CLI defaults overwrite. Because every other argument is forwarded untouched,
|
|
26
|
+
* invocation policy (`--recursive`) stays in the script registry, visible in `nmr` help output and overridable
|
|
27
|
+
* per repo, and upgrade policy stays in `taze.ts`.
|
|
27
28
|
*/
|
|
28
29
|
export declare function runTaze(args: string[], options?: RunTazeOptions): number;
|
|
29
30
|
/** Resolves the absolute path of taze's CLI entry from nmr's own dependency tree. */
|
package/dist/esm/config.d.ts
CHANGED
|
@@ -9,15 +9,15 @@ export declare function loadConfig(baseDir: string): Promise<NmrConfig>;
|
|
|
9
9
|
* Loads the monorepo-root `.config/nmr.config.ts`, the tier that feeds script resolution and `devBin`.
|
|
10
10
|
* Returns an empty config if the file doesn't exist.
|
|
11
11
|
*
|
|
12
|
-
* Throws on a recognized key this tier does not honor. `build` governs one package's compile, which reads the
|
|
12
|
+
* Throws on a recognized key that this tier does not honor. `build` governs one package's compile, which reads the
|
|
13
13
|
* package's own config alone, so a `build` accepted here would apply its patterns nowhere.
|
|
14
14
|
*/
|
|
15
15
|
export declare function loadRootConfig(monorepoRoot: string): Promise<NmrConfig>;
|
|
16
16
|
/**
|
|
17
|
-
* Loads a single package's `.config/nmr.config.ts`, the tier `nmr-compile` reads from its working
|
|
18
|
-
* Returns an empty config if the file doesn't exist.
|
|
17
|
+
* Loads a single package's `.config/nmr.config.ts`, the tier that `nmr-compile` reads from its working
|
|
18
|
+
* directory. Returns an empty config if the file doesn't exist.
|
|
19
19
|
*
|
|
20
|
-
* Throws on a recognized key this tier does not honor. Dropping one would leave a package building on
|
|
21
|
-
* settings its own config file appears to change, which nothing in the build's output would reveal.
|
|
20
|
+
* Throws on a recognized key that this tier does not honor. Dropping one would leave a package building on
|
|
21
|
+
* settings that its own config file appears to change, which nothing in the build's output would reveal.
|
|
22
22
|
*/
|
|
23
23
|
export declare function loadWorkspaceConfig(packageDir: string): Promise<NmrConfig>;
|
package/dist/esm/config.js
CHANGED
|
@@ -79,7 +79,7 @@ function assertValidElements(scripts, fieldName, configPath) {
|
|
|
79
79
|
const token = findUnexpressibleToken(element);
|
|
80
80
|
if (token === undefined)
|
|
81
81
|
continue;
|
|
82
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}.${command}\` element \`${element}\`
|
|
82
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}.${command}\` element \`${element}\` contains ` +
|
|
83
83
|
`\`${token}\`, which is neither a command name nor an nmr flag. Name it as a script of its own and ` +
|
|
84
84
|
`reference that name here.`);
|
|
85
85
|
}
|
|
@@ -113,12 +113,12 @@ function validateCheckCacheField(value, configPath) {
|
|
|
113
113
|
}
|
|
114
114
|
assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, 'checkCache.');
|
|
115
115
|
const config = {};
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
if (typeof
|
|
116
|
+
const enabledValue = checkCache['enabled'];
|
|
117
|
+
if (enabledValue !== undefined) {
|
|
118
|
+
if (typeof enabledValue !== 'boolean') {
|
|
119
119
|
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
|
|
120
120
|
}
|
|
121
|
-
config.enabled =
|
|
121
|
+
config.enabled = enabledValue;
|
|
122
122
|
}
|
|
123
123
|
for (const field of ['excludeCommands', 'extraCommands']) {
|
|
124
124
|
const commands = checkCache[field];
|
|
@@ -142,24 +142,24 @@ function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
|
|
|
142
142
|
const hook = entries.find(({ command }) => isHookName(command));
|
|
143
143
|
if (hook !== undefined) {
|
|
144
144
|
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${hook.field}\` names the hook \`${hook.command}\`, ` +
|
|
145
|
-
'which is never gated on its own:
|
|
145
|
+
'which is never gated on its own: It runs as part of the chain of the command that it wraps.');
|
|
146
146
|
}
|
|
147
|
-
const
|
|
147
|
+
const registeredNames = new Set([
|
|
148
148
|
...Object.keys(buildRootRegistry(config)),
|
|
149
149
|
...Object.keys(buildWorkspaceRegistry(config)),
|
|
150
150
|
]);
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
151
|
+
const missingEntries = entries.filter(({ command }) => !registeredNames.has(command));
|
|
152
|
+
if (missingEntries.length === 0) {
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
if (
|
|
155
|
+
const declaredNames = readDeclaredScriptNames(baseDir);
|
|
156
|
+
const unresolvableEntry = missingEntries.find(({ command }) => !declaredNames.has(command));
|
|
157
|
+
if (unresolvableEntry === undefined) {
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
|
-
const
|
|
161
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${
|
|
162
|
-
`\`${
|
|
160
|
+
const closestName = findClosestName(unresolvableEntry.command, [...registeredNames, ...declaredNames]);
|
|
161
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${unresolvableEntry.field}\` does not name any command: ` +
|
|
162
|
+
`\`${unresolvableEntry.command}\`.${closestName === undefined ? '' : ` Did you mean \`${closestName}\`?`}`);
|
|
163
163
|
}
|
|
164
164
|
function readDeclaredScriptNames(monorepoRoot) {
|
|
165
165
|
let packageDirs;
|
|
@@ -203,8 +203,8 @@ function validateOutputField(value, configPath) {
|
|
|
203
203
|
const commandVerbosity = output['commandVerbosity'];
|
|
204
204
|
if (commandVerbosity !== undefined) {
|
|
205
205
|
if (typeof commandVerbosity !== 'string' || !isCommandVerbosity(commandVerbosity)) {
|
|
206
|
-
const
|
|
207
|
-
throw new UserError(`Invalid nmr config at ${configPath}: ${formatVerbosityRejection('`output.commandVerbosity`',
|
|
206
|
+
const renderedValue = typeof commandVerbosity === 'string' ? commandVerbosity : JSON.stringify(commandVerbosity);
|
|
207
|
+
throw new UserError(`Invalid nmr config at ${configPath}: ${formatVerbosityRejection('`output.commandVerbosity`', renderedValue)}`);
|
|
208
208
|
}
|
|
209
209
|
config.commandVerbosity = commandVerbosity;
|
|
210
210
|
}
|
|
@@ -258,9 +258,9 @@ export async function loadConfig(baseDir) {
|
|
|
258
258
|
if (!existsSync(configPath)) {
|
|
259
259
|
return {};
|
|
260
260
|
}
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
return validateConfig(
|
|
261
|
+
const importedModule = await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href));
|
|
262
|
+
const loadedConfig = isObject(importedModule) ? importedModule['default'] : undefined;
|
|
263
|
+
return validateConfig(loadedConfig, configPath, baseDir);
|
|
264
264
|
}
|
|
265
265
|
export async function loadRootConfig(monorepoRoot) {
|
|
266
266
|
const config = await loadConfig(monorepoRoot);
|
|
@@ -281,20 +281,20 @@ function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
|
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = '') {
|
|
284
|
-
const
|
|
285
|
-
if (
|
|
284
|
+
const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
|
|
285
|
+
if (unrecognizedKeys.length === 0) {
|
|
286
286
|
return;
|
|
287
287
|
}
|
|
288
|
-
throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${
|
|
289
|
-
`${formatKeyList(
|
|
288
|
+
throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${unrecognizedKeys.length === 1 ? 'key' : 'keys'} ` +
|
|
289
|
+
`${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`);
|
|
290
290
|
}
|
|
291
291
|
function assertTierKeys(config, tier, configPath) {
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
292
|
+
const unsupportedKeys = Object.keys(config).filter((key) => !tier.honoredKeys.includes(key));
|
|
293
|
+
if (unsupportedKeys.length === 0) {
|
|
294
294
|
return;
|
|
295
295
|
}
|
|
296
296
|
throw new UserError(`Invalid nmr config at ${configPath}: ${tier.label} honors ${tier.honoredKeys.toSorted().join(', ')} alone, ` +
|
|
297
|
-
`not ${
|
|
297
|
+
`not ${unsupportedKeys.toSorted().join(', ')}. Move those keys to ${tier.elsewhere}.`);
|
|
298
298
|
}
|
|
299
299
|
function formatKeyList(keys, prefix) {
|
|
300
300
|
return keys
|
package/dist/esm/context.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ export interface ResolvedContext {
|
|
|
3
3
|
monorepoRoot: string;
|
|
4
4
|
isRoot: boolean;
|
|
5
5
|
packageDir?: string;
|
|
6
|
-
/** The workspace's package directories, resolved once here rather than swept for again by each reader. */
|
|
7
6
|
workspacePackageDirs: string[];
|
|
8
7
|
config: NmrConfig;
|
|
9
8
|
}
|
|
@@ -11,7 +10,7 @@ export interface ResolvedContext {
|
|
|
11
10
|
* Determines whether a directory is inside a workspace package.
|
|
12
11
|
* Returns the package directory if so, or `undefined` if in root context.
|
|
13
12
|
*
|
|
14
|
-
* A workspace may nest one package inside another, so the deepest containing directory wins
|
|
13
|
+
* A workspace may nest one package inside another, so the deepest containing directory wins. This matches
|
|
15
14
|
* pnpm, which resolves a cwd to the package that encloses it most nearly.
|
|
16
15
|
*/
|
|
17
16
|
export declare function findContainingPackageDir(dir: string, workspacePackageDirs: string[]): string | undefined;
|
package/dist/esm/context.js
CHANGED
|
@@ -2,17 +2,17 @@ import path from 'node:path';
|
|
|
2
2
|
import { loadRootConfig } from "./config.js";
|
|
3
3
|
import { findMonorepoRoot, getWorkspacePackageDirs } from "./workspace.js";
|
|
4
4
|
export function findContainingPackageDir(dir, workspacePackageDirs) {
|
|
5
|
-
const
|
|
6
|
-
let
|
|
7
|
-
for (const
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
5
|
+
const resolvedDir = path.resolve(dir);
|
|
6
|
+
let nearestDir;
|
|
7
|
+
for (const packageDir of workspacePackageDirs) {
|
|
8
|
+
const resolvedPackageDir = path.resolve(packageDir);
|
|
9
|
+
if (resolvedDir !== resolvedPackageDir && !resolvedDir.startsWith(resolvedPackageDir + path.sep))
|
|
10
10
|
continue;
|
|
11
|
-
if (
|
|
12
|
-
|
|
11
|
+
if (nearestDir === undefined || resolvedPackageDir.length > nearestDir.length) {
|
|
12
|
+
nearestDir = resolvedPackageDir;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
return
|
|
15
|
+
return nearestDir;
|
|
16
16
|
}
|
|
17
17
|
export async function resolveContext(cwd) {
|
|
18
18
|
const resolvedCwd = path.resolve(cwd ?? process.cwd());
|
|
@@ -6,18 +6,17 @@ export interface StepSpec {
|
|
|
6
6
|
/** A command name, optionally preceded by nmr's own flags, as it would be typed after `nmr`. */
|
|
7
7
|
run: string;
|
|
8
8
|
/**
|
|
9
|
-
* Set
|
|
10
|
-
* run
|
|
9
|
+
* Set when the trailing arguments cannot narrow this step's work: It is a prerequisite against which the
|
|
10
|
+
* narrowed steps run, or the tool that it invokes would be misled by them. A declining step runs unnarrowed.
|
|
11
11
|
*/
|
|
12
12
|
shouldDeclineArguments?: boolean;
|
|
13
13
|
}
|
|
14
14
|
export type ScriptValue = string | ReadonlyArray<string | StepSpec>;
|
|
15
15
|
export type ScriptRegistry = Record<string, ScriptValue>;
|
|
16
16
|
/**
|
|
17
|
-
* Workspace scripts, identical for every package.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* Example: `nmr test:tool` runs every file carrying the `tool` infix, such as `my-file.tool.test.ts`.
|
|
17
|
+
* Workspace scripts, identical for every package. A `test:<tier>` script runs the Vitest project of that tier, which
|
|
18
|
+
* collects the test files that name it: `nmr test:tool` runs `my-file.tool.test.ts`. The `unit` project also
|
|
19
|
+
* collects every file that does not name any other tier.
|
|
21
20
|
*/
|
|
22
21
|
export declare const workspaceScripts: ScriptRegistry;
|
|
23
22
|
export declare const rootScripts: ScriptRegistry;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NmrConfig } from './types.js';
|
|
2
2
|
export type { BuildConfig, CheckCacheConfig, CommandVerbosity, NmrConfig, OutputConfig, ScriptValue, StepSpec, } from './types.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Returns `config` unchanged, typing a configuration file's default export as `NmrConfig`.
|
|
5
5
|
*
|
|
6
6
|
* Usage in `.config/nmr.config.ts`:
|
|
7
7
|
* ```ts
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lists the untracked paths that git ignores under `dir`, relative to it and POSIX-separated, sorted. An ignored
|
|
3
|
+
* directory is one entry ending in `/`, standing for everything beneath it; a tracked file never appears, whatever
|
|
4
|
+
* pattern it matches.
|
|
5
|
+
*
|
|
6
|
+
* Returns an empty list whenever git does not answer, outside a repository or with git missing, so that a caller
|
|
7
|
+
* excluding these paths over-reports rather than hides a path.
|
|
8
|
+
*/
|
|
9
|
+
export declare function listGitIgnoredPaths(dir: string): string[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
export function listGitIgnoredPaths(dir) {
|
|
3
|
+
const result = spawnSync('git', ['ls-files', '-z', '--others', '--ignored', '--exclude-standard', '--directory', '--no-empty-directory'], { cwd: dir, encoding: 'utf8' });
|
|
4
|
+
if (result.error !== undefined || result.status !== 0)
|
|
5
|
+
return [];
|
|
6
|
+
return result.stdout
|
|
7
|
+
.split('\0')
|
|
8
|
+
.filter((entry) => entry !== '')
|
|
9
|
+
.toSorted();
|
|
10
|
+
}
|
package/dist/esm/glyphs.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Glyphs that name
|
|
3
|
-
*
|
|
2
|
+
* Glyphs that do not name a status; a status marker comes from nmr-core's `STATUS_GLYPHS`. A plain variant is
|
|
3
|
+
* empty when a word always follows the glyph, and a word when the glyph opens a line of its own.
|
|
4
4
|
*
|
|
5
|
-
* `noop` is a neutral circle:
|
|
5
|
+
* `noop` is a neutral circle: Nothing ran because the repo asked for nothing to run, which is neither a
|
|
6
6
|
* failure nor a block.
|
|
7
7
|
*/
|
|
8
8
|
export declare const NMR_GLYPHS: Readonly<Record<"plain" | "rich", Readonly<Record<"overrides" | "clean" | "package" | "catalog" | "noop" | "recording", import("@williamthorsen/toolbelt.terminal/candidate").Glyph>>>>;
|
package/dist/esm/help.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { NmrConfig } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Generates the help text for the `nmr` CLI. Renders only nmr commands
|
|
3
|
+
* Generates the help text for the `nmr` CLI. Renders only nmr commands:
|
|
4
4
|
* names from the workspace and root registries, excluding hooks (`*:pre`,
|
|
5
5
|
* `*:post`).
|
|
6
6
|
*
|
|
7
7
|
* When `packageDir` is provided, tier-3 entries from that package's
|
|
8
8
|
* `package.json:scripts` that match a registry name in the active section
|
|
9
|
-
* are inlined as overrides:
|
|
9
|
+
* are inlined as overrides: The registry value is replaced with the
|
|
10
10
|
* override value and the row's command name is suffixed with `*`. The
|
|
11
|
-
* active section is the root section when `
|
|
11
|
+
* active section is the root section when `shouldUseRoot` is true (root cwd
|
|
12
12
|
* or `-w`), otherwise the workspace section. A footnote is appended once
|
|
13
13
|
* if any override marker was rendered.
|
|
14
14
|
*/
|
|
15
|
-
export declare function generateHelp(config: NmrConfig, packageDir: string | undefined,
|
|
15
|
+
export declare function generateHelp(config: NmrConfig, packageDir: string | undefined, shouldUseRoot: boolean): string;
|