@williamthorsen/nmr 0.30.0 → 0.32.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/.readyup/kits/default.js +56 -16
- package/.readyup/manifest.json +34 -3
- package/README.md +265 -64
- package/agents/guidance/rulebooks/nmr.md +5 -2
- package/bin/nmr-report-catalog.js +11 -0
- package/dist/esm/UserError.d.ts +3 -0
- package/dist/esm/UserError.js +6 -0
- package/dist/esm/check-cache.d.ts +36 -1
- package/dist/esm/check-cache.js +91 -23
- package/dist/esm/cli-build.js +2 -1
- package/dist/esm/cli-clean.js +2 -1
- package/dist/esm/cli-ensure-prepublish-hooks.js +4 -24
- package/dist/esm/cli-report-catalog.d.ts +1 -0
- package/dist/esm/cli-report-catalog.js +11 -0
- package/dist/esm/cli-report-overrides.js +2 -1
- package/dist/esm/cli-taze.js +2 -1
- package/dist/esm/cli.js +2 -1
- package/dist/esm/commands/build-output.d.ts +1 -0
- package/dist/esm/commands/build-output.js +25 -2
- package/dist/esm/commands/build.d.ts +5 -1
- package/dist/esm/commands/build.js +22 -8
- package/dist/esm/commands/clean.js +12 -0
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +1 -0
- package/dist/esm/commands/ensure-prepublish-hooks.js +43 -0
- package/dist/esm/commands/report-catalog.d.ts +1 -0
- package/dist/esm/commands/report-catalog.js +40 -0
- package/dist/esm/commands/report-overrides.js +30 -7
- package/dist/esm/commands/taze.js +2 -1
- package/dist/esm/config.d.ts +1 -0
- package/dist/esm/config.js +66 -18
- package/dist/esm/default-scripts.d.ts +5 -1
- package/dist/esm/default-scripts.js +13 -10
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/help.js +3 -1
- package/dist/esm/helpers/clampToBytes.d.ts +2 -0
- package/dist/esm/helpers/clampToBytes.js +20 -0
- package/dist/esm/helpers/createBoundedBuffer.js +1 -4
- package/dist/esm/helpers/deriveExcerpt.d.ts +1 -0
- package/dist/esm/helpers/deriveExcerpt.js +30 -0
- package/dist/esm/helpers/duration.js +16 -9
- package/dist/esm/helpers/package-json.d.ts +11 -4
- package/dist/esm/helpers/package-json.js +51 -22
- package/dist/esm/helpers/readStringValues.d.ts +1 -0
- package/dist/esm/helpers/readStringValues.js +8 -0
- package/dist/esm/helpers/reportClosing.d.ts +1 -0
- package/dist/esm/helpers/reportClosing.js +3 -0
- package/dist/esm/helpers/transcript.d.ts +5 -0
- package/dist/esm/helpers/transcript.js +59 -0
- package/dist/esm/recording.d.ts +65 -0
- package/dist/esm/recording.js +96 -0
- package/dist/esm/replay-assembly.d.ts +9 -0
- package/dist/esm/replay-assembly.js +26 -0
- package/dist/esm/report-format.d.ts +18 -0
- package/dist/esm/report-format.js +25 -0
- package/dist/esm/reportCliFailure.d.ts +2 -0
- package/dist/esm/reportCliFailure.js +11 -0
- package/dist/esm/resolve-scripts.d.ts +1 -1
- package/dist/esm/resolver.d.ts +9 -1
- package/dist/esm/resolver.js +28 -18
- package/dist/esm/runCli.js +339 -74
- package/dist/esm/runner.d.ts +9 -3
- package/dist/esm/runner.js +25 -4
- package/dist/esm/scripts.d.ts +1 -1
- package/dist/esm/steps.d.ts +9 -2
- package/dist/esm/steps.js +117 -4
- package/dist/esm/types.d.ts +11 -2
- package/dist/esm/verbosity.d.ts +15 -4
- package/dist/esm/verbosity.js +32 -12
- package/dist/esm/verdict.d.ts +29 -0
- package/dist/esm/verdict.js +150 -0
- package/dist/esm/workspace.d.ts +2 -0
- package/dist/esm/workspace.js +20 -3
- package/package.json +14 -5
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { findContainingPackageDir } from "../context.js";
|
|
2
|
+
import { DEPENDENCY_FIELDS, readPackageJson } from "../helpers/package-json.js";
|
|
3
|
+
import { reportClosing } from "../helpers/reportClosing.js";
|
|
4
|
+
import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
|
|
5
|
+
const CATALOG_ICON = '📚';
|
|
6
|
+
const CATALOG_PROTOCOL = 'catalog:';
|
|
7
|
+
export function reportCatalog(cwd) {
|
|
8
|
+
const monorepoRoot = findMonorepoRoot(cwd);
|
|
9
|
+
const packageDir = findContainingPackageDir(cwd, getWorkspacePackageDirs(monorepoRoot));
|
|
10
|
+
if (packageDir === undefined) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const dependencies = findCataloguedDependencies(packageDir);
|
|
14
|
+
if (dependencies.length === 0) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
console.warn(`${CATALOG_ICON} WARN: A package-scoped upgrade does not read the catalogs these come from:`);
|
|
18
|
+
for (const { name, specifier } of dependencies) {
|
|
19
|
+
console.warn(`- ${name} → ${specifier}`);
|
|
20
|
+
}
|
|
21
|
+
reportClosing(`${CATALOG_ICON} ${describeCatalog(dependencies.length, monorepoRoot)}`, console.warn);
|
|
22
|
+
}
|
|
23
|
+
function describeCatalog(count, monorepoRoot) {
|
|
24
|
+
const subject = count === 1 ? '1 catalogued dependency went' : `${count} catalogued dependencies went`;
|
|
25
|
+
const object = count === 1 ? 'it' : 'them';
|
|
26
|
+
return `${subject} unread. Run \`nmr upgrade\` from ${monorepoRoot} to include ${object}.`;
|
|
27
|
+
}
|
|
28
|
+
function findCataloguedDependencies(packageDir) {
|
|
29
|
+
const pkg = readPackageJson(packageDir);
|
|
30
|
+
const found = new Map();
|
|
31
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
32
|
+
const declared = Object.entries(pkg[field] ?? {});
|
|
33
|
+
for (const [name, specifier] of declared) {
|
|
34
|
+
if (specifier.startsWith(CATALOG_PROTOCOL) && !found.has(name)) {
|
|
35
|
+
found.set(name, specifier);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return [...found].map(([name, specifier]) => ({ name, specifier })).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
40
|
+
}
|
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import { getPnpmOverrides, readPackageJson } from "../helpers/package-json.js";
|
|
2
|
+
import { reportClosing } from "../helpers/reportClosing.js";
|
|
3
|
+
import { UserError } from "../UserError.js";
|
|
4
|
+
import { readWorkspaceOverrides } from "../workspace.js";
|
|
5
|
+
const OVERRIDES_ICON = '🔒';
|
|
2
6
|
export function reportOverrides(monorepoRoot) {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
const declared = listEntries(readWorkspaceOverrides(monorepoRoot));
|
|
8
|
+
if (declared.length > 0) {
|
|
9
|
+
console.warn(`${OVERRIDES_ICON} WARN: pnpm overrides are active:`);
|
|
10
|
+
for (const [name, version] of declared) {
|
|
11
|
+
console.warn(`- ${name} → ${version}`);
|
|
12
|
+
}
|
|
13
|
+
reportClosing(`${OVERRIDES_ICON} ${describeOverrides(declared.length)}`, console.warn);
|
|
7
14
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
rejectLegacyOverrides(monorepoRoot);
|
|
16
|
+
}
|
|
17
|
+
function describeOverrides(count) {
|
|
18
|
+
return count === 1
|
|
19
|
+
? '1 override is active. Check whether it is still needed.'
|
|
20
|
+
: `${count} overrides are active. Check whether they are still needed.`;
|
|
21
|
+
}
|
|
22
|
+
function listEntries(overrides) {
|
|
23
|
+
return Object.entries(overrides ?? {}).toSorted(([a], [b]) => a.localeCompare(b));
|
|
24
|
+
}
|
|
25
|
+
function rejectLegacyOverrides(monorepoRoot) {
|
|
26
|
+
const legacy = listEntries(getPnpmOverrides(readPackageJson(monorepoRoot)));
|
|
27
|
+
if (legacy.length === 0) {
|
|
28
|
+
return;
|
|
11
29
|
}
|
|
30
|
+
throw new UserError([
|
|
31
|
+
'pnpm 11 reads no `pnpm.overrides` from package.json, so these pin nothing while an upgrade run with `--write` goes on rewriting them:',
|
|
32
|
+
...legacy.map(([name, version]) => `- ${name} → ${String(version)}`),
|
|
33
|
+
'Move them to the `overrides` block in pnpm-workspace.yaml, quoting each version, or run `pnpx codemod run pnpm-v10-to-v11`.',
|
|
34
|
+
].join('\n'));
|
|
12
35
|
}
|
|
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
5
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
5
6
|
const TAZE_CLI_SPECIFIER = 'taze/cli';
|
|
6
7
|
export function runTaze(args, options = {}) {
|
|
7
8
|
const stderr = options.stderr ?? process.stderr;
|
|
@@ -12,7 +13,7 @@ export function runTaze(args, options = {}) {
|
|
|
12
13
|
cliPath = resolveCliPath();
|
|
13
14
|
}
|
|
14
15
|
catch (error) {
|
|
15
|
-
const detail =
|
|
16
|
+
const detail = describeError(error);
|
|
16
17
|
reportError(`Could not resolve '${TAZE_CLI_SPECIFIER}' from nmr's dependencies. Reinstall the workspace to restore it. (${detail})`, stderr);
|
|
17
18
|
return 1;
|
|
18
19
|
}
|
package/dist/esm/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NmrConfig } from './types.js';
|
|
2
|
+
export declare const CONFIG_RELATIVE_PATH: string;
|
|
2
3
|
export declare function resolveConfigPath(baseDir: string): string;
|
|
3
4
|
export declare function loadConfig(baseDir: string): Promise<NmrConfig>;
|
|
4
5
|
export declare function loadRootConfig(monorepoRoot: string): Promise<NmrConfig>;
|
package/dist/esm/config.js
CHANGED
|
@@ -11,12 +11,15 @@ import path from 'node:path';
|
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
import { isObject, isStringRecord } from "./helpers/type-guards.js";
|
|
13
13
|
import { findUnexpressibleToken } from "./steps.js";
|
|
14
|
+
import { UserError } from "./UserError.js";
|
|
15
|
+
import { formatVerbosityRejection, isCommandVerbosity } from "./verbosity.js";
|
|
14
16
|
const CONFIG_FILENAME = 'nmr.config.ts';
|
|
15
17
|
const CONFIG_DIR = '.config';
|
|
18
|
+
export const CONFIG_RELATIVE_PATH = path.join(CONFIG_DIR, CONFIG_FILENAME);
|
|
16
19
|
const CONFIG_TIERS = {
|
|
17
20
|
root: {
|
|
18
21
|
label: 'a monorepo-root config',
|
|
19
|
-
honoredKeys: ['checkCache', 'devBin', 'rootScripts', 'workspaceScripts'],
|
|
22
|
+
honoredKeys: ['checkCache', 'devBin', 'output', 'rootScripts', 'workspaceScripts'],
|
|
20
23
|
elsewhere: "the package's own config",
|
|
21
24
|
},
|
|
22
25
|
workspace: {
|
|
@@ -28,6 +31,8 @@ const CONFIG_TIERS = {
|
|
|
28
31
|
const RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.workspace.honoredKeys];
|
|
29
32
|
const RECOGNIZED_BUILD_KEYS = ['extraIgnorePatterns'];
|
|
30
33
|
const RECOGNIZED_CHECK_CACHE_KEYS = ['enabled', 'excludeCommands', 'extraCommands'];
|
|
34
|
+
const RECOGNIZED_OUTPUT_KEYS = ['commandVerbosity', 'extraAgentEnvVars'];
|
|
35
|
+
const RECOGNIZED_STEP_KEYS = ['declinesArgs', 'run'];
|
|
31
36
|
export function resolveConfigPath(baseDir) {
|
|
32
37
|
return path.join(baseDir, CONFIG_DIR, CONFIG_FILENAME);
|
|
33
38
|
}
|
|
@@ -35,33 +40,46 @@ function isScriptRecord(value) {
|
|
|
35
40
|
if (!isObject(value))
|
|
36
41
|
return false;
|
|
37
42
|
for (const v of Object.values(value)) {
|
|
38
|
-
if (typeof v
|
|
39
|
-
|
|
40
|
-
if (Array.isArray(v)
|
|
43
|
+
if (typeof v === 'string')
|
|
44
|
+
continue;
|
|
45
|
+
if (!Array.isArray(v) || !v.every(isScriptElement))
|
|
41
46
|
return false;
|
|
42
47
|
}
|
|
43
48
|
return true;
|
|
44
49
|
}
|
|
50
|
+
function isScriptElement(value) {
|
|
51
|
+
if (typeof value === 'string')
|
|
52
|
+
return true;
|
|
53
|
+
if (!isObject(value) || typeof value['run'] !== 'string')
|
|
54
|
+
return false;
|
|
55
|
+
const declinesArgs = value['declinesArgs'];
|
|
56
|
+
return declinesArgs === undefined || typeof declinesArgs === 'boolean';
|
|
57
|
+
}
|
|
45
58
|
function validateScriptField(value, fieldName, configPath) {
|
|
46
59
|
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
47
60
|
return undefined;
|
|
48
61
|
}
|
|
49
62
|
const scripts = value[fieldName];
|
|
50
63
|
if (!isScriptRecord(scripts)) {
|
|
51
|
-
throw new
|
|
64
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | element[]>, ` +
|
|
65
|
+
'where an element is a command string or `{ run: string, declinesArgs?: boolean }`');
|
|
52
66
|
}
|
|
53
|
-
|
|
67
|
+
assertValidElements(scripts, fieldName, configPath);
|
|
54
68
|
return scripts;
|
|
55
69
|
}
|
|
56
|
-
function
|
|
70
|
+
function assertValidElements(scripts, fieldName, configPath) {
|
|
57
71
|
for (const [command, script] of Object.entries(scripts)) {
|
|
58
72
|
if (typeof script === 'string')
|
|
59
73
|
continue;
|
|
60
|
-
for (const
|
|
74
|
+
for (const spec of script) {
|
|
75
|
+
if (typeof spec !== 'string') {
|
|
76
|
+
assertRecognizedKeys(spec, RECOGNIZED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
77
|
+
}
|
|
78
|
+
const element = typeof spec === 'string' ? spec : spec.run;
|
|
61
79
|
const token = findUnexpressibleToken(element);
|
|
62
80
|
if (token === undefined)
|
|
63
81
|
continue;
|
|
64
|
-
throw new
|
|
82
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}.${command}\` element \`${element}\` carries ` +
|
|
65
83
|
`\`${token}\`, which is neither a command name nor an nmr flag. Name it as a script of its own and ` +
|
|
66
84
|
`reference that name here.`);
|
|
67
85
|
}
|
|
@@ -76,12 +94,12 @@ function validateBuildField(value, configPath) {
|
|
|
76
94
|
return undefined;
|
|
77
95
|
}
|
|
78
96
|
if (!isObject(build)) {
|
|
79
|
-
throw new
|
|
97
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`build\` must be an object`);
|
|
80
98
|
}
|
|
81
99
|
assertRecognizedKeys(build, RECOGNIZED_BUILD_KEYS, configPath, 'build.');
|
|
82
100
|
const extraIgnorePatterns = build['extraIgnorePatterns'];
|
|
83
101
|
if (extraIgnorePatterns !== undefined && !isStringArray(extraIgnorePatterns)) {
|
|
84
|
-
throw new
|
|
102
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`build.extraIgnorePatterns\` must be a string[]`);
|
|
85
103
|
}
|
|
86
104
|
return extraIgnorePatterns === undefined ? {} : { extraIgnorePatterns };
|
|
87
105
|
}
|
|
@@ -91,14 +109,14 @@ function validateCheckCacheField(value, configPath) {
|
|
|
91
109
|
return undefined;
|
|
92
110
|
}
|
|
93
111
|
if (!isObject(checkCache)) {
|
|
94
|
-
throw new
|
|
112
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache\` must be an object`);
|
|
95
113
|
}
|
|
96
114
|
assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, 'checkCache.');
|
|
97
115
|
const config = {};
|
|
98
116
|
const enabled = checkCache['enabled'];
|
|
99
117
|
if (enabled !== undefined) {
|
|
100
118
|
if (typeof enabled !== 'boolean') {
|
|
101
|
-
throw new
|
|
119
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
|
|
102
120
|
}
|
|
103
121
|
config.enabled = enabled;
|
|
104
122
|
}
|
|
@@ -108,24 +126,51 @@ function validateCheckCacheField(value, configPath) {
|
|
|
108
126
|
continue;
|
|
109
127
|
}
|
|
110
128
|
if (!isStringArray(commands)) {
|
|
111
|
-
throw new
|
|
129
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${field}\` must be a string[]`);
|
|
112
130
|
}
|
|
113
131
|
config[field] = commands;
|
|
114
132
|
}
|
|
115
133
|
return config;
|
|
116
134
|
}
|
|
135
|
+
function validateOutputField(value, configPath) {
|
|
136
|
+
const output = value['output'];
|
|
137
|
+
if (output === undefined) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
if (!isObject(output)) {
|
|
141
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`output\` must be an object`);
|
|
142
|
+
}
|
|
143
|
+
assertRecognizedKeys(output, RECOGNIZED_OUTPUT_KEYS, configPath, 'output.');
|
|
144
|
+
const config = {};
|
|
145
|
+
const commandVerbosity = output['commandVerbosity'];
|
|
146
|
+
if (commandVerbosity !== undefined) {
|
|
147
|
+
if (typeof commandVerbosity !== 'string' || !isCommandVerbosity(commandVerbosity)) {
|
|
148
|
+
const rendered = typeof commandVerbosity === 'string' ? commandVerbosity : JSON.stringify(commandVerbosity);
|
|
149
|
+
throw new UserError(`Invalid nmr config at ${configPath}: ${formatVerbosityRejection('`output.commandVerbosity`', rendered)}`);
|
|
150
|
+
}
|
|
151
|
+
config.commandVerbosity = commandVerbosity;
|
|
152
|
+
}
|
|
153
|
+
const extraAgentEnvVars = output['extraAgentEnvVars'];
|
|
154
|
+
if (extraAgentEnvVars !== undefined) {
|
|
155
|
+
if (!isStringArray(extraAgentEnvVars)) {
|
|
156
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`output.extraAgentEnvVars\` must be a string[]`);
|
|
157
|
+
}
|
|
158
|
+
config.extraAgentEnvVars = extraAgentEnvVars;
|
|
159
|
+
}
|
|
160
|
+
return config;
|
|
161
|
+
}
|
|
117
162
|
function validateStringRecordField(value, fieldName, configPath) {
|
|
118
163
|
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
119
164
|
return undefined;
|
|
120
165
|
}
|
|
121
166
|
if (!isStringRecord(value[fieldName])) {
|
|
122
|
-
throw new
|
|
167
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string>`);
|
|
123
168
|
}
|
|
124
169
|
return value[fieldName];
|
|
125
170
|
}
|
|
126
171
|
function validateConfig(value, configPath) {
|
|
127
172
|
if (!isObject(value)) {
|
|
128
|
-
throw new
|
|
173
|
+
throw new UserError(`Invalid nmr config at ${configPath}: expected an object, got ${typeof value}`);
|
|
129
174
|
}
|
|
130
175
|
assertRecognizedKeys(value, RECOGNIZED_KEYS, configPath);
|
|
131
176
|
const config = {};
|
|
@@ -138,6 +183,9 @@ function validateConfig(value, configPath) {
|
|
|
138
183
|
const devBin = validateStringRecordField(value, 'devBin', configPath);
|
|
139
184
|
if (devBin)
|
|
140
185
|
config.devBin = devBin;
|
|
186
|
+
const output = validateOutputField(value, configPath);
|
|
187
|
+
if (output)
|
|
188
|
+
config.output = output;
|
|
141
189
|
const workspaceScripts = validateScriptField(value, 'workspaceScripts', configPath);
|
|
142
190
|
if (workspaceScripts)
|
|
143
191
|
config.workspaceScripts = workspaceScripts;
|
|
@@ -170,7 +218,7 @@ function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = '') {
|
|
|
170
218
|
if (unrecognized.length === 0) {
|
|
171
219
|
return;
|
|
172
220
|
}
|
|
173
|
-
throw new
|
|
221
|
+
throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${unrecognized.length === 1 ? 'key' : 'keys'} ` +
|
|
174
222
|
`${formatKeyList(unrecognized, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`);
|
|
175
223
|
}
|
|
176
224
|
function assertTierKeys(config, tier, configPath) {
|
|
@@ -178,7 +226,7 @@ function assertTierKeys(config, tier, configPath) {
|
|
|
178
226
|
if (unsupported.length === 0) {
|
|
179
227
|
return;
|
|
180
228
|
}
|
|
181
|
-
throw new
|
|
229
|
+
throw new UserError(`Invalid nmr config at ${configPath}: ${tier.label} honors ${tier.honoredKeys.toSorted().join(', ')} alone, ` +
|
|
182
230
|
`not ${unsupported.toSorted().join(', ')}. Move those keys to ${tier.elsewhere}.`);
|
|
183
231
|
}
|
|
184
232
|
function formatKeyList(keys, prefix) {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface StepSpec {
|
|
2
|
+
run: string;
|
|
3
|
+
declinesArgs?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export type ScriptValue = string | ReadonlyArray<string | StepSpec>;
|
|
2
6
|
export type ScriptRegistry = Record<string, ScriptValue>;
|
|
3
7
|
export declare const workspaceScripts: ScriptRegistry;
|
|
4
8
|
export declare const rootScripts: ScriptRegistry;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
const GATE_PROJECTS = '--project unit --project tool';
|
|
2
|
+
const TYPECHECK_STEP = { run: 'typecheck', declinesArgs: true };
|
|
3
|
+
const ROOT_TYPECHECK_STEP = { run: 'root:typecheck', declinesArgs: true };
|
|
2
4
|
export const workspaceScripts = {
|
|
3
5
|
build: ['compile'],
|
|
4
|
-
check: [
|
|
5
|
-
'check:strict': [
|
|
6
|
+
check: [TYPECHECK_STEP, 'fmt:check', 'lint:check', 'test'],
|
|
7
|
+
'check:strict': [TYPECHECK_STEP, 'fmt:check', 'lint:strict', 'test:coverage'],
|
|
6
8
|
clean: 'nmr-clean',
|
|
7
9
|
compile: 'nmr-compile',
|
|
8
10
|
fix: ['lint', 'fmt'],
|
|
@@ -12,6 +14,7 @@ export const workspaceScripts = {
|
|
|
12
14
|
lint: 'eslint --fix .',
|
|
13
15
|
'lint:check': 'eslint .',
|
|
14
16
|
'lint:strict': 'strict-lint',
|
|
17
|
+
'report-catalog': 'nmr-report-catalog',
|
|
15
18
|
test: `pnpm exec vitest ${GATE_PROJECTS}`,
|
|
16
19
|
'test:all': 'pnpm exec vitest',
|
|
17
20
|
'test:coverage': `pnpm exec vitest ${GATE_PROJECTS} --coverage`,
|
|
@@ -19,7 +22,7 @@ export const workspaceScripts = {
|
|
|
19
22
|
'test:unit': 'pnpm exec vitest --project unit',
|
|
20
23
|
'test:watch': `pnpm exec vitest ${GATE_PROJECTS} --watch`,
|
|
21
24
|
typecheck: 'tsgo --noEmit',
|
|
22
|
-
upgrade: 'nmr-taze --include-locked',
|
|
25
|
+
upgrade: 'nmr-report-catalog && nmr-taze --include-locked',
|
|
23
26
|
'view-coverage': 'open coverage/index.html',
|
|
24
27
|
};
|
|
25
28
|
export const rootScripts = {
|
|
@@ -27,9 +30,9 @@ export const rootScripts = {
|
|
|
27
30
|
'audit:dev': 'pnpm exec v11y --dev',
|
|
28
31
|
'audit:prod': 'pnpm exec v11y --prod',
|
|
29
32
|
build: ['-R build'],
|
|
30
|
-
check: [
|
|
31
|
-
'check:strict': [
|
|
32
|
-
ci: ['build', 'check:strict'],
|
|
33
|
+
check: [TYPECHECK_STEP, 'fmt:check', 'lint:check', 'test'],
|
|
34
|
+
'check:strict': [TYPECHECK_STEP, 'fmt:check', 'lint:strict', 'test:coverage'],
|
|
35
|
+
ci: [{ run: 'build', declinesArgs: true }, 'check:strict'],
|
|
33
36
|
clean: 'nmr-clean',
|
|
34
37
|
fix: ['lint', 'fmt'],
|
|
35
38
|
'fix:check': ['fmt:check', 'lint:check'],
|
|
@@ -38,9 +41,9 @@ export const rootScripts = {
|
|
|
38
41
|
lint: 'eslint --fix .',
|
|
39
42
|
'lint:check': 'eslint .',
|
|
40
43
|
'lint:strict': 'strict-lint',
|
|
41
|
-
prepush: ['audit', 'ci'],
|
|
44
|
+
prepush: [{ run: 'audit', declinesArgs: true }, 'ci'],
|
|
42
45
|
'report-overrides': 'nmr-report-overrides',
|
|
43
|
-
'root:check': [
|
|
46
|
+
'root:check': [ROOT_TYPECHECK_STEP, 'fmt:check', 'root:lint:check', 'root:test'],
|
|
44
47
|
'root:lint': "eslint --fix --ignore-pattern 'packages/**' .",
|
|
45
48
|
'root:lint:check': "eslint --ignore-pattern 'packages/**' .",
|
|
46
49
|
'root:lint:strict': "strict-lint --ignore-pattern 'packages/**' .",
|
|
@@ -49,13 +52,13 @@ export const rootScripts = {
|
|
|
49
52
|
'root:test:tool': 'vitest --config ./vitest.root.config.ts --project tool',
|
|
50
53
|
'root:test:unit': 'vitest --config ./vitest.root.config.ts --project unit',
|
|
51
54
|
'root:typecheck': 'tsgo --noEmit',
|
|
52
|
-
'root:upgrade': 'nmr-taze --include-locked',
|
|
55
|
+
'root:upgrade': 'nmr-report-overrides && nmr-taze --include-locked',
|
|
53
56
|
test: ['root:test', '-R test'],
|
|
54
57
|
'test:all': ['root:test:all', '-R test:all'],
|
|
55
58
|
'test:coverage': ['root:test', '-R test:coverage'],
|
|
56
59
|
'test:tool': ['root:test:tool', '-R test:tool'],
|
|
57
60
|
'test:unit': ['root:test:unit', '-R test:unit'],
|
|
58
61
|
'test:watch': `vitest ${GATE_PROJECTS} --watch`,
|
|
59
|
-
typecheck: [
|
|
62
|
+
typecheck: [ROOT_TYPECHECK_STEP, { run: '-R typecheck', declinesArgs: true }],
|
|
60
63
|
upgrade: 'nmr-report-overrides && nmr-taze --include-locked --recursive',
|
|
61
64
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { NmrConfig } from './types.js';
|
|
2
|
-
export type { BuildConfig, CheckCacheConfig, NmrConfig } from './types.js';
|
|
2
|
+
export type { BuildConfig, CheckCacheConfig, CommandVerbosity, NmrConfig, OutputConfig, ScriptValue, StepSpec, } from './types.js';
|
|
3
3
|
export declare function defineConfig(config: NmrConfig): NmrConfig;
|
package/dist/esm/help.js
CHANGED
|
@@ -8,7 +8,9 @@ export function generateHelp(config, packageDir, useRoot) {
|
|
|
8
8
|
' -F, --filter <pattern> Run command in matching packages',
|
|
9
9
|
' -R, --recursive Run command in all packages',
|
|
10
10
|
' -w, --workspace-root Use root scripts, running at the monorepo root',
|
|
11
|
-
|
|
11
|
+
" -q, --quiet Suppress command output, keeping nmr's verdicts",
|
|
12
|
+
' --json Report one JSON object per command, withholding command output',
|
|
13
|
+
' --log Print the recorded run instead of running (up to 256 KiB)',
|
|
12
14
|
' --no-cache Run even if this tree already passed; record the result',
|
|
13
15
|
' -?, --help Show this help',
|
|
14
16
|
' -V, --version Show version number',
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const TRUNCATION_MARK = '…';
|
|
2
|
+
export function clampToBytes(value, budgetBytes) {
|
|
3
|
+
if (Buffer.byteLength(value) <= budgetBytes) {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
const budget = budgetBytes - Buffer.byteLength(TRUNCATION_MARK);
|
|
7
|
+
if (budget < 0) {
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
let kept = '';
|
|
11
|
+
let bytes = 0;
|
|
12
|
+
for (const character of value) {
|
|
13
|
+
const size = Buffer.byteLength(character);
|
|
14
|
+
if (bytes + size > budget)
|
|
15
|
+
break;
|
|
16
|
+
kept += character;
|
|
17
|
+
bytes += size;
|
|
18
|
+
}
|
|
19
|
+
return `${kept}${TRUNCATION_MARK}`;
|
|
20
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatElisionMarker } from "./transcript.js";
|
|
1
2
|
const DEFAULT_HEAD_LIMIT_BYTES = 1_048_576;
|
|
2
3
|
const DEFAULT_TAIL_LIMIT_BYTES = 1_048_576;
|
|
3
4
|
export function createBoundedBuffer(options = {}) {
|
|
@@ -54,7 +55,3 @@ export function createBoundedBuffer(options = {}) {
|
|
|
54
55
|
}
|
|
55
56
|
return { append, toBuffer };
|
|
56
57
|
}
|
|
57
|
-
function formatElisionMarker(byteCount) {
|
|
58
|
-
const unit = byteCount === 1 ? 'byte' : 'bytes';
|
|
59
|
-
return `\n… nmr elided ${byteCount.toLocaleString('en-US')} ${unit} …\n`;
|
|
60
|
-
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function deriveExcerpt(transcript: string): string | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { clampToBytes } from "./clampToBytes.js";
|
|
2
|
+
import { cleanTranscript } from "./transcript.js";
|
|
3
|
+
const MAX_BLOCK_LINES = 8;
|
|
4
|
+
const MAX_EXCERPT_BYTES = 2_048;
|
|
5
|
+
const NON_RULE_CHARACTER = /[^-=_~*+.|:#]/u;
|
|
6
|
+
export function deriveExcerpt(transcript) {
|
|
7
|
+
const lines = cleanTranscript(transcript).split('\n');
|
|
8
|
+
let end = lines.length;
|
|
9
|
+
while (end > 0 && isBlank(lines[end - 1])) {
|
|
10
|
+
end--;
|
|
11
|
+
}
|
|
12
|
+
let start = end;
|
|
13
|
+
while (start > 0 && !isBlank(lines[start - 1])) {
|
|
14
|
+
start--;
|
|
15
|
+
}
|
|
16
|
+
const excerpt = lines
|
|
17
|
+
.slice(Math.max(start, end - MAX_BLOCK_LINES), end)
|
|
18
|
+
.filter((line) => !isRuleOnly(line))
|
|
19
|
+
.join(' ')
|
|
20
|
+
.replaceAll(/\s+/gu, ' ')
|
|
21
|
+
.trim();
|
|
22
|
+
return excerpt === '' ? undefined : clampToBytes(excerpt, MAX_EXCERPT_BYTES);
|
|
23
|
+
}
|
|
24
|
+
function isBlank(line) {
|
|
25
|
+
return line === undefined || line.trim() === '';
|
|
26
|
+
}
|
|
27
|
+
function isRuleOnly(line) {
|
|
28
|
+
const content = line.replaceAll(/\s/gu, '');
|
|
29
|
+
return content !== '' && !NON_RULE_CHARACTER.test(content);
|
|
30
|
+
}
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
export function formatDuration(milliseconds) {
|
|
2
|
-
const
|
|
3
|
-
if (
|
|
4
|
-
return `${
|
|
2
|
+
const total = Math.max(0, milliseconds);
|
|
3
|
+
if (total < MILLISECONDS_PER_MINUTE) {
|
|
4
|
+
return `${truncateToTenth(total)}s`;
|
|
5
5
|
}
|
|
6
|
-
const
|
|
7
|
-
if (
|
|
8
|
-
|
|
6
|
+
const totalMinutes = Math.floor(total / MILLISECONDS_PER_MINUTE);
|
|
7
|
+
if (totalMinutes < MINUTES_PER_HOUR) {
|
|
8
|
+
const seconds = Math.floor(total / MILLISECONDS_PER_SECOND) % SECONDS_PER_MINUTE;
|
|
9
|
+
return joinUnits(`${totalMinutes}m`, seconds, 's');
|
|
9
10
|
}
|
|
10
|
-
return `${Math.
|
|
11
|
+
return joinUnits(`${Math.floor(totalMinutes / MINUTES_PER_HOUR)}h`, totalMinutes % MINUTES_PER_HOUR, 'm');
|
|
11
12
|
}
|
|
12
13
|
export function formatSaving(milliseconds) {
|
|
13
14
|
if (!Number.isFinite(milliseconds) || milliseconds < MILLISECONDS_PER_SECOND) {
|
|
14
15
|
return undefined;
|
|
15
16
|
}
|
|
16
|
-
return
|
|
17
|
+
return `saved ~${formatDuration(milliseconds)}`;
|
|
17
18
|
}
|
|
19
|
+
const MILLISECONDS_PER_MINUTE = 60_000;
|
|
18
20
|
const MILLISECONDS_PER_SECOND = 1_000;
|
|
19
21
|
const MINUTES_PER_HOUR = 60;
|
|
20
22
|
const SECONDS_PER_MINUTE = 60;
|
|
21
|
-
|
|
23
|
+
function joinUnits(major, minor, minorUnit) {
|
|
24
|
+
return minor === 0 ? major : `${major} ${minor}${minorUnit}`;
|
|
25
|
+
}
|
|
26
|
+
function truncateToTenth(milliseconds) {
|
|
27
|
+
return Math.floor(milliseconds / 100) / 10;
|
|
28
|
+
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const DEPENDENCY_FIELDS: readonly ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
2
|
+
export type DependencyField = (typeof DEPENDENCY_FIELDS)[number];
|
|
3
|
+
export type PackageJson = {
|
|
2
4
|
name?: string;
|
|
3
5
|
private?: boolean;
|
|
4
6
|
version?: string;
|
|
5
7
|
packageManager?: string;
|
|
6
8
|
scripts?: Record<string, string>;
|
|
7
9
|
pnpm?: {
|
|
8
|
-
overrides?: Record<string,
|
|
10
|
+
overrides?: Record<string, unknown>;
|
|
9
11
|
};
|
|
10
|
-
}
|
|
12
|
+
} & {
|
|
13
|
+
[K in DependencyField]?: Record<string, string>;
|
|
14
|
+
};
|
|
11
15
|
export declare function readPackageJson(dir: string): PackageJson;
|
|
12
|
-
export declare function getPnpmOverrides(pkg: PackageJson): Record<string,
|
|
16
|
+
export declare function getPnpmOverrides(pkg: PackageJson): Record<string, unknown> | undefined;
|
|
17
|
+
export declare function parsePackageJson(raw: string, file: string): unknown;
|
|
18
|
+
export declare function readScriptRecord(dir: string, scripts: Record<string, unknown>): Record<string, string>;
|
|
19
|
+
export declare function resolvePackageJsonPath(dir: string): string;
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
|
+
import { CONFIG_RELATIVE_PATH } from "../config.js";
|
|
5
|
+
import { UserError } from "../UserError.js";
|
|
6
|
+
import { isMonorepoRoot } from "../workspace.js";
|
|
7
|
+
import { readStringValues } from "./readStringValues.js";
|
|
3
8
|
import { isObject } from "./type-guards.js";
|
|
9
|
+
export const DEPENDENCY_FIELDS = [
|
|
10
|
+
'dependencies',
|
|
11
|
+
'devDependencies',
|
|
12
|
+
'optionalDependencies',
|
|
13
|
+
'peerDependencies',
|
|
14
|
+
];
|
|
4
15
|
export function readPackageJson(dir) {
|
|
5
|
-
const
|
|
16
|
+
const file = resolvePackageJsonPath(dir);
|
|
17
|
+
const parsed = parsePackageJson(readFileSync(file, 'utf8'), file);
|
|
6
18
|
if (!isObject(parsed)) {
|
|
7
|
-
throw new
|
|
19
|
+
throw new UserError(`Invalid package.json in ${dir}: expected an object`);
|
|
8
20
|
}
|
|
9
21
|
const pkg = {};
|
|
10
22
|
if (typeof parsed['name'] === 'string')
|
|
@@ -16,24 +28,18 @@ export function readPackageJson(dir) {
|
|
|
16
28
|
if (typeof parsed['packageManager'] === 'string')
|
|
17
29
|
pkg.packageManager = parsed['packageManager'];
|
|
18
30
|
if (isObject(parsed['scripts'])) {
|
|
19
|
-
|
|
20
|
-
const scriptEntries = Object.entries(parsed['scripts']);
|
|
21
|
-
for (const [key, val] of scriptEntries) {
|
|
22
|
-
if (typeof val === 'string')
|
|
23
|
-
scripts[key] = val;
|
|
24
|
-
}
|
|
25
|
-
pkg.scripts = scripts;
|
|
31
|
+
pkg.scripts = readScriptRecord(dir, parsed['scripts']);
|
|
26
32
|
}
|
|
27
33
|
if (isObject(parsed['pnpm'])) {
|
|
28
34
|
const pnpm = parsed['pnpm'];
|
|
29
35
|
if (isObject(pnpm['overrides'])) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
pkg
|
|
36
|
+
pkg.pnpm = { overrides: pnpm['overrides'] };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
40
|
+
const declared = parsed[field];
|
|
41
|
+
if (isObject(declared)) {
|
|
42
|
+
pkg[field] = readStringValues(declared);
|
|
37
43
|
}
|
|
38
44
|
}
|
|
39
45
|
return pkg;
|
|
@@ -42,11 +48,34 @@ export function getPnpmOverrides(pkg) {
|
|
|
42
48
|
if (!isObject(pkg.pnpm))
|
|
43
49
|
return undefined;
|
|
44
50
|
const overrides = pkg.pnpm.overrides;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
return isObject(overrides) ? overrides : undefined;
|
|
52
|
+
}
|
|
53
|
+
export function parsePackageJson(raw, file) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(raw);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new UserError(`Invalid package.json at ${file}: ${describeError(error)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function readScriptRecord(dir, scripts) {
|
|
62
|
+
const result = {};
|
|
63
|
+
for (const [key, value] of Object.entries(scripts)) {
|
|
64
|
+
if (typeof value !== 'string') {
|
|
65
|
+
throw new UserError(formatMalformedScript(dir, key, value));
|
|
66
|
+
}
|
|
67
|
+
result[key] = value;
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
export function resolvePackageJsonPath(dir) {
|
|
72
|
+
return path.join(dir, 'package.json');
|
|
73
|
+
}
|
|
74
|
+
function formatMalformedScript(dir, key, value) {
|
|
75
|
+
const rejection = `Invalid package.json at ${resolvePackageJsonPath(dir)}: \`scripts.${key}\` must be a string.`;
|
|
76
|
+
if (!Array.isArray(value)) {
|
|
77
|
+
return rejection;
|
|
50
78
|
}
|
|
51
|
-
|
|
79
|
+
const field = isMonorepoRoot(dir) ? 'rootScripts' : 'workspaceScripts';
|
|
80
|
+
return `${rejection} A step list belongs in \`${CONFIG_RELATIVE_PATH}\` under \`${field}\`.`;
|
|
52
81
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readStringValues(record: Record<string, unknown>): Record<string, string>;
|