@williamthorsen/nmr 0.37.0 → 0.39.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 +114 -0
- package/.readyup/kits/default.js +92 -87
- package/.readyup/manifest.json +8 -8
- package/CHANGELOG.md +89 -0
- package/README.md +37 -28
- package/agents/guidance/rulebooks/nmr.md +2 -2
- package/dist/esm/check-cache.d.ts +4 -3
- package/dist/esm/check-cache.js +28 -28
- package/dist/esm/cli-build.js +3 -0
- package/dist/esm/cli-clean.js +3 -1
- package/dist/esm/cli-ensure-prepublish-hooks.js +7 -5
- package/dist/esm/cli-report-catalog.js +2 -1
- package/dist/esm/cli-report-overrides.js +2 -1
- package/dist/esm/commands/build-output.d.ts +2 -2
- package/dist/esm/commands/build-output.js +9 -9
- package/dist/esm/commands/build.d.ts +6 -1
- package/dist/esm/commands/build.js +73 -72
- package/dist/esm/commands/clean.d.ts +3 -2
- package/dist/esm/commands/clean.js +25 -24
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +5 -4
- package/dist/esm/commands/ensure-prepublish-hooks.js +44 -42
- package/dist/esm/commands/fmt.js +33 -33
- package/dist/esm/commands/report-catalog.d.ts +2 -1
- package/dist/esm/commands/report-catalog.js +14 -11
- package/dist/esm/commands/report-overrides.d.ts +2 -1
- package/dist/esm/commands/report-overrides.js +11 -10
- package/dist/esm/config.js +39 -29
- package/dist/esm/context.js +8 -8
- package/dist/esm/default-scripts.d.ts +2 -2
- package/dist/esm/default-scripts.js +5 -5
- package/dist/esm/glyphs.d.ts +9 -0
- package/dist/esm/glyphs.js +9 -0
- package/dist/esm/help.d.ts +2 -2
- package/dist/esm/help.js +36 -30
- package/dist/esm/helpers/clampToBytes.js +3 -3
- package/dist/esm/helpers/createBoundedBuffer.d.ts +1 -1
- package/dist/esm/helpers/createBoundedBuffer.js +2 -2
- package/dist/esm/helpers/duration.d.ts +2 -2
- package/dist/esm/helpers/duration.js +7 -7
- package/dist/esm/helpers/filter-selection.js +12 -12
- package/dist/esm/helpers/findClosestName.js +15 -15
- package/dist/esm/helpers/package-json.d.ts +2 -2
- package/dist/esm/helpers/package-json.js +26 -25
- package/dist/esm/helpers/transcript.d.ts +2 -2
- package/dist/esm/helpers/transcript.js +14 -14
- package/dist/esm/output-style.d.ts +41 -0
- package/dist/esm/output-style.js +17 -0
- package/dist/esm/prettier.js +5 -5
- package/dist/esm/recording.d.ts +8 -5
- package/dist/esm/recording.js +27 -17
- package/dist/esm/report-format.d.ts +1 -1
- package/dist/esm/report-format.js +7 -7
- package/dist/esm/resolveBinStyles.d.ts +12 -0
- package/dist/esm/resolveBinStyles.js +16 -0
- package/dist/esm/resolver.d.ts +2 -2
- package/dist/esm/resolver.js +16 -16
- package/dist/esm/runCli.js +309 -221
- package/dist/esm/runner.d.ts +1 -1
- package/dist/esm/runner.js +18 -18
- package/dist/esm/steps.d.ts +17 -5
- package/dist/esm/steps.js +37 -28
- package/dist/esm/tests.d.ts +7 -7
- package/dist/esm/tests.js +20 -8
- package/dist/esm/tiers.d.ts +1 -1
- package/dist/esm/tiers.js +8 -8
- package/dist/esm/verbosity.d.ts +1 -1
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +15 -4
- package/dist/esm/verdict.js +78 -59
- package/dist/esm/vitest-source-resolution.js +48 -46
- package/dist/esm/vitest.d.ts +2 -2
- package/dist/esm/vitest.js +74 -25
- package/dist/esm/workspace.d.ts +2 -18
- package/dist/esm/workspace.js +9 -67
- package/docs/check-cache.md +5 -5
- package/docs/reporting.md +20 -2
- package/docs/scripts.md +2 -2
- package/docs/vitest.md +6 -6
- package/docs/workspace.md +24 -1
- package/package.json +9 -10
- 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: no formattable files matched: ${
|
|
34
|
+
if (parsedArgs.pathspecs.length > 0) {
|
|
35
|
+
reportError(`nmr-fmt: no formattable files matched: ${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,3 +1,4 @@
|
|
|
1
|
+
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
1
2
|
/**
|
|
2
3
|
* Reports the catalogued dependencies a package-scoped upgrade pass cannot see.
|
|
3
4
|
*
|
|
@@ -8,4 +9,4 @@
|
|
|
8
9
|
*
|
|
9
10
|
* Runs ahead of the report produced by the workspace `upgrade` script.
|
|
10
11
|
*/
|
|
11
|
-
export declare function reportCatalog(cwd: string): void;
|
|
12
|
+
export declare function reportCatalog(cwd: string, style: OutputStyle): void;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { formatGlyphLine } from '@williamthorsen/nmr-core';
|
|
1
2
|
import { findContainingPackageDir } from "../context.js";
|
|
3
|
+
import { NMR_GLYPHS } from "../glyphs.js";
|
|
2
4
|
import { DEPENDENCY_FIELDS, readPackageJson } from "../helpers/package-json.js";
|
|
3
5
|
import { reportClosing } from "../helpers/reportClosing.js";
|
|
4
6
|
import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
|
|
5
|
-
const CATALOG_ICON = '📚';
|
|
6
7
|
const CATALOG_PROTOCOL = 'catalog:';
|
|
7
|
-
export function reportCatalog(cwd) {
|
|
8
|
+
export function reportCatalog(cwd, style) {
|
|
8
9
|
const monorepoRoot = findMonorepoRoot(cwd);
|
|
9
10
|
const packageDir = findContainingPackageDir(cwd, getWorkspacePackageDirs(monorepoRoot));
|
|
10
11
|
if (packageDir === undefined) {
|
|
@@ -14,11 +15,11 @@ export function reportCatalog(cwd) {
|
|
|
14
15
|
if (dependencies.length === 0) {
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
|
-
console.warn(
|
|
18
|
+
console.warn(formatGlyphLine(NMR_GLYPHS, style, 'catalog', 'WARN: A package-scoped upgrade does not read the catalogs these come from:'));
|
|
18
19
|
for (const { name, specifier } of dependencies) {
|
|
19
20
|
console.warn(`- ${name} → ${specifier}`);
|
|
20
21
|
}
|
|
21
|
-
reportClosing(
|
|
22
|
+
reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'catalog', describeCatalog(dependencies.length, monorepoRoot)), console.warn);
|
|
22
23
|
}
|
|
23
24
|
function describeCatalog(count, monorepoRoot) {
|
|
24
25
|
const subject = count === 1 ? '1 catalogued dependency went' : `${count} catalogued dependencies went`;
|
|
@@ -26,15 +27,17 @@ function describeCatalog(count, monorepoRoot) {
|
|
|
26
27
|
return `${subject} unread. Run \`nmr upgrade\` from ${monorepoRoot} to include ${object}.`;
|
|
27
28
|
}
|
|
28
29
|
function findCataloguedDependencies(packageDir) {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
30
|
+
const packageJson = readPackageJson(packageDir);
|
|
31
|
+
const foundDependencies = new Map();
|
|
31
32
|
for (const field of DEPENDENCY_FIELDS) {
|
|
32
|
-
const
|
|
33
|
-
for (const [name, specifier] of
|
|
34
|
-
if (specifier.startsWith(CATALOG_PROTOCOL) && !
|
|
35
|
-
|
|
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);
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
return [...
|
|
40
|
+
return [...foundDependencies]
|
|
41
|
+
.map(([name, specifier]) => ({ name, specifier }))
|
|
42
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
40
43
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
1
2
|
/**
|
|
2
3
|
* Reports the pnpm dependency overrides declared in the monorepo root's `pnpm-workspace.yaml`, and rejects a
|
|
3
4
|
* `pnpm.overrides` block left behind in the root `package.json`.
|
|
4
5
|
*
|
|
5
6
|
* Runs ahead of the report produced by the root `upgrade` script.
|
|
6
7
|
*/
|
|
7
|
-
export declare function reportOverrides(monorepoRoot: string): void;
|
|
8
|
+
export declare function reportOverrides(monorepoRoot: string, style: OutputStyle): void;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { formatGlyphLine } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { NMR_GLYPHS } from "../glyphs.js";
|
|
1
3
|
import { getPnpmOverrides, readPackageJson } from "../helpers/package-json.js";
|
|
2
4
|
import { reportClosing } from "../helpers/reportClosing.js";
|
|
3
5
|
import { UserError } from "../UserError.js";
|
|
4
6
|
import { readWorkspaceOverrides } from "../workspace.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
for (const [name, version] of declared) {
|
|
7
|
+
export function reportOverrides(monorepoRoot, style) {
|
|
8
|
+
const declaredOverrides = listEntries(readWorkspaceOverrides(monorepoRoot));
|
|
9
|
+
if (declaredOverrides.length > 0) {
|
|
10
|
+
console.warn(formatGlyphLine(NMR_GLYPHS, style, 'overrides', 'WARN: pnpm overrides are active:'));
|
|
11
|
+
for (const [name, version] of declaredOverrides) {
|
|
11
12
|
console.warn(`- ${name} → ${version}`);
|
|
12
13
|
}
|
|
13
|
-
reportClosing(
|
|
14
|
+
reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'overrides', describeOverrides(declaredOverrides.length)), console.warn);
|
|
14
15
|
}
|
|
15
16
|
rejectLegacyOverrides(monorepoRoot);
|
|
16
17
|
}
|
|
@@ -23,13 +24,13 @@ function listEntries(overrides) {
|
|
|
23
24
|
return Object.entries(overrides ?? {}).toSorted(([a], [b]) => a.localeCompare(b));
|
|
24
25
|
}
|
|
25
26
|
function rejectLegacyOverrides(monorepoRoot) {
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
27
|
+
const legacyOverrides = listEntries(getPnpmOverrides(readPackageJson(monorepoRoot)));
|
|
28
|
+
if (legacyOverrides.length === 0) {
|
|
28
29
|
return;
|
|
29
30
|
}
|
|
30
31
|
throw new UserError([
|
|
31
32
|
'pnpm 11 reads no `pnpm.overrides` from package.json, so these pin nothing while an upgrade run with `--write` goes on rewriting them:',
|
|
32
|
-
...
|
|
33
|
+
...legacyOverrides.map(([name, version]) => `- ${name} → ${String(version)}`),
|
|
33
34
|
'Move them to the `overrides` block in pnpm-workspace.yaml, quoting each version, or run `pnpx codemod run pnpm-v10-to-v11`.',
|
|
34
35
|
].join('\n'));
|
|
35
36
|
}
|
package/dist/esm/config.js
CHANGED
|
@@ -33,7 +33,8 @@ const RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.works
|
|
|
33
33
|
const RECOGNIZED_BUILD_KEYS = ['extraIgnorePatterns'];
|
|
34
34
|
const RECOGNIZED_CHECK_CACHE_KEYS = ['enabled', 'excludeCommands', 'extraCommands'];
|
|
35
35
|
const RECOGNIZED_OUTPUT_KEYS = ['commandVerbosity', 'extraAgentEnvVars'];
|
|
36
|
-
const RECOGNIZED_STEP_KEYS = ['
|
|
36
|
+
const RECOGNIZED_STEP_KEYS = ['run', 'shouldDeclineArguments'];
|
|
37
|
+
const RETIRED_STEP_KEYS = new Map([['declinesArgs', 'shouldDeclineArguments']]);
|
|
37
38
|
function isScriptRecord(value) {
|
|
38
39
|
if (!isObject(value))
|
|
39
40
|
return false;
|
|
@@ -50,8 +51,8 @@ function isScriptElement(value) {
|
|
|
50
51
|
return true;
|
|
51
52
|
if (!isObject(value) || typeof value['run'] !== 'string')
|
|
52
53
|
return false;
|
|
53
|
-
const
|
|
54
|
-
return
|
|
54
|
+
const shouldDeclineArguments = value['shouldDeclineArguments'];
|
|
55
|
+
return shouldDeclineArguments === undefined || typeof shouldDeclineArguments === 'boolean';
|
|
55
56
|
}
|
|
56
57
|
function validateScriptField(value, fieldName, configPath) {
|
|
57
58
|
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
@@ -60,7 +61,7 @@ function validateScriptField(value, fieldName, configPath) {
|
|
|
60
61
|
const scripts = value[fieldName];
|
|
61
62
|
if (!isScriptRecord(scripts)) {
|
|
62
63
|
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | element[]>, ` +
|
|
63
|
-
'where an element is a command string or `{ run: string,
|
|
64
|
+
'where an element is a command string or `{ run: string, shouldDeclineArguments?: boolean }`');
|
|
64
65
|
}
|
|
65
66
|
assertValidElements(scripts, fieldName, configPath);
|
|
66
67
|
return scripts;
|
|
@@ -71,6 +72,7 @@ function assertValidElements(scripts, fieldName, configPath) {
|
|
|
71
72
|
continue;
|
|
72
73
|
for (const spec of script) {
|
|
73
74
|
if (typeof spec !== 'string') {
|
|
75
|
+
assertNoRetiredKeys(spec, RETIRED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
74
76
|
assertRecognizedKeys(spec, RECOGNIZED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
75
77
|
}
|
|
76
78
|
const element = typeof spec === 'string' ? spec : spec.run;
|
|
@@ -111,12 +113,12 @@ function validateCheckCacheField(value, configPath) {
|
|
|
111
113
|
}
|
|
112
114
|
assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, 'checkCache.');
|
|
113
115
|
const config = {};
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
if (typeof
|
|
116
|
+
const enabledValue = checkCache['enabled'];
|
|
117
|
+
if (enabledValue !== undefined) {
|
|
118
|
+
if (typeof enabledValue !== 'boolean') {
|
|
117
119
|
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
|
|
118
120
|
}
|
|
119
|
-
config.enabled =
|
|
121
|
+
config.enabled = enabledValue;
|
|
120
122
|
}
|
|
121
123
|
for (const field of ['excludeCommands', 'extraCommands']) {
|
|
122
124
|
const commands = checkCache[field];
|
|
@@ -142,22 +144,22 @@ function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
|
|
|
142
144
|
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${hook.field}\` names the hook \`${hook.command}\`, ` +
|
|
143
145
|
'which is never gated on its own: it runs as part of the chain of the command it wraps.');
|
|
144
146
|
}
|
|
145
|
-
const
|
|
147
|
+
const registeredNames = new Set([
|
|
146
148
|
...Object.keys(buildRootRegistry(config)),
|
|
147
149
|
...Object.keys(buildWorkspaceRegistry(config)),
|
|
148
150
|
]);
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
151
|
+
const missingEntries = entries.filter(({ command }) => !registeredNames.has(command));
|
|
152
|
+
if (missingEntries.length === 0) {
|
|
151
153
|
return;
|
|
152
154
|
}
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
155
|
+
const declaredNames = readDeclaredScriptNames(baseDir);
|
|
156
|
+
const unresolvableEntry = missingEntries.find(({ command }) => !declaredNames.has(command));
|
|
157
|
+
if (unresolvableEntry === undefined) {
|
|
156
158
|
return;
|
|
157
159
|
}
|
|
158
|
-
const
|
|
159
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${
|
|
160
|
-
`\`${
|
|
160
|
+
const closestName = findClosestName(unresolvableEntry.command, [...registeredNames, ...declaredNames]);
|
|
161
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${unresolvableEntry.field}\` names no command: ` +
|
|
162
|
+
`\`${unresolvableEntry.command}\`.${closestName === undefined ? '' : ` Did you mean \`${closestName}\`?`}`);
|
|
161
163
|
}
|
|
162
164
|
function readDeclaredScriptNames(monorepoRoot) {
|
|
163
165
|
let packageDirs;
|
|
@@ -201,8 +203,8 @@ function validateOutputField(value, configPath) {
|
|
|
201
203
|
const commandVerbosity = output['commandVerbosity'];
|
|
202
204
|
if (commandVerbosity !== undefined) {
|
|
203
205
|
if (typeof commandVerbosity !== 'string' || !isCommandVerbosity(commandVerbosity)) {
|
|
204
|
-
const
|
|
205
|
-
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)}`);
|
|
206
208
|
}
|
|
207
209
|
config.commandVerbosity = commandVerbosity;
|
|
208
210
|
}
|
|
@@ -256,9 +258,9 @@ export async function loadConfig(baseDir) {
|
|
|
256
258
|
if (!existsSync(configPath)) {
|
|
257
259
|
return {};
|
|
258
260
|
}
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
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);
|
|
262
264
|
}
|
|
263
265
|
export async function loadRootConfig(monorepoRoot) {
|
|
264
266
|
const config = await loadConfig(monorepoRoot);
|
|
@@ -270,21 +272,29 @@ export async function loadWorkspaceConfig(packageDir) {
|
|
|
270
272
|
assertTierKeys(config, CONFIG_TIERS.workspace, resolveConfigPath(packageDir));
|
|
271
273
|
return config;
|
|
272
274
|
}
|
|
275
|
+
function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
|
|
276
|
+
for (const key of Object.keys(value)) {
|
|
277
|
+
const replacement = retiredKeys.get(key);
|
|
278
|
+
if (replacement === undefined)
|
|
279
|
+
continue;
|
|
280
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`${prefix}${key}\` was renamed to \`${prefix}${replacement}\`.`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
273
283
|
function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = '') {
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
284
|
+
const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
|
|
285
|
+
if (unrecognizedKeys.length === 0) {
|
|
276
286
|
return;
|
|
277
287
|
}
|
|
278
|
-
throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${
|
|
279
|
-
`${formatKeyList(
|
|
288
|
+
throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${unrecognizedKeys.length === 1 ? 'key' : 'keys'} ` +
|
|
289
|
+
`${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`);
|
|
280
290
|
}
|
|
281
291
|
function assertTierKeys(config, tier, configPath) {
|
|
282
|
-
const
|
|
283
|
-
if (
|
|
292
|
+
const unsupportedKeys = Object.keys(config).filter((key) => !tier.honoredKeys.includes(key));
|
|
293
|
+
if (unsupportedKeys.length === 0) {
|
|
284
294
|
return;
|
|
285
295
|
}
|
|
286
296
|
throw new UserError(`Invalid nmr config at ${configPath}: ${tier.label} honors ${tier.honoredKeys.toSorted().join(', ')} alone, ` +
|
|
287
|
-
`not ${
|
|
297
|
+
`not ${unsupportedKeys.toSorted().join(', ')}. Move those keys to ${tier.elsewhere}.`);
|
|
288
298
|
}
|
|
289
299
|
function formatKeyList(keys, prefix) {
|
|
290
300
|
return keys
|
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());
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One element of a composite, paired with what it does with the invocation's trailing arguments. The bare
|
|
3
|
-
* string form of an element is this spec with `
|
|
3
|
+
* string form of an element is this spec with `shouldDeclineArguments` left at its default.
|
|
4
4
|
*/
|
|
5
5
|
export interface StepSpec {
|
|
6
6
|
/** A command name, optionally preceded by nmr's own flags, as it would be typed after `nmr`. */
|
|
@@ -9,7 +9,7 @@ export interface StepSpec {
|
|
|
9
9
|
* Set where the trailing arguments cannot narrow this step's work: it is a prerequisite the narrowed steps
|
|
10
10
|
* run against, or the tool it reaches 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>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const GATE_PROJECTS = '--project unit --project tool';
|
|
2
|
-
const TYPECHECK_STEP = { run: 'typecheck',
|
|
3
|
-
const ROOT_TYPECHECK_STEP = { run: 'root:typecheck',
|
|
2
|
+
const TYPECHECK_STEP = { run: 'typecheck', shouldDeclineArguments: true };
|
|
3
|
+
const ROOT_TYPECHECK_STEP = { run: 'root:typecheck', shouldDeclineArguments: true };
|
|
4
4
|
export const workspaceScripts = {
|
|
5
5
|
build: ['compile'],
|
|
6
6
|
check: [TYPECHECK_STEP, 'fmt:check', 'lint:check', 'test'],
|
|
@@ -32,7 +32,7 @@ export const rootScripts = {
|
|
|
32
32
|
build: ['-R build'],
|
|
33
33
|
check: [TYPECHECK_STEP, 'fmt:check', 'lint:check', 'test'],
|
|
34
34
|
'check:strict': [TYPECHECK_STEP, 'fmt:check', 'lint:strict', 'test'],
|
|
35
|
-
ci: [{ run: 'build',
|
|
35
|
+
ci: [{ run: 'build', shouldDeclineArguments: true }, 'check:strict'],
|
|
36
36
|
clean: 'nmr-clean',
|
|
37
37
|
fix: ['lint', 'fmt'],
|
|
38
38
|
'fix:check': ['fmt:check', 'lint:check'],
|
|
@@ -41,7 +41,7 @@ export const rootScripts = {
|
|
|
41
41
|
lint: 'eslint --fix .',
|
|
42
42
|
'lint:check': 'eslint .',
|
|
43
43
|
'lint:strict': 'strict-lint',
|
|
44
|
-
prepush: [{ run: 'audit',
|
|
44
|
+
prepush: [{ run: 'audit', shouldDeclineArguments: true }, 'ci'],
|
|
45
45
|
'report-overrides': 'nmr-report-overrides',
|
|
46
46
|
'root:check': [ROOT_TYPECHECK_STEP, 'fmt:check', 'root:lint:check', 'root:test'],
|
|
47
47
|
'root:lint': "eslint --fix --ignore-pattern 'packages/**' .",
|
|
@@ -59,6 +59,6 @@ export const rootScripts = {
|
|
|
59
59
|
'test:tool': ['root:test:tool', '-R test:tool'],
|
|
60
60
|
'test:unit': ['root:test:unit', '-R test:unit'],
|
|
61
61
|
'test:watch': `vitest ${GATE_PROJECTS} --watch`,
|
|
62
|
-
typecheck: [ROOT_TYPECHECK_STEP, { run: '-R typecheck',
|
|
62
|
+
typecheck: [ROOT_TYPECHECK_STEP, { run: '-R typecheck', shouldDeclineArguments: true }],
|
|
63
63
|
upgrade: 'nmr-report-overrides && nmr-taze --recursive',
|
|
64
64
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glyphs that name no status; a status marker comes from nmr-core's `STATUS_GLYPHS`. A plain variant is empty
|
|
3
|
+
* where a word always follows the glyph, and a word where the glyph opens a line of its own.
|
|
4
|
+
*
|
|
5
|
+
* `noop` is a neutral circle: nothing ran because the repo asked for nothing to run, which is neither a
|
|
6
|
+
* failure nor a block.
|
|
7
|
+
*/
|
|
8
|
+
export declare const NMR_GLYPHS: Readonly<Record<"plain" | "rich", Readonly<Record<"overrides" | "clean" | "package" | "catalog" | "noop" | "recording", import("@williamthorsen/toolbelt.terminal/candidate").Glyph>>>>;
|
|
9
|
+
export type NmrGlyphName = keyof (typeof NMR_GLYPHS)['rich'];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineGlyphSet } from '@williamthorsen/nmr-core';
|
|
2
|
+
export const NMR_GLYPHS = defineGlyphSet({
|
|
3
|
+
catalog: { plain: '', rich: '\u{1F4DA}' },
|
|
4
|
+
clean: { plain: '', rich: '\u{1F9F9}' },
|
|
5
|
+
noop: { plain: 'NOOP', rich: '\u{26AA}' },
|
|
6
|
+
overrides: { plain: '', rich: '\u{1F512}' },
|
|
7
|
+
package: { plain: '', rich: '\u{1F4E6}' },
|
|
8
|
+
recording: { plain: '', rich: '\u{1F4BE}' },
|
|
9
|
+
});
|
package/dist/esm/help.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ import type { NmrConfig } from './types.js';
|
|
|
8
8
|
* `package.json:scripts` that match a registry name in the active section
|
|
9
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;
|