@williamthorsen/nmr 0.30.0 → 0.31.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 +27 -16
- package/.readyup/manifest.json +2 -2
- package/README.md +299 -62
- package/agents/guidance/rulebooks/nmr.md +4 -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 +2 -1
- 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/report-catalog.d.ts +1 -0
- package/dist/esm/commands/report-catalog.js +34 -0
- package/dist/esm/commands/report-overrides.js +22 -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/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 +26 -17
- 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 +12 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Writable } from 'node:stream';
|
|
2
|
+
import type { OutputChannels } from './runner.js';
|
|
2
3
|
import type { CheckCacheConfig, NmrConfig } from './types.js';
|
|
3
4
|
export interface CheckCacheEntry {
|
|
4
5
|
key: string;
|
|
@@ -10,6 +11,22 @@ export interface CheckCacheEntry {
|
|
|
10
11
|
durationMs: number;
|
|
11
12
|
recordedAt: string;
|
|
12
13
|
buildDigests: Record<string, string>;
|
|
14
|
+
retention?: Retention;
|
|
15
|
+
}
|
|
16
|
+
export interface EntryRef {
|
|
17
|
+
anchorDir: string;
|
|
18
|
+
command: string;
|
|
19
|
+
monorepoRoot: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ReplayLine {
|
|
22
|
+
command: string;
|
|
23
|
+
excerpt: string;
|
|
24
|
+
scope: string;
|
|
25
|
+
}
|
|
26
|
+
export interface Retention {
|
|
27
|
+
key: string;
|
|
28
|
+
replay: ReplayLine[];
|
|
29
|
+
runId: string;
|
|
13
30
|
}
|
|
14
31
|
export interface BuildOutputState {
|
|
15
32
|
missing: string[];
|
|
@@ -28,7 +45,17 @@ export declare const CURRENT_RUNTIME: RuntimeIdentity;
|
|
|
28
45
|
export declare const DEBUG_ENV_VAR = "NMR_DEBUG";
|
|
29
46
|
export declare const DEFAULT_CACHEABLE_COMMANDS: string[];
|
|
30
47
|
export declare const NO_CACHE_ENV_VAR = "NMR_NO_CACHE";
|
|
48
|
+
export declare const RUN_ID_ENV_VAR = "NMR_RUN_ID";
|
|
31
49
|
export declare const TREE_SNAPSHOT_ENV_VAR = "NMR_TREE_SNAPSHOT";
|
|
50
|
+
export declare function certifyRetention(options: {
|
|
51
|
+
anchorDir: string;
|
|
52
|
+
command: string;
|
|
53
|
+
entry: CheckCacheEntry;
|
|
54
|
+
env: NodeJS.ProcessEnv;
|
|
55
|
+
monorepoRoot: string;
|
|
56
|
+
runId: string;
|
|
57
|
+
stderr: Writable;
|
|
58
|
+
}): Promise<void>;
|
|
32
59
|
export declare function computeCacheKey(options: {
|
|
33
60
|
anchorDir: string;
|
|
34
61
|
command: string;
|
|
@@ -45,18 +72,26 @@ export declare function computeCacheKey(options: {
|
|
|
45
72
|
ok: false;
|
|
46
73
|
reason: string;
|
|
47
74
|
};
|
|
75
|
+
export declare function computeRetentionKey(options: {
|
|
76
|
+
channels: OutputChannels;
|
|
77
|
+
env: NodeJS.ProcessEnv;
|
|
78
|
+
passKey: string;
|
|
79
|
+
}): string;
|
|
48
80
|
export declare function encodeTreeSnapshot(snapshot: TreeSnapshot): string;
|
|
49
81
|
export declare function findStaleBuildOutput(earlier: Record<string, string>, later: Record<string, string>): string | undefined;
|
|
50
82
|
export declare function formatMisplacedNoCacheWarning(command: string): string;
|
|
51
|
-
export declare function
|
|
83
|
+
export declare function isCacheableCommand(checkCache: CheckCacheConfig | undefined, command: string): boolean;
|
|
52
84
|
export declare function readBuildOutputState(monorepoRoot: string, config: NmrConfig): Promise<BuildOutputState>;
|
|
53
85
|
export declare function readCheckCacheEntry(options: {
|
|
54
86
|
anchorDir: string;
|
|
55
87
|
command: string;
|
|
56
88
|
monorepoRoot: string;
|
|
57
89
|
}): Promise<CheckCacheEntry | undefined>;
|
|
90
|
+
export declare function readTranscript(ref: EntryRef): Promise<string | undefined>;
|
|
91
|
+
export declare function recordTranscript(ref: EntryRef, transcript: string | undefined): Promise<void>;
|
|
58
92
|
export declare function removeCheckCache(scopeDir: string): Promise<void>;
|
|
59
93
|
export declare function resolveCacheableCommands(checkCache: CheckCacheConfig | undefined): Set<string>;
|
|
94
|
+
export declare function resolveRunId(env: NodeJS.ProcessEnv): string;
|
|
60
95
|
export declare function resolveTreeSnapshot(options: {
|
|
61
96
|
monorepoRoot: string;
|
|
62
97
|
env: NodeJS.ProcessEnv;
|
package/dist/esm/check-cache.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { readFileSync, realpathSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import process from 'node:process';
|
|
5
|
-
import { hashWorkingTree, readHeadSha, readJsonCacheEntry, removeCacheDir, resolveCacheEntryPath, writeCacheEntry, } from '@williamthorsen/nmr-core';
|
|
5
|
+
import { hashWorkingTree, readCacheEntry, readHeadSha, readJsonCacheEntry, removeCacheDir, removeCacheEntry, resolveCacheEntryPath, writeCacheEntry, } from '@williamthorsen/nmr-core';
|
|
6
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
6
7
|
import { hasBuildOutput, readBuildDigest } from "./commands/build-output.js";
|
|
7
8
|
import { loadWorkspaceConfig } from "./config.js";
|
|
8
|
-
import {
|
|
9
|
+
import { isHookName } from "./helpers/hook-name.js";
|
|
9
10
|
import { isObject, isStringRecord } from "./helpers/type-guards.js";
|
|
10
11
|
import { getDefaultWorkspaceScripts } from "./resolve-scripts.js";
|
|
11
12
|
import { buildWorkspaceRegistry, resolveScript } from "./resolver.js";
|
|
@@ -39,6 +40,7 @@ export const DEFAULT_CACHEABLE_COMMANDS = [
|
|
|
39
40
|
'typecheck',
|
|
40
41
|
];
|
|
41
42
|
export const NO_CACHE_ENV_VAR = 'NMR_NO_CACHE';
|
|
43
|
+
export const RUN_ID_ENV_VAR = 'NMR_RUN_ID';
|
|
42
44
|
export const TREE_SNAPSHOT_ENV_VAR = 'NMR_TREE_SNAPSHOT';
|
|
43
45
|
const BUILT_IN_COMPILE = 'nmr-compile';
|
|
44
46
|
const CACHE_TOOL = 'nmr-check';
|
|
@@ -48,7 +50,26 @@ const INSTALL_FINGERPRINT_FILES = [
|
|
|
48
50
|
];
|
|
49
51
|
const KEY_FORMAT = 'nmr-check-cache-v1';
|
|
50
52
|
const KEYED_ENV_VARS = ['LANG', 'LC_ALL', 'NODE_OPTIONS', 'TZ'];
|
|
53
|
+
const RETENTION_KEY_FORMAT = 'nmr-retention-v1';
|
|
54
|
+
const RETENTION_KEYED_ENV_VARS = ['CI', 'COLUMNS', 'FORCE_COLOR', 'NO_COLOR', 'TERM'];
|
|
51
55
|
const UNSAFE_SLUG_CHARACTERS = /[^\w.-]+/g;
|
|
56
|
+
export async function certifyRetention(options) {
|
|
57
|
+
const { entry, runId } = options;
|
|
58
|
+
if (entry.retention === undefined || entry.retention.runId === runId) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
await writeCheckCacheEntry({
|
|
63
|
+
anchorDir: options.anchorDir,
|
|
64
|
+
command: options.command,
|
|
65
|
+
monorepoRoot: options.monorepoRoot,
|
|
66
|
+
entry: { ...entry, retention: { ...entry.retention, runId } },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
writeDebugNote(`could not certify ${options.command}: ${describeError(error)}`, options.env, options.stderr);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
52
73
|
export function computeCacheKey(options) {
|
|
53
74
|
const fingerprint = resolveInstallFingerprint(options.monorepoRoot);
|
|
54
75
|
if (!fingerprint.ok) {
|
|
@@ -68,16 +89,16 @@ export function computeCacheKey(options) {
|
|
|
68
89
|
runtime.arch,
|
|
69
90
|
fingerprint.fingerprint,
|
|
70
91
|
];
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return
|
|
92
|
+
return { ok: true, key: digestParts([...parts, ...composeEnvParts(KEYED_ENV_VARS, options.env)]) };
|
|
93
|
+
}
|
|
94
|
+
export function computeRetentionKey(options) {
|
|
95
|
+
const parts = [
|
|
96
|
+
RETENTION_KEY_FORMAT,
|
|
97
|
+
options.passKey,
|
|
98
|
+
describeChannel(options.channels.stdout),
|
|
99
|
+
describeChannel(options.channels.stderr),
|
|
100
|
+
];
|
|
101
|
+
return digestParts([...parts, ...composeEnvParts(RETENTION_KEYED_ENV_VARS, options.env)]);
|
|
81
102
|
}
|
|
82
103
|
export function encodeTreeSnapshot(snapshot) {
|
|
83
104
|
return `${snapshot.hash} ${snapshot.headSha}`;
|
|
@@ -90,11 +111,8 @@ export function formatMisplacedNoCacheWarning(command) {
|
|
|
90
111
|
return (`⚠️ --no-cache after the command name is passed to \`${command}\`, not read by nmr. ` +
|
|
91
112
|
`Did you mean \`nmr --no-cache ${command}\`?`);
|
|
92
113
|
}
|
|
93
|
-
export function
|
|
94
|
-
|
|
95
|
-
const saving = formatSaving(entry.durationMs);
|
|
96
|
-
const savingClause = saving === undefined ? '' : ` (${saving})`;
|
|
97
|
-
return `⏭️ ${command}: passed ${age} ago on this tree${savingClause}.`;
|
|
114
|
+
export function isCacheableCommand(checkCache, command) {
|
|
115
|
+
return !isHookName(command) && checkCache?.enabled !== false && resolveCacheableCommands(checkCache).has(command);
|
|
98
116
|
}
|
|
99
117
|
export async function readBuildOutputState(monorepoRoot, config) {
|
|
100
118
|
const state = { missing: [], digests: {} };
|
|
@@ -126,7 +144,23 @@ export async function readBuildOutputState(monorepoRoot, config) {
|
|
|
126
144
|
return state;
|
|
127
145
|
}
|
|
128
146
|
export async function readCheckCacheEntry(options) {
|
|
129
|
-
|
|
147
|
+
const parsed = await readJsonCacheEntry(resolveEntryPath(options), isParsedCheckCacheEntry);
|
|
148
|
+
if (parsed === undefined) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const { retention, ...pass } = parsed;
|
|
152
|
+
return isRetention(retention) ? { ...pass, retention } : pass;
|
|
153
|
+
}
|
|
154
|
+
export async function readTranscript(ref) {
|
|
155
|
+
return readCacheEntry(resolveEntryPath(ref, '.log'));
|
|
156
|
+
}
|
|
157
|
+
export async function recordTranscript(ref, transcript) {
|
|
158
|
+
const entryPath = resolveEntryPath(ref, '.log');
|
|
159
|
+
if (transcript === undefined) {
|
|
160
|
+
await removeCacheEntry(entryPath);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
await writeCacheEntry(entryPath, transcript);
|
|
130
164
|
}
|
|
131
165
|
export async function removeCheckCache(scopeDir) {
|
|
132
166
|
await removeCacheDir({ tool: CACHE_TOOL, scopeDir });
|
|
@@ -139,6 +173,10 @@ export function resolveCacheableCommands(checkCache) {
|
|
|
139
173
|
}
|
|
140
174
|
return commands;
|
|
141
175
|
}
|
|
176
|
+
export function resolveRunId(env) {
|
|
177
|
+
const inherited = env[RUN_ID_ENV_VAR];
|
|
178
|
+
return inherited === undefined || inherited === '' ? randomUUID() : inherited;
|
|
179
|
+
}
|
|
142
180
|
export function resolveTreeSnapshot(options) {
|
|
143
181
|
const inherited = decodeTreeSnapshot(options.env[TREE_SNAPSHOT_ENV_VAR]);
|
|
144
182
|
if (inherited !== undefined && readHeadSha(options.monorepoRoot) === inherited.headSha) {
|
|
@@ -168,6 +206,12 @@ export function writeDebugNote(message, env, stderr) {
|
|
|
168
206
|
stderr.write(`nmr check-cache: ${message}\n`);
|
|
169
207
|
}
|
|
170
208
|
}
|
|
209
|
+
function composeEnvParts(names, env) {
|
|
210
|
+
return names.flatMap((name) => {
|
|
211
|
+
const value = env[name];
|
|
212
|
+
return [name, value === undefined ? 'unset' : 'set', value ?? ''];
|
|
213
|
+
});
|
|
214
|
+
}
|
|
171
215
|
function decodeTreeSnapshot(encoded) {
|
|
172
216
|
if (encoded === undefined) {
|
|
173
217
|
return undefined;
|
|
@@ -178,7 +222,18 @@ function decodeTreeSnapshot(encoded) {
|
|
|
178
222
|
}
|
|
179
223
|
return { hash, headSha };
|
|
180
224
|
}
|
|
181
|
-
function
|
|
225
|
+
function describeChannel(channel) {
|
|
226
|
+
return channel === 'pipe' ? 'pipe' : 'descriptor';
|
|
227
|
+
}
|
|
228
|
+
function digestParts(parts) {
|
|
229
|
+
const hash = createHash('sha256');
|
|
230
|
+
for (const part of parts) {
|
|
231
|
+
hash.update(part);
|
|
232
|
+
hash.update('\0');
|
|
233
|
+
}
|
|
234
|
+
return hash.digest('hex');
|
|
235
|
+
}
|
|
236
|
+
function isParsedCheckCacheEntry(value) {
|
|
182
237
|
if (!isObject(value)) {
|
|
183
238
|
return false;
|
|
184
239
|
}
|
|
@@ -193,19 +248,32 @@ function isCheckCacheEntry(value) {
|
|
|
193
248
|
}
|
|
194
249
|
function isProbeSubject(packageDir, registry) {
|
|
195
250
|
const build = resolveScript('build', registry, packageDir, false);
|
|
196
|
-
if (build === undefined || build.
|
|
251
|
+
if (build === undefined || build.origin.tier === 'package') {
|
|
197
252
|
return false;
|
|
198
253
|
}
|
|
199
254
|
const compile = resolveScript('compile', registry, packageDir, false);
|
|
200
255
|
return compile !== undefined && renderChain(compile.steps) === BUILT_IN_COMPILE;
|
|
201
256
|
}
|
|
202
|
-
function
|
|
257
|
+
function isReplayLine(value) {
|
|
258
|
+
return (isObject(value) &&
|
|
259
|
+
typeof value['command'] === 'string' &&
|
|
260
|
+
typeof value['excerpt'] === 'string' &&
|
|
261
|
+
typeof value['scope'] === 'string');
|
|
262
|
+
}
|
|
263
|
+
function isRetention(value) {
|
|
264
|
+
return (isObject(value) &&
|
|
265
|
+
typeof value['key'] === 'string' &&
|
|
266
|
+
typeof value['runId'] === 'string' &&
|
|
267
|
+
Array.isArray(value['replay']) &&
|
|
268
|
+
value['replay'].every((line) => isReplayLine(line)));
|
|
269
|
+
}
|
|
270
|
+
function resolveEntryPath(options, extension = '.json') {
|
|
203
271
|
const anchorDir = path.resolve(options.anchorDir);
|
|
204
272
|
const ref = {
|
|
205
273
|
tool: CACHE_TOOL,
|
|
206
274
|
scopeDir: options.monorepoRoot,
|
|
207
275
|
slug: `${path.basename(anchorDir)}-${options.command.replaceAll(UNSAFE_SLUG_CHARACTERS, '-')}`,
|
|
208
|
-
extension
|
|
276
|
+
extension,
|
|
209
277
|
discriminators: [anchorDir, options.command],
|
|
210
278
|
};
|
|
211
279
|
return resolveCacheEntryPath(ref);
|
package/dist/esm/cli-build.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
2
3
|
import { buildPackage } from "./commands/build.js";
|
|
3
4
|
import { loadWorkspaceConfig } from "./config.js";
|
|
4
5
|
try {
|
|
@@ -9,6 +10,6 @@ try {
|
|
|
9
10
|
});
|
|
10
11
|
}
|
|
11
12
|
catch (error) {
|
|
12
|
-
reportError(
|
|
13
|
+
reportError(describeError(error));
|
|
13
14
|
process.exitCode = 1;
|
|
14
15
|
}
|
package/dist/esm/cli-clean.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
2
3
|
import { runClean } from "./commands/clean.js";
|
|
3
4
|
try {
|
|
4
5
|
await runClean(process.cwd());
|
|
5
6
|
}
|
|
6
7
|
catch (error) {
|
|
7
|
-
reportError(
|
|
8
|
+
reportError(describeError(error));
|
|
8
9
|
process.exitCode = 1;
|
|
9
10
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
2
3
|
import { DEFAULT_HOOK, ensurePrepublishHooks } from "./commands/ensure-prepublish-hooks.js";
|
|
3
4
|
import { findMonorepoRoot } from "./workspace.js";
|
|
4
5
|
const flagSchema = {
|
|
@@ -38,6 +39,6 @@ try {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
catch (error) {
|
|
41
|
-
reportError(
|
|
42
|
+
reportError(describeError(error));
|
|
42
43
|
process.exitCode = 1;
|
|
43
44
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { reportError } from '@williamthorsen/nmr-core';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
|
+
import { reportCatalog } from "./commands/report-catalog.js";
|
|
5
|
+
try {
|
|
6
|
+
reportCatalog(process.cwd());
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
reportError(describeError(error));
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
2
3
|
import { reportOverrides } from "./commands/report-overrides.js";
|
|
3
4
|
import { findMonorepoRoot } from "./workspace.js";
|
|
4
5
|
try {
|
|
@@ -6,6 +7,6 @@ try {
|
|
|
6
7
|
reportOverrides(monorepoRoot);
|
|
7
8
|
}
|
|
8
9
|
catch (error) {
|
|
9
|
-
reportError(
|
|
10
|
+
reportError(describeError(error));
|
|
10
11
|
process.exitCode = 1;
|
|
11
12
|
}
|
package/dist/esm/cli-taze.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
2
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
4
|
import { runTaze } from "./commands/taze.js";
|
|
4
5
|
try {
|
|
5
6
|
process.exitCode = runTaze(process.argv.slice(2));
|
|
6
7
|
}
|
|
7
8
|
catch (error) {
|
|
8
|
-
reportError(
|
|
9
|
+
reportError(describeError(error));
|
|
9
10
|
process.exitCode = 1;
|
|
10
11
|
}
|
package/dist/esm/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
+
import { reportCliFailure } from "./reportCliFailure.js";
|
|
2
3
|
import { runCli } from "./runCli.js";
|
|
3
4
|
try {
|
|
4
5
|
const { exitCode } = await runCli({
|
|
@@ -11,6 +12,6 @@ try {
|
|
|
11
12
|
process.exitCode = exitCode;
|
|
12
13
|
}
|
|
13
14
|
catch (error) {
|
|
14
|
-
|
|
15
|
+
reportCliFailure(error, process.stderr);
|
|
15
16
|
process.exitCode = 1;
|
|
16
17
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function reportCatalog(cwd: string): void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { findContainingPackageDir } from "../context.js";
|
|
2
|
+
import { DEPENDENCY_FIELDS, readPackageJson } from "../helpers/package-json.js";
|
|
3
|
+
import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
|
|
4
|
+
const CATALOG_PROTOCOL = 'catalog:';
|
|
5
|
+
export function reportCatalog(cwd) {
|
|
6
|
+
const monorepoRoot = findMonorepoRoot(cwd);
|
|
7
|
+
const packageDir = findContainingPackageDir(cwd, getWorkspacePackageDirs(monorepoRoot));
|
|
8
|
+
if (packageDir === undefined) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const dependencies = findCataloguedDependencies(packageDir);
|
|
12
|
+
if (dependencies.length === 0) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const subject = dependencies.length === 1 ? '1 dependency comes' : `${dependencies.length} dependencies come`;
|
|
16
|
+
console.warn(`📚 WARN: ${subject} from a catalog, which a package-scoped upgrade does not read:`);
|
|
17
|
+
for (const { name, specifier } of dependencies) {
|
|
18
|
+
console.warn(`- ${name} → ${specifier}`);
|
|
19
|
+
}
|
|
20
|
+
console.warn(`Run \`nmr upgrade\` from ${monorepoRoot} to include them.`);
|
|
21
|
+
}
|
|
22
|
+
function findCataloguedDependencies(packageDir) {
|
|
23
|
+
const pkg = readPackageJson(packageDir);
|
|
24
|
+
const found = new Map();
|
|
25
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
26
|
+
const declared = Object.entries(pkg[field] ?? {});
|
|
27
|
+
for (const [name, specifier] of declared) {
|
|
28
|
+
if (specifier.startsWith(CATALOG_PROTOCOL) && !found.has(name)) {
|
|
29
|
+
found.set(name, specifier);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return [...found].map(([name, specifier]) => ({ name, specifier })).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
34
|
+
}
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import { getPnpmOverrides, readPackageJson } from "../helpers/package-json.js";
|
|
2
|
+
import { UserError } from "../UserError.js";
|
|
3
|
+
import { readWorkspaceOverrides } from "../workspace.js";
|
|
2
4
|
export function reportOverrides(monorepoRoot) {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const declared = listEntries(readWorkspaceOverrides(monorepoRoot));
|
|
6
|
+
if (declared.length > 0) {
|
|
7
|
+
console.warn('🔒 WARN: pnpm overrides are active! Check whether these are still needed:');
|
|
8
|
+
for (const [name, version] of declared) {
|
|
9
|
+
console.warn(`- ${name} → ${version}`);
|
|
10
|
+
}
|
|
7
11
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
rejectLegacyOverrides(monorepoRoot);
|
|
13
|
+
}
|
|
14
|
+
function listEntries(overrides) {
|
|
15
|
+
return Object.entries(overrides ?? {}).toSorted(([a], [b]) => a.localeCompare(b));
|
|
16
|
+
}
|
|
17
|
+
function rejectLegacyOverrides(monorepoRoot) {
|
|
18
|
+
const legacy = listEntries(getPnpmOverrides(readPackageJson(monorepoRoot)));
|
|
19
|
+
if (legacy.length === 0) {
|
|
20
|
+
return;
|
|
11
21
|
}
|
|
22
|
+
throw new UserError([
|
|
23
|
+
'pnpm 11 reads no `pnpm.overrides` from package.json, so these pin nothing while an upgrade run with `--write` goes on rewriting them:',
|
|
24
|
+
...legacy.map(([name, version]) => `- ${name} → ${String(version)}`),
|
|
25
|
+
'Move them to the `overrides` block in pnpm-workspace.yaml, quoting each version, or run `pnpx codemod run pnpm-v10-to-v11`.',
|
|
26
|
+
].join('\n'));
|
|
12
27
|
}
|
|
@@ -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) {
|