@williamthorsen/nmr 0.29.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.
Files changed (66) hide show
  1. package/.readyup/kits/default.js +33 -22
  2. package/.readyup/manifest.json +2 -2
  3. package/README.md +327 -78
  4. package/agents/guidance/rulebooks/nmr.md +4 -2
  5. package/bin/nmr-report-catalog.js +11 -0
  6. package/dist/esm/UserError.d.ts +3 -0
  7. package/dist/esm/UserError.js +6 -0
  8. package/dist/esm/check-cache.d.ts +36 -1
  9. package/dist/esm/check-cache.js +93 -24
  10. package/dist/esm/cli-build.js +2 -1
  11. package/dist/esm/cli-clean.js +2 -1
  12. package/dist/esm/cli-ensure-prepublish-hooks.js +2 -1
  13. package/dist/esm/cli-report-catalog.d.ts +1 -0
  14. package/dist/esm/cli-report-catalog.js +11 -0
  15. package/dist/esm/cli-report-overrides.js +2 -1
  16. package/dist/esm/cli-taze.js +2 -1
  17. package/dist/esm/cli.js +2 -1
  18. package/dist/esm/commands/clean.js +9 -5
  19. package/dist/esm/commands/report-catalog.d.ts +1 -0
  20. package/dist/esm/commands/report-catalog.js +34 -0
  21. package/dist/esm/commands/report-overrides.js +22 -7
  22. package/dist/esm/commands/taze.js +2 -1
  23. package/dist/esm/config.d.ts +1 -0
  24. package/dist/esm/config.js +81 -16
  25. package/dist/esm/default-scripts.d.ts +5 -1
  26. package/dist/esm/default-scripts.js +19 -16
  27. package/dist/esm/defineConfig.d.ts +1 -1
  28. package/dist/esm/help.js +3 -1
  29. package/dist/esm/helpers/clampToBytes.d.ts +2 -0
  30. package/dist/esm/helpers/clampToBytes.js +20 -0
  31. package/dist/esm/helpers/createBoundedBuffer.js +1 -4
  32. package/dist/esm/helpers/deriveExcerpt.d.ts +1 -0
  33. package/dist/esm/helpers/deriveExcerpt.js +30 -0
  34. package/dist/esm/helpers/duration.js +16 -9
  35. package/dist/esm/helpers/package-json.d.ts +11 -4
  36. package/dist/esm/helpers/package-json.js +51 -22
  37. package/dist/esm/helpers/readStringValues.d.ts +1 -0
  38. package/dist/esm/helpers/readStringValues.js +8 -0
  39. package/dist/esm/helpers/transcript.d.ts +5 -0
  40. package/dist/esm/helpers/transcript.js +59 -0
  41. package/dist/esm/recording.d.ts +65 -0
  42. package/dist/esm/recording.js +96 -0
  43. package/dist/esm/replay-assembly.d.ts +9 -0
  44. package/dist/esm/replay-assembly.js +26 -0
  45. package/dist/esm/report-format.d.ts +18 -0
  46. package/dist/esm/report-format.js +25 -0
  47. package/dist/esm/reportCliFailure.d.ts +2 -0
  48. package/dist/esm/reportCliFailure.js +11 -0
  49. package/dist/esm/resolve-scripts.d.ts +1 -1
  50. package/dist/esm/resolver.d.ts +13 -3
  51. package/dist/esm/resolver.js +34 -19
  52. package/dist/esm/runCli.d.ts +1 -0
  53. package/dist/esm/runCli.js +372 -70
  54. package/dist/esm/runner.d.ts +20 -1
  55. package/dist/esm/runner.js +72 -12
  56. package/dist/esm/scripts.d.ts +1 -1
  57. package/dist/esm/steps.d.ts +18 -0
  58. package/dist/esm/steps.js +144 -0
  59. package/dist/esm/types.d.ts +11 -2
  60. package/dist/esm/verbosity.d.ts +22 -0
  61. package/dist/esm/verbosity.js +37 -0
  62. package/dist/esm/verdict.d.ts +29 -0
  63. package/dist/esm/verdict.js +150 -0
  64. package/dist/esm/workspace.d.ts +2 -0
  65. package/dist/esm/workspace.js +20 -3
  66. package/package.json +12 -5
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ try {
3
+ await import('../dist/esm/cli-report-catalog.js');
4
+ } catch (error) {
5
+ if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
+ process.stderr.write('nmr-report-catalog: build output not found โ€” run `pnpm run bootstrap` first\n');
7
+ } else {
8
+ process.stderr.write(`nmr-report-catalog: failed to load: ${error.message}\n`);
9
+ }
10
+ process.exit(1);
11
+ }
@@ -0,0 +1,3 @@
1
+ export declare class UserError extends Error {
2
+ constructor(message: string);
3
+ }
@@ -0,0 +1,6 @@
1
+ export class UserError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'UserError';
5
+ }
6
+ }
@@ -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 formatSkipLine(command: string, entry: CheckCacheEntry, now: number): string;
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;
@@ -1,14 +1,16 @@
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 { formatDuration, formatSaving } from "./helpers/duration.js";
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";
13
+ import { renderChain } from "./steps.js";
12
14
  import { getWorkspacePackageDirs } from "./workspace.js";
13
15
  export const CURRENT_RUNTIME = {
14
16
  arch: process.arch,
@@ -38,6 +40,7 @@ export const DEFAULT_CACHEABLE_COMMANDS = [
38
40
  'typecheck',
39
41
  ];
40
42
  export const NO_CACHE_ENV_VAR = 'NMR_NO_CACHE';
43
+ export const RUN_ID_ENV_VAR = 'NMR_RUN_ID';
41
44
  export const TREE_SNAPSHOT_ENV_VAR = 'NMR_TREE_SNAPSHOT';
42
45
  const BUILT_IN_COMPILE = 'nmr-compile';
43
46
  const CACHE_TOOL = 'nmr-check';
@@ -47,7 +50,26 @@ const INSTALL_FINGERPRINT_FILES = [
47
50
  ];
48
51
  const KEY_FORMAT = 'nmr-check-cache-v1';
49
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'];
50
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
+ }
51
73
  export function computeCacheKey(options) {
52
74
  const fingerprint = resolveInstallFingerprint(options.monorepoRoot);
53
75
  if (!fingerprint.ok) {
@@ -67,16 +89,16 @@ export function computeCacheKey(options) {
67
89
  runtime.arch,
68
90
  fingerprint.fingerprint,
69
91
  ];
70
- for (const name of KEYED_ENV_VARS) {
71
- const value = options.env[name];
72
- parts.push(name, value === undefined ? 'unset' : 'set', value ?? '');
73
- }
74
- const hash = createHash('sha256');
75
- for (const part of parts) {
76
- hash.update(part);
77
- hash.update('\0');
78
- }
79
- return { ok: true, key: hash.digest('hex') };
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)]);
80
102
  }
81
103
  export function encodeTreeSnapshot(snapshot) {
82
104
  return `${snapshot.hash} ${snapshot.headSha}`;
@@ -89,11 +111,8 @@ export function formatMisplacedNoCacheWarning(command) {
89
111
  return (`โš ๏ธ --no-cache after the command name is passed to \`${command}\`, not read by nmr. ` +
90
112
  `Did you mean \`nmr --no-cache ${command}\`?`);
91
113
  }
92
- export function formatSkipLine(command, entry, now) {
93
- const age = formatDuration(Math.max(0, now - Date.parse(entry.recordedAt)));
94
- const saving = formatSaving(entry.durationMs);
95
- const savingClause = saving === undefined ? '' : ` (${saving})`;
96
- 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);
97
116
  }
98
117
  export async function readBuildOutputState(monorepoRoot, config) {
99
118
  const state = { missing: [], digests: {} };
@@ -125,7 +144,23 @@ export async function readBuildOutputState(monorepoRoot, config) {
125
144
  return state;
126
145
  }
127
146
  export async function readCheckCacheEntry(options) {
128
- return readJsonCacheEntry(resolveEntryPath(options), isCheckCacheEntry);
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);
129
164
  }
130
165
  export async function removeCheckCache(scopeDir) {
131
166
  await removeCacheDir({ tool: CACHE_TOOL, scopeDir });
@@ -138,6 +173,10 @@ export function resolveCacheableCommands(checkCache) {
138
173
  }
139
174
  return commands;
140
175
  }
176
+ export function resolveRunId(env) {
177
+ const inherited = env[RUN_ID_ENV_VAR];
178
+ return inherited === undefined || inherited === '' ? randomUUID() : inherited;
179
+ }
141
180
  export function resolveTreeSnapshot(options) {
142
181
  const inherited = decodeTreeSnapshot(options.env[TREE_SNAPSHOT_ENV_VAR]);
143
182
  if (inherited !== undefined && readHeadSha(options.monorepoRoot) === inherited.headSha) {
@@ -167,6 +206,12 @@ export function writeDebugNote(message, env, stderr) {
167
206
  stderr.write(`nmr check-cache: ${message}\n`);
168
207
  }
169
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
+ }
170
215
  function decodeTreeSnapshot(encoded) {
171
216
  if (encoded === undefined) {
172
217
  return undefined;
@@ -177,7 +222,18 @@ function decodeTreeSnapshot(encoded) {
177
222
  }
178
223
  return { hash, headSha };
179
224
  }
180
- function isCheckCacheEntry(value) {
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) {
181
237
  if (!isObject(value)) {
182
238
  return false;
183
239
  }
@@ -192,19 +248,32 @@ function isCheckCacheEntry(value) {
192
248
  }
193
249
  function isProbeSubject(packageDir, registry) {
194
250
  const build = resolveScript('build', registry, packageDir, false);
195
- if (build === undefined || build.source === 'package') {
251
+ if (build === undefined || build.origin.tier === 'package') {
196
252
  return false;
197
253
  }
198
254
  const compile = resolveScript('compile', registry, packageDir, false);
199
- return compile !== undefined && compile.command === BUILT_IN_COMPILE;
255
+ return compile !== undefined && renderChain(compile.steps) === BUILT_IN_COMPILE;
256
+ }
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)));
200
269
  }
201
- function resolveEntryPath(options) {
270
+ function resolveEntryPath(options, extension = '.json') {
202
271
  const anchorDir = path.resolve(options.anchorDir);
203
272
  const ref = {
204
273
  tool: CACHE_TOOL,
205
274
  scopeDir: options.monorepoRoot,
206
275
  slug: `${path.basename(anchorDir)}-${options.command.replaceAll(UNSAFE_SLUG_CHARACTERS, '-')}`,
207
- extension: '.json',
276
+ extension,
208
277
  discriminators: [anchorDir, options.command],
209
278
  };
210
279
  return resolveCacheEntryPath(ref);
@@ -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(error instanceof Error ? error.message : String(error));
13
+ reportError(describeError(error));
13
14
  process.exitCode = 1;
14
15
  }
@@ -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(error instanceof Error ? error.message : String(error));
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(error instanceof Error ? error.message : String(error));
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(error instanceof Error ? error.message : String(error));
10
+ reportError(describeError(error));
10
11
  process.exitCode = 1;
11
12
  }
@@ -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(error instanceof Error ? error.message : String(error));
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
- process.stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
15
+ reportCliFailure(error, process.stderr);
15
16
  process.exitCode = 1;
16
17
  }
@@ -4,7 +4,8 @@ import { removeCheckCache } from "../check-cache.js";
4
4
  import { loadRootConfig } from "../config.js";
5
5
  import { findContainingPackageDir } from "../context.js";
6
6
  import { applyDevBin, buildWorkspaceRegistry, resolveScript } from "../resolver.js";
7
- import { runCommand } from "../runner.js";
7
+ import { resolveChannel, runCommand } from "../runner.js";
8
+ import { renderChain } from "../steps.js";
8
9
  import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
9
10
  import { resolveBuildCachePath } from "./build-output.js";
10
11
  const CLEAN_ICON = '๐Ÿงน';
@@ -44,15 +45,18 @@ async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
44
45
  const registry = buildWorkspaceRegistry(config);
45
46
  for (const packageDir of workspacePackageDirs) {
46
47
  const resolved = resolveScript(CLEAN_COMMAND, registry, packageDir, false);
47
- if (resolved === undefined || resolved.command === '') {
48
+ const resolvedCommand = resolved === undefined ? undefined : renderChain(resolved.steps);
49
+ if (resolvedCommand === undefined || resolvedCommand === '') {
48
50
  continue;
49
51
  }
50
- if (resolved.command === BUILT_IN_CLEAN) {
52
+ if (resolvedCommand === BUILT_IN_CLEAN) {
51
53
  await cleanPackage(packageDir);
52
54
  continue;
53
55
  }
54
- const command = applyDevBin(resolved.command, config.devBin, monorepoRoot);
55
- const { exitCode } = await runCommand(command, packageDir);
56
+ const command = applyDevBin(resolvedCommand, config.devBin, monorepoRoot);
57
+ const { exitCode } = await runCommand(command, packageDir, {
58
+ channels: { stderr: resolveChannel(process.stderr, false), stdout: resolveChannel(process.stdout, false) },
59
+ });
56
60
  if (exitCode !== 0) {
57
61
  throw new Error(`nmr-clean: \`${command}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
58
62
  }
@@ -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 pkg = readPackageJson(monorepoRoot);
4
- const overrides = getPnpmOverrides(pkg);
5
- if (!overrides || Object.keys(overrides).length === 0) {
6
- return;
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
- console.warn('๐Ÿ”’ WARN: pnpm overrides are active! Check whether these are still needed:');
9
- for (const [name, version] of Object.entries(overrides)) {
10
- console.warn(`- ${name} โ†’ ${version}`);
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 = error instanceof Error ? error.message : String(error);
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
  }
@@ -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>;