@williamthorsen/nmr 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.meta/changelog.json +113 -0
- package/.readyup/kits/default.js +98 -10
- package/.readyup/manifest.json +7 -7
- package/CHANGELOG.md +82 -0
- package/README.md +107 -1135
- package/agents/README.md +1 -1
- package/agents/guidance/rulebooks/nmr.md +4 -4
- package/dist/esm/check-cache.d.ts +2 -1
- package/dist/esm/check-cache.js +4 -4
- package/dist/esm/cli-build.js +3 -0
- package/dist/esm/cli-clean.js +3 -1
- package/dist/esm/cli-ensure-prepublish-hooks.js +3 -1
- package/dist/esm/cli-report-catalog.js +2 -1
- package/dist/esm/cli-report-overrides.js +2 -1
- package/dist/esm/commands/build.d.ts +6 -1
- package/dist/esm/commands/build.js +9 -10
- package/dist/esm/commands/clean.d.ts +3 -2
- package/dist/esm/commands/clean.js +15 -14
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +3 -2
- package/dist/esm/commands/ensure-prepublish-hooks.js +8 -6
- package/dist/esm/commands/report-catalog.d.ts +2 -1
- package/dist/esm/commands/report-catalog.js +5 -4
- package/dist/esm/commands/report-overrides.d.ts +2 -1
- package/dist/esm/commands/report-overrides.js +5 -4
- package/dist/esm/config.js +14 -4
- package/dist/esm/context.d.ts +2 -0
- package/dist/esm/context.js +1 -0
- package/dist/esm/default-scripts.d.ts +2 -2
- package/dist/esm/default-scripts.js +5 -5
- package/dist/esm/glyphs.d.ts +9 -0
- package/dist/esm/glyphs.js +9 -0
- package/dist/esm/help.js +15 -9
- package/dist/esm/helpers/filter-selection.d.ts +42 -0
- package/dist/esm/helpers/filter-selection.js +61 -0
- package/dist/esm/output-style.d.ts +41 -0
- package/dist/esm/output-style.js +17 -0
- package/dist/esm/recording.d.ts +3 -0
- package/dist/esm/recording.js +6 -3
- package/dist/esm/resolveBinStyles.d.ts +12 -0
- package/dist/esm/resolveBinStyles.js +16 -0
- package/dist/esm/resolver.js +2 -2
- package/dist/esm/runCli.js +152 -62
- package/dist/esm/runner.js +6 -5
- package/dist/esm/steps.d.ts +8 -4
- package/dist/esm/steps.js +6 -2
- package/dist/esm/verdict.d.ts +7 -3
- package/dist/esm/verdict.js +16 -10
- package/dist/esm/vitest-source-resolution.d.ts +37 -0
- package/dist/esm/vitest-source-resolution.js +205 -0
- package/dist/esm/vitest.d.ts +3 -2
- package/dist/esm/vitest.js +7 -6
- package/dist/esm/workspace.d.ts +9 -0
- package/dist/esm/workspace.js +17 -0
- package/docs/check-cache.md +176 -0
- package/docs/prettier.md +47 -0
- package/docs/reporting.md +104 -0
- package/docs/scripts.md +168 -0
- package/docs/upgrades.md +110 -0
- package/docs/utilities.md +64 -0
- package/docs/vitest.md +233 -0
- package/docs/workspace.md +11 -0
- package/package.json +11 -9
package/dist/esm/runCli.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { readPackageVersion, reportError } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { describeInvalidOutputStyle, formatGlyphLine, readPackageVersion, reportError, STATUS_GLYPHS, } from '@williamthorsen/nmr-core';
|
|
3
3
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
4
|
import { certifyRetention, computeCacheKey, computeRetentionKey, CURRENT_RUNTIME, encodeTreeSnapshot, findStaleBuildOutput, formatMisplacedNoCacheWarning, isCacheableCommand, NO_CACHE_ENV_VAR, readBuildOutputState, readCheckCacheEntry, recordTranscript, resolveRunId, resolveTreeSnapshot, RUN_ID_ENV_VAR, TREE_SNAPSHOT_ENV_VAR, writeCheckCacheEntry, writeDebugNote, } from "./check-cache.js";
|
|
5
5
|
import { resolveContext } from "./context.js";
|
|
6
|
+
import { NMR_GLYPHS } from "./glyphs.js";
|
|
6
7
|
import { generateHelp } from "./help.js";
|
|
7
8
|
import { resolveConfigPath } from "./helpers/config-path.js";
|
|
8
9
|
import { deriveExcerpt } from "./helpers/deriveExcerpt.js";
|
|
10
|
+
import { readFilterSelection } from "./helpers/filter-selection.js";
|
|
11
|
+
import { findClosestName } from "./helpers/findClosestName.js";
|
|
9
12
|
import { isHookName } from "./helpers/hook-name.js";
|
|
10
13
|
import { resolvePackageJsonPath } from "./helpers/package-json.js";
|
|
11
14
|
import { composeTranscript } from "./helpers/transcript.js";
|
|
15
|
+
import { isTerminalStream, OUTPUT_STYLE_ENV_VAR, OUTPUT_STYLE_FLAG, resolveOutputStyles } from "./output-style.js";
|
|
12
16
|
import { renderRecording, renderRefusal, resolveRecording } from "./recording.js";
|
|
13
17
|
import { assembleReplay } from "./replay-assembly.js";
|
|
14
18
|
import { readReportFormatEnv, REPORT_FORMAT_ENV_VAR, resolveReportFormat } from "./report-format.js";
|
|
@@ -18,8 +22,10 @@ import { composeNmrStep, findNmrCrossing, renderChain } from "./steps.js";
|
|
|
18
22
|
import { UserError } from "./UserError.js";
|
|
19
23
|
import { COMMAND_VERBOSITY_ENV_VAR, readVerbosityEnv, resolveVerbosity, } from "./verbosity.js";
|
|
20
24
|
import { writeVerdict } from "./verdict.js";
|
|
25
|
+
import { readWorkspacePackageNames } from "./workspace.js";
|
|
21
26
|
const VERSION = readPackageVersion(import.meta.url);
|
|
22
27
|
const CROSSING_CONSEQUENCE = "so nmr handles the nested run's output as a tool's.";
|
|
28
|
+
const NAME_CEILING = 10;
|
|
23
29
|
const NAMED_ESCAPES = new Map([
|
|
24
30
|
['\n', String.raw `\n`],
|
|
25
31
|
['\r', String.raw `\r`],
|
|
@@ -34,11 +40,17 @@ export async function runCli(options) {
|
|
|
34
40
|
return { exitCode: 1 };
|
|
35
41
|
}
|
|
36
42
|
const { parsed } = parseResult;
|
|
37
|
-
const inherited =
|
|
43
|
+
const inherited = readPresentation({
|
|
44
|
+
env,
|
|
45
|
+
flagValue: parsed.outputStyle,
|
|
46
|
+
stderrIsTty: isTerminalStream(stderr),
|
|
47
|
+
stdoutIsTty: isTerminalStream(stdout),
|
|
48
|
+
});
|
|
38
49
|
if (!inherited.ok) {
|
|
39
50
|
reportError(inherited.error, stderr);
|
|
40
51
|
return { exitCode: 1 };
|
|
41
52
|
}
|
|
53
|
+
const { styles } = inherited;
|
|
42
54
|
if (parsed.version) {
|
|
43
55
|
stdout.write(`${VERSION}\n`);
|
|
44
56
|
return { exitCode: 0 };
|
|
@@ -68,6 +80,7 @@ export async function runCli(options) {
|
|
|
68
80
|
monorepoRoot: context.monorepoRoot,
|
|
69
81
|
passthrough: parsed.passthrough,
|
|
70
82
|
stderr,
|
|
83
|
+
style: styles.stderr,
|
|
71
84
|
});
|
|
72
85
|
const noCache = parsed.noCache || env[NO_CACHE_ENV_VAR] === '1';
|
|
73
86
|
const runId = resolveRunId(env);
|
|
@@ -78,6 +91,7 @@ export async function runCli(options) {
|
|
|
78
91
|
passthrough: parsed.passthrough,
|
|
79
92
|
runId,
|
|
80
93
|
snapshot,
|
|
94
|
+
styles,
|
|
81
95
|
verbosity,
|
|
82
96
|
});
|
|
83
97
|
const runOptions = {
|
|
@@ -88,7 +102,7 @@ export async function runCli(options) {
|
|
|
88
102
|
};
|
|
89
103
|
const delegation = composeDelegation({ childEnv, command, parsed });
|
|
90
104
|
if (delegation !== undefined) {
|
|
91
|
-
return
|
|
105
|
+
return runDelegation({ context, delegation, parsed, runOptions, stderr });
|
|
92
106
|
}
|
|
93
107
|
const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
|
|
94
108
|
assertNoSelfReference({
|
|
@@ -110,7 +124,7 @@ export async function runCli(options) {
|
|
|
110
124
|
const resolvedCommand = renderChain(resolved.steps);
|
|
111
125
|
const noOpReason = findNoOpReason(resolvedCommand);
|
|
112
126
|
if (noOpReason !== undefined && !parsed.log) {
|
|
113
|
-
reportVerdict({ command, scope, outcome: 'no-op', reason: noOpReason }, stdout, format);
|
|
127
|
+
reportVerdict({ command, scope, outcome: 'no-op', reason: noOpReason }, stdout, format, styles.stdout);
|
|
114
128
|
return { exitCode: 0 };
|
|
115
129
|
}
|
|
116
130
|
const substitutedSteps = applyDevBinToSteps(resolved.steps, context.config.devBin, context.monorepoRoot);
|
|
@@ -133,6 +147,7 @@ export async function runCli(options) {
|
|
|
133
147
|
registry,
|
|
134
148
|
resolved,
|
|
135
149
|
stderr,
|
|
150
|
+
style: styles.stderr,
|
|
136
151
|
useRoot,
|
|
137
152
|
workspaceRoot: parsed.workspaceRoot,
|
|
138
153
|
});
|
|
@@ -159,6 +174,7 @@ export async function runCli(options) {
|
|
|
159
174
|
snapshot,
|
|
160
175
|
stderr,
|
|
161
176
|
stdout,
|
|
177
|
+
styles,
|
|
162
178
|
});
|
|
163
179
|
}
|
|
164
180
|
const { exitCode, outcome } = await runGated({
|
|
@@ -171,7 +187,7 @@ export async function runCli(options) {
|
|
|
171
187
|
key,
|
|
172
188
|
monorepoRoot: context.monorepoRoot,
|
|
173
189
|
noCache,
|
|
174
|
-
overrideNotice: formatOverrideNotice(resolved, registry, command, anchorDir, quiet),
|
|
190
|
+
overrideNotice: formatOverrideNotice(resolved, registry, command, anchorDir, quiet, styles.stdout),
|
|
175
191
|
ownSteps: mainSteps,
|
|
176
192
|
runId,
|
|
177
193
|
runOptions,
|
|
@@ -179,33 +195,49 @@ export async function runCli(options) {
|
|
|
179
195
|
stderr,
|
|
180
196
|
stdout,
|
|
181
197
|
});
|
|
182
|
-
reportVerdict({ command, scope, ...outcome }, stdout, format);
|
|
198
|
+
reportVerdict({ command, scope, ...outcome }, stdout, format, styles.stdout);
|
|
183
199
|
return { exitCode };
|
|
184
200
|
}
|
|
201
|
+
const BOOLEAN_FLAGS = new Map([
|
|
202
|
+
['-?', 'help'],
|
|
203
|
+
['--help', 'help'],
|
|
204
|
+
['--json', 'json'],
|
|
205
|
+
['--log', 'log'],
|
|
206
|
+
['--no-cache', 'noCache'],
|
|
207
|
+
['-q', 'quiet'],
|
|
208
|
+
['--quiet', 'quiet'],
|
|
209
|
+
['-R', 'recursive'],
|
|
210
|
+
['--recursive', 'recursive'],
|
|
211
|
+
['-V', 'version'],
|
|
212
|
+
['--version', 'version'],
|
|
213
|
+
['-w', 'workspaceRoot'],
|
|
214
|
+
['--workspace-root', 'workspaceRoot'],
|
|
215
|
+
]);
|
|
185
216
|
function buildChildEnv(options) {
|
|
186
|
-
const { env, format, noCache, passthrough, runId, snapshot, verbosity } = options;
|
|
217
|
+
const { env, format, noCache, passthrough, runId, snapshot, styles, verbosity } = options;
|
|
187
218
|
const bypassesCache = noCache || passthrough.length > 0;
|
|
188
219
|
return {
|
|
189
220
|
...env,
|
|
190
221
|
...(snapshot !== undefined && { [TREE_SNAPSHOT_ENV_VAR]: encodeTreeSnapshot(snapshot) }),
|
|
191
222
|
...(bypassesCache && { [NO_CACHE_ENV_VAR]: '1' }),
|
|
192
223
|
[COMMAND_VERBOSITY_ENV_VAR]: verbosity,
|
|
224
|
+
[OUTPUT_STYLE_ENV_VAR]: styles.stdout,
|
|
193
225
|
[REPORT_FORMAT_ENV_VAR]: format,
|
|
194
226
|
[RUN_ID_ENV_VAR]: runId,
|
|
195
227
|
};
|
|
196
228
|
}
|
|
197
|
-
function
|
|
198
|
-
return step.kind === 'opaque' || step.
|
|
229
|
+
function acceptsArguments(step) {
|
|
230
|
+
return step.kind === 'opaque' || step.shouldDeclineArguments !== true;
|
|
199
231
|
}
|
|
200
232
|
function bindPassthrough(steps, passthrough, command) {
|
|
201
233
|
if (passthrough.length === 0) {
|
|
202
234
|
return { ok: true, steps };
|
|
203
235
|
}
|
|
204
|
-
if (!steps.some(
|
|
205
|
-
return { ok: false, error:
|
|
236
|
+
if (!steps.some(acceptsArguments)) {
|
|
237
|
+
return { ok: false, error: formatUnroutableArgumentsError(command) };
|
|
206
238
|
}
|
|
207
239
|
const boundSteps = steps.map((step) => {
|
|
208
|
-
if (!
|
|
240
|
+
if (!acceptsArguments(step)) {
|
|
209
241
|
return step;
|
|
210
242
|
}
|
|
211
243
|
return step.kind === 'structural'
|
|
@@ -214,7 +246,7 @@ function bindPassthrough(steps, passthrough, command) {
|
|
|
214
246
|
});
|
|
215
247
|
return { ok: true, steps: boundSteps };
|
|
216
248
|
}
|
|
217
|
-
function
|
|
249
|
+
function formatUnroutableArgumentsError(command) {
|
|
218
250
|
return (`\`${command}\` takes no trailing arguments: every step of its chain declines them. ` +
|
|
219
251
|
'Running it unnarrowed is not what the arguments asked for, so nothing ran.');
|
|
220
252
|
}
|
|
@@ -309,15 +341,16 @@ function escapeControlCharacters(text) {
|
|
|
309
341
|
}
|
|
310
342
|
function formatNmrCrossingWarning(options) {
|
|
311
343
|
const { remedy, subject } = describeCrossingRemedy(options);
|
|
312
|
-
return (
|
|
313
|
-
|
|
344
|
+
return (`${STATUS_GLYPHS[options.style].warning.text} ${subject} reaches nmr through a shell ` +
|
|
345
|
+
`(\`${escapeControlCharacters(options.crossing)}\`), ${CROSSING_CONSEQUENCE} ${remedy}`);
|
|
314
346
|
}
|
|
315
|
-
function formatOverrideNotice(resolved, registry, command, anchorDir, quiet) {
|
|
347
|
+
function formatOverrideNotice(resolved, registry, command, anchorDir, quiet, style) {
|
|
316
348
|
const registryEntry = Object.hasOwn(registry, command) ? registry[command] : undefined;
|
|
317
349
|
if (quiet || resolved.origin.tier !== 'package' || registryEntry === undefined) {
|
|
318
350
|
return undefined;
|
|
319
351
|
}
|
|
320
|
-
|
|
352
|
+
const notice = `${path.basename(anchorDir)}: Using override script: ${renderChain(resolved.steps)}`;
|
|
353
|
+
return `${formatGlyphLine(NMR_GLYPHS, style, 'package', notice)}\n`;
|
|
321
354
|
}
|
|
322
355
|
function formatSelfReferenceRemedy(options) {
|
|
323
356
|
const { command, monorepoRoot, registry, script, workspaceRoot } = options;
|
|
@@ -346,6 +379,58 @@ function findArgError(parsed) {
|
|
|
346
379
|
}
|
|
347
380
|
return undefined;
|
|
348
381
|
}
|
|
382
|
+
function findEmptySelectionRefusal(options) {
|
|
383
|
+
const { context, parsed, selection } = options;
|
|
384
|
+
if (parsed.filter !== undefined) {
|
|
385
|
+
if (selection !== 'empty') {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
return formatEmptyFilterError(parsed.filter, readWorkspacePackageNames(context.workspacePackageDirs));
|
|
389
|
+
}
|
|
390
|
+
return context.workspacePackageDirs.length === 0 ? formatEmptyWorkspaceError() : undefined;
|
|
391
|
+
}
|
|
392
|
+
function formatEmptyFilterError(pattern, names) {
|
|
393
|
+
const rejection = `-F/--filter matched no workspace: \`${pattern}\`.`;
|
|
394
|
+
if (pattern.startsWith('!')) {
|
|
395
|
+
return `${rejection} A pattern beginning with \`!\` excludes what it matches, and this one leaves no package standing.`;
|
|
396
|
+
}
|
|
397
|
+
if (pattern.includes('[')) {
|
|
398
|
+
return `${rejection} A pattern carrying \`[<ref>]\` selects the packages changed since a git ref, and none has changed.`;
|
|
399
|
+
}
|
|
400
|
+
if (pattern.startsWith('.') || pattern.startsWith('/') || pattern.startsWith('{')) {
|
|
401
|
+
return (`${rejection} ` +
|
|
402
|
+
'A pattern written `./dir`, `/dir`, or `{dir}` selects the packages under a directory, ' +
|
|
403
|
+
'and no package sits under this one.');
|
|
404
|
+
}
|
|
405
|
+
return (`${rejection} A pattern matches a package's manifest \`name\`, ` +
|
|
406
|
+
`not its directory name.${suggestWorkspaceNames(pattern, names)}`);
|
|
407
|
+
}
|
|
408
|
+
function formatEmptyWorkspaceError() {
|
|
409
|
+
return ('-R/--recursive matched no workspace: pnpm-workspace.yaml declares no package directory, ' +
|
|
410
|
+
'so there is no scope to fan out to.');
|
|
411
|
+
}
|
|
412
|
+
function suggestWorkspaceNames(pattern, names) {
|
|
413
|
+
if (names.length === 0) {
|
|
414
|
+
return '';
|
|
415
|
+
}
|
|
416
|
+
const containing = names.filter((name) => name.toLowerCase().includes(pattern.toLowerCase()));
|
|
417
|
+
if (containing.length > 0) {
|
|
418
|
+
return ` Did you mean ${renderNames(containing)}?`;
|
|
419
|
+
}
|
|
420
|
+
const closest = findClosestName(pattern, names);
|
|
421
|
+
if (closest !== undefined) {
|
|
422
|
+
return ` Did you mean \`${closest}\`?`;
|
|
423
|
+
}
|
|
424
|
+
return ` The workspace declares ${renderNames(names)}.`;
|
|
425
|
+
}
|
|
426
|
+
function renderNames(names) {
|
|
427
|
+
const shown = names
|
|
428
|
+
.slice(0, NAME_CEILING)
|
|
429
|
+
.map((name) => `\`${name}\``)
|
|
430
|
+
.join(', ');
|
|
431
|
+
const remainder = names.length - NAME_CEILING;
|
|
432
|
+
return remainder > 0 ? `${shown}, and ${remainder} more` : shown;
|
|
433
|
+
}
|
|
349
434
|
function findNoOpReason(resolvedCommand) {
|
|
350
435
|
if (resolvedCommand === '') {
|
|
351
436
|
return 'empty-override';
|
|
@@ -408,12 +493,12 @@ async function lookUpRecordedPass(options) {
|
|
|
408
493
|
};
|
|
409
494
|
}
|
|
410
495
|
function openGate(options) {
|
|
411
|
-
const { command, config, env, monorepoRoot, passthrough, stderr } = options;
|
|
496
|
+
const { command, config, env, monorepoRoot, passthrough, stderr, style } = options;
|
|
412
497
|
if (!isCacheableCommand(config.checkCache, command)) {
|
|
413
498
|
return undefined;
|
|
414
499
|
}
|
|
415
500
|
if (passthrough.includes('--no-cache')) {
|
|
416
|
-
stderr.write(`${formatMisplacedNoCacheWarning(command)}\n`);
|
|
501
|
+
stderr.write(`${formatMisplacedNoCacheWarning(command, style)}\n`);
|
|
417
502
|
}
|
|
418
503
|
if (passthrough.length > 0) {
|
|
419
504
|
writeDebugNote(`gate disabled: ${command} was passed arguments`, env, stderr);
|
|
@@ -446,51 +531,26 @@ function parseArgs(args) {
|
|
|
446
531
|
if (arg === '-F' || arg === '--filter') {
|
|
447
532
|
i++;
|
|
448
533
|
const filterValue = args[i];
|
|
449
|
-
if (filterValue
|
|
534
|
+
if (!filterValue) {
|
|
450
535
|
return { ok: false, error: '-F/--filter requires a pattern argument' };
|
|
451
536
|
}
|
|
452
537
|
parsed.filter = filterValue;
|
|
453
538
|
i++;
|
|
454
539
|
continue;
|
|
455
540
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
if (arg === '-w' || arg === '--workspace-root') {
|
|
462
|
-
parsed.workspaceRoot = true;
|
|
463
|
-
i++;
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
if (arg === '-?' || arg === '--help') {
|
|
467
|
-
parsed.help = true;
|
|
541
|
+
const booleanFlag = BOOLEAN_FLAGS.get(arg);
|
|
542
|
+
if (booleanFlag !== undefined) {
|
|
543
|
+
parsed[booleanFlag] = true;
|
|
468
544
|
i++;
|
|
469
545
|
continue;
|
|
470
546
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
i++;
|
|
479
|
-
continue;
|
|
480
|
-
}
|
|
481
|
-
if (arg === '--json') {
|
|
482
|
-
parsed.json = true;
|
|
483
|
-
i++;
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
if (arg === '--log') {
|
|
487
|
-
parsed.log = true;
|
|
488
|
-
i++;
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
if (arg === '--no-cache') {
|
|
492
|
-
parsed.noCache = true;
|
|
493
|
-
i++;
|
|
547
|
+
const styleArgument = readOutputStyleArgument(args, i);
|
|
548
|
+
if (styleArgument !== undefined) {
|
|
549
|
+
if (!styleArgument.ok) {
|
|
550
|
+
return styleArgument;
|
|
551
|
+
}
|
|
552
|
+
parsed.outputStyle = styleArgument.value;
|
|
553
|
+
i += styleArgument.consumed;
|
|
494
554
|
continue;
|
|
495
555
|
}
|
|
496
556
|
parsed.command = arg;
|
|
@@ -501,7 +561,7 @@ function parseArgs(args) {
|
|
|
501
561
|
return error === undefined ? { ok: true, parsed } : { ok: false, error };
|
|
502
562
|
}
|
|
503
563
|
async function reportRecording(options) {
|
|
504
|
-
const { command, config, scope } = options;
|
|
564
|
+
const { command, config, scope, styles } = options;
|
|
505
565
|
const lookup = await resolveRecording({
|
|
506
566
|
anchorDir: options.anchorDir,
|
|
507
567
|
command,
|
|
@@ -516,13 +576,14 @@ async function reportRecording(options) {
|
|
|
516
576
|
monorepoRoot: options.monorepoRoot,
|
|
517
577
|
});
|
|
518
578
|
if (!lookup.ok) {
|
|
519
|
-
options.stderr.write(`${renderRefusal({ command, refusal: lookup.refusal, scope })}\n`);
|
|
579
|
+
options.stderr.write(`${renderRefusal({ command, refusal: lookup.refusal, scope, style: styles.stderr })}\n`);
|
|
520
580
|
return { exitCode: options.env[RUN_IF_PRESENT_ENV_VAR] === '1' ? 0 : 1 };
|
|
521
581
|
}
|
|
522
|
-
options.stdout.write(renderRecording({ command, recording: lookup.recording, scope }));
|
|
582
|
+
options.stdout.write(renderRecording({ command, recording: lookup.recording, scope, style: styles.stdout }));
|
|
523
583
|
return { exitCode: 0 };
|
|
524
584
|
}
|
|
525
|
-
function
|
|
585
|
+
function readPresentation(options) {
|
|
586
|
+
const { env } = options;
|
|
526
587
|
const verbosity = readVerbosityEnv(env);
|
|
527
588
|
if (!verbosity.ok) {
|
|
528
589
|
return verbosity;
|
|
@@ -531,7 +592,23 @@ function readPresentationEnv(env) {
|
|
|
531
592
|
if (!format.ok) {
|
|
532
593
|
return format;
|
|
533
594
|
}
|
|
534
|
-
|
|
595
|
+
const { invalid, styles } = resolveOutputStyles(options);
|
|
596
|
+
if (invalid !== undefined) {
|
|
597
|
+
return { ok: false, error: describeInvalidOutputStyle(invalid) };
|
|
598
|
+
}
|
|
599
|
+
return { ok: true, format: format.format, styles, verbosity: verbosity.verbosity };
|
|
600
|
+
}
|
|
601
|
+
function readOutputStyleArgument(args, index) {
|
|
602
|
+
const arg = args[index] ?? '';
|
|
603
|
+
const assignment = arg.startsWith(`${OUTPUT_STYLE_FLAG}=`) ? arg.slice(OUTPUT_STYLE_FLAG.length + 1) : undefined;
|
|
604
|
+
if (arg !== OUTPUT_STYLE_FLAG && assignment === undefined) {
|
|
605
|
+
return undefined;
|
|
606
|
+
}
|
|
607
|
+
const value = assignment ?? args[index + 1];
|
|
608
|
+
if (!value) {
|
|
609
|
+
return { ok: false, error: `${OUTPUT_STYLE_FLAG} requires a value argument: auto, plain, or rich` };
|
|
610
|
+
}
|
|
611
|
+
return { ok: true, value, consumed: assignment === undefined ? 2 : 1 };
|
|
535
612
|
}
|
|
536
613
|
function reportNmrCrossing(options) {
|
|
537
614
|
const crossing = findNmrCrossing(options.resolved.steps);
|
|
@@ -543,15 +620,16 @@ function reportNmrCrossing(options) {
|
|
|
543
620
|
monorepoRoot: options.monorepoRoot,
|
|
544
621
|
origin: describeOrigin(options.resolved.origin, options.config, options.useRoot),
|
|
545
622
|
registry: options.registry,
|
|
623
|
+
style: options.style,
|
|
546
624
|
workspaceRoot: options.workspaceRoot,
|
|
547
625
|
});
|
|
548
626
|
options.stderr.write(`${warning}\n`);
|
|
549
627
|
}
|
|
550
|
-
function reportVerdict(verdict, stdout, format) {
|
|
628
|
+
function reportVerdict(verdict, stdout, format, style) {
|
|
551
629
|
if (isHookName(verdict.command)) {
|
|
552
630
|
return;
|
|
553
631
|
}
|
|
554
|
-
writeVerdict(verdict, stdout, format);
|
|
632
|
+
writeVerdict(verdict, stdout, format, style);
|
|
555
633
|
}
|
|
556
634
|
function resolveReportingVerbosity(options) {
|
|
557
635
|
const { format, ...ladder } = options;
|
|
@@ -642,6 +720,18 @@ function resolveCacheKey(options) {
|
|
|
642
720
|
}
|
|
643
721
|
return result.key;
|
|
644
722
|
}
|
|
723
|
+
async function runDelegation(options) {
|
|
724
|
+
const { context, delegation, parsed, runOptions, stderr } = options;
|
|
725
|
+
const selection = parsed.filter === undefined ? undefined : readFilterSelection(parsed.filter, context.monorepoRoot);
|
|
726
|
+
const refusal = findEmptySelectionRefusal({ context, parsed, selection });
|
|
727
|
+
if (refusal !== undefined) {
|
|
728
|
+
reportError(refusal, stderr);
|
|
729
|
+
return { exitCode: 1 };
|
|
730
|
+
}
|
|
731
|
+
const shouldWithholdInput = parsed.filter === undefined || selection === 'multiple';
|
|
732
|
+
const step = shouldWithholdInput ? { ...delegation.step, shouldWithholdInput } : delegation.step;
|
|
733
|
+
return runSteps([step], context.monorepoRoot, { ...runOptions, env: delegation.env });
|
|
734
|
+
}
|
|
645
735
|
async function runGated(options) {
|
|
646
736
|
const { anchorDir, command, commandString, env, key, monorepoRoot, snapshot, stderr, stdout } = options;
|
|
647
737
|
const quiet = options.runOptions.quiet === true;
|
package/dist/esm/runner.js
CHANGED
|
@@ -39,16 +39,16 @@ export async function runSteps(steps, cwd, options) {
|
|
|
39
39
|
return { exitCode: 0, ...composeRetainedOutput(parts, isCaptureWhole) };
|
|
40
40
|
}
|
|
41
41
|
export async function runCommand(command, cwd, options) {
|
|
42
|
-
return runSpawned({ args: [], file: command, useShell: true }, cwd, options);
|
|
42
|
+
return runSpawned({ args: [], file: command, input: 'inherit', useShell: true }, cwd, options);
|
|
43
43
|
}
|
|
44
|
-
async function runSpawned({ args, file, useShell }, cwd, options) {
|
|
44
|
+
async function runSpawned({ args, file, input, useShell }, cwd, options) {
|
|
45
45
|
const quiet = options.quiet === true;
|
|
46
46
|
const stdout = options.stdout ?? process.stdout;
|
|
47
47
|
const stderr = options.stderr ?? process.stderr;
|
|
48
48
|
const env = options.env ?? process.env;
|
|
49
49
|
const child = spawn(file, args, {
|
|
50
50
|
shell: useShell,
|
|
51
|
-
stdio: [
|
|
51
|
+
stdio: [input, options.channels.stdout, options.channels.stderr],
|
|
52
52
|
cwd,
|
|
53
53
|
env,
|
|
54
54
|
});
|
|
@@ -78,13 +78,14 @@ async function runSpawned({ args, file, useShell }, cwd, options) {
|
|
|
78
78
|
function runStep(step, cwd, options) {
|
|
79
79
|
const { quiet, stderr, stdout } = options;
|
|
80
80
|
if (step.kind === 'opaque') {
|
|
81
|
-
return runSpawned({ args: [], file: step.command, useShell: true }, cwd, {
|
|
81
|
+
return runSpawned({ args: [], file: step.command, input: 'inherit', useShell: true }, cwd, {
|
|
82
82
|
...options,
|
|
83
83
|
channels: { stderr: resolveChannel(stderr, quiet), stdout: resolveChannel(stdout, quiet) },
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
const [file, ...args] = step.argv;
|
|
87
|
-
|
|
87
|
+
const input = step.shouldWithholdInput === true ? 'ignore' : 'inherit';
|
|
88
|
+
return runSpawned({ args, file, input, useShell: false }, cwd, {
|
|
88
89
|
...options,
|
|
89
90
|
quiet: false,
|
|
90
91
|
channels: { stderr: resolveInheritedChannel(stderr), stdout: resolveInheritedChannel(stdout) },
|
package/dist/esm/steps.d.ts
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A structural step's argv leads with the file to spawn, so the runner has one to hand `spawn` without a shell.
|
|
6
6
|
*
|
|
7
|
-
* `
|
|
7
|
+
* `shouldDeclineArguments` travels from the composite element that composed the step to the one reader that acts on it,
|
|
8
8
|
* the binding of the invocation's trailing arguments. Every stage between the two -- the devBin substitution,
|
|
9
9
|
* the chain rendering, the replay assembly, the runner -- passes it through and asks nothing of it.
|
|
10
|
+
*
|
|
11
|
+
* `shouldWithholdInput` is read by the runner alone, which gives the step's child the null device as stdin rather
|
|
12
|
+
* than nmr's own. Like `shouldDeclineArguments`, it is set only where it holds and leaves the rendered chain unchanged.
|
|
10
13
|
*/
|
|
11
14
|
export type Step = {
|
|
12
15
|
kind: 'opaque';
|
|
@@ -14,7 +17,8 @@ export type Step = {
|
|
|
14
17
|
} | {
|
|
15
18
|
kind: 'structural';
|
|
16
19
|
argv: readonly [string, ...(readonly string[])];
|
|
17
|
-
|
|
20
|
+
shouldDeclineArguments?: boolean;
|
|
21
|
+
shouldWithholdInput?: boolean;
|
|
18
22
|
};
|
|
19
23
|
/** What a structural step asks of the nmr process it spawns. */
|
|
20
24
|
export interface NmrStepTarget {
|
|
@@ -35,10 +39,10 @@ export type SelfReference = 'chained' | 'sole';
|
|
|
35
39
|
* The element tokenizes on whitespace, so it may carry nmr's own flags but cannot carry a space-bearing token.
|
|
36
40
|
* `-w` is prepended as its own token, so the child selects the root registry on its own.
|
|
37
41
|
*
|
|
38
|
-
* `
|
|
42
|
+
* `shouldDeclineArguments` is set only where it holds, so a step that takes the trailing arguments renders and compares
|
|
39
43
|
* exactly as it did before any element declared anything.
|
|
40
44
|
*/
|
|
41
|
-
export declare function composeNmrStep(element: string, workspaceRoot: boolean,
|
|
45
|
+
export declare function composeNmrStep(element: string, workspaceRoot: boolean, shouldDeclineArguments?: boolean): Step;
|
|
42
46
|
/**
|
|
43
47
|
* Returns the text of the first opaque step that reaches nmr through a shell, or `undefined` when none does.
|
|
44
48
|
*
|
package/dist/esm/steps.js
CHANGED
|
@@ -11,9 +11,13 @@ const QUOTES = new Set(['"', "'"]);
|
|
|
11
11
|
const SEGMENT_SEPARATORS = new Set([';', '|', '&', '\n', '\r']);
|
|
12
12
|
const SHELL_SAFE_TOKEN = /^[\w@%+=:,./-]+$/;
|
|
13
13
|
const TOKEN_SEPARATORS = new Set([' ', '\t', '\n', '\r']);
|
|
14
|
-
export function composeNmrStep(element, workspaceRoot,
|
|
14
|
+
export function composeNmrStep(element, workspaceRoot, shouldDeclineArguments = false) {
|
|
15
15
|
const flags = workspaceRoot ? ['-w'] : [];
|
|
16
|
-
return {
|
|
16
|
+
return {
|
|
17
|
+
kind: 'structural',
|
|
18
|
+
argv: ['nmr', ...flags, ...tokenize(element)],
|
|
19
|
+
...(shouldDeclineArguments && { shouldDeclineArguments }),
|
|
20
|
+
};
|
|
17
21
|
}
|
|
18
22
|
export function findNmrCrossing(steps) {
|
|
19
23
|
for (const step of steps) {
|
package/dist/esm/verdict.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Writable } from 'node:stream';
|
|
2
|
+
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
2
3
|
import type { ReplayLine } from './check-cache.js';
|
|
3
4
|
import type { ReportFormat } from './report-format.js';
|
|
4
5
|
/**
|
|
@@ -54,15 +55,18 @@ export type VerdictOutcome = {
|
|
|
54
55
|
* The line ends without terminal punctuation and reserves its tail for `detail`, so a later change appends to
|
|
55
56
|
* the grammar rather than rewriting it. A detail carrying nothing but line breaks takes its whole clause with
|
|
56
57
|
* it, rather than leaving a separator pointing at nothing.
|
|
58
|
+
*
|
|
59
|
+
* The marker takes no padding column: every plain marker a verdict reaches for is four characters, so the
|
|
60
|
+
* lines align by construction. `formatStatusLine` pads to a column that `BLOCK` widens, and no verdict is one.
|
|
57
61
|
*/
|
|
58
|
-
export declare function renderVerdict(verdict: Verdict): string;
|
|
62
|
+
export declare function renderVerdict(verdict: Verdict, style: OutputStyle): string;
|
|
59
63
|
/**
|
|
60
64
|
* Renders a verdict as the JSON object a machine consumer reads, without the newline that terminates it.
|
|
61
65
|
*
|
|
62
66
|
* Held to the ceiling the prose line is held to, so one write still reaches a pipe whole where concurrent
|
|
63
67
|
* scopes share a descriptor. Cuts land inside the record's text rather than across its structure, which is
|
|
64
68
|
* what leaves the line parseable, and a record is fitted by rungs, each shedding what a reader can better
|
|
65
|
-
* spare than the rung below it. What the ceiling costs is [documented](../
|
|
69
|
+
* spare than the rung below it. What the ceiling costs is [documented](../docs/reporting.md#reporting-for-a-machine)
|
|
66
70
|
* in the same order.
|
|
67
71
|
*/
|
|
68
72
|
export declare function serializeVerdict(verdict: Verdict): string;
|
|
@@ -71,4 +75,4 @@ export declare function serializeVerdict(verdict: Verdict): string;
|
|
|
71
75
|
* share one descriptor. Both renderings spend the one record, so neither can come to report what the other
|
|
72
76
|
* does not.
|
|
73
77
|
*/
|
|
74
|
-
export declare function writeVerdict(verdict: Verdict, stream: Writable, format: ReportFormat): void;
|
|
78
|
+
export declare function writeVerdict(verdict: Verdict, stream: Writable, format: ReportFormat, style: OutputStyle): void;
|
package/dist/esm/verdict.js
CHANGED
|
@@ -1,41 +1,47 @@
|
|
|
1
|
+
import { STATUS_GLYPHS } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { NMR_GLYPHS } from "./glyphs.js";
|
|
1
3
|
import { clampToBytes, TRUNCATION_MARK } from "./helpers/clampToBytes.js";
|
|
2
4
|
import { formatDuration, formatSaving } from "./helpers/duration.js";
|
|
3
5
|
export const VERDICT_LINE_LIMIT = 512;
|
|
4
|
-
export function renderVerdict(verdict) {
|
|
5
|
-
const {
|
|
6
|
+
export function renderVerdict(verdict, style) {
|
|
7
|
+
const { marker, phrase } = describeOutcome(verdict, style);
|
|
6
8
|
const detail = flattenDetail(verdict.detail ?? renderReplay(verdict) ?? '');
|
|
7
9
|
const detailClause = detail === '' ? '' : ` — ${detail}`;
|
|
8
|
-
return clampToBytes(`${
|
|
10
|
+
return clampToBytes(`${marker} ${verdict.scope}: ${verdict.command}: ${phrase}${detailClause}`, LINE_BUDGET_BYTES);
|
|
9
11
|
}
|
|
10
12
|
export function serializeVerdict(verdict) {
|
|
11
13
|
const rendered = JSON.stringify(verdict);
|
|
12
14
|
return isWithinBudget(rendered) ? rendered : renderWithinBudget(verdict);
|
|
13
15
|
}
|
|
14
|
-
export function writeVerdict(verdict, stream, format) {
|
|
15
|
-
const line = format === 'json' ? serializeVerdict(verdict) : renderVerdict(verdict);
|
|
16
|
+
export function writeVerdict(verdict, stream, format, style) {
|
|
17
|
+
const line = format === 'json' ? serializeVerdict(verdict) : renderVerdict(verdict, style);
|
|
16
18
|
stream.write(`${line}\n`);
|
|
17
19
|
}
|
|
18
20
|
const NEWLINE_BYTES = 1;
|
|
19
21
|
const LINE_BUDGET_BYTES = VERDICT_LINE_LIMIT - NEWLINE_BYTES;
|
|
20
22
|
const MIN_CUT_BYTES = Buffer.byteLength(TRUNCATION_MARK);
|
|
21
23
|
const REPLAY_MARKER = 'replayed:';
|
|
22
|
-
function describeOutcome(verdict) {
|
|
24
|
+
function describeOutcome(verdict, style) {
|
|
25
|
+
const statuses = STATUS_GLYPHS[style];
|
|
23
26
|
switch (verdict.outcome) {
|
|
24
27
|
case 'passed':
|
|
25
|
-
return {
|
|
28
|
+
return { marker: statuses.passed.text, phrase: `passed in ${formatDuration(verdict.durationMs)}` };
|
|
26
29
|
case 'failed':
|
|
27
30
|
return {
|
|
28
|
-
|
|
31
|
+
marker: statuses.failed.text,
|
|
29
32
|
phrase: `failed in ${formatDuration(verdict.durationMs)} (exit ${verdict.exitCode})`,
|
|
30
33
|
};
|
|
31
34
|
case 'recalled': {
|
|
32
35
|
const saving = formatSaving(verdict.savedMs);
|
|
33
36
|
const savingClause = saving === undefined ? '' : `, ${saving}`;
|
|
34
|
-
return {
|
|
37
|
+
return {
|
|
38
|
+
marker: statuses.skipped.text,
|
|
39
|
+
phrase: `passed ${formatDuration(verdict.ageMs)} ago on this tree${savingClause}`,
|
|
40
|
+
};
|
|
35
41
|
}
|
|
36
42
|
case 'no-op':
|
|
37
43
|
return {
|
|
38
|
-
|
|
44
|
+
marker: NMR_GLYPHS[style].noop.text,
|
|
39
45
|
phrase: `skipped, the override is ${verdict.reason === 'empty-override' ? 'empty' : 'a no-op'}`,
|
|
40
46
|
};
|
|
41
47
|
default: {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the plugin that resolves a package's `source` export condition, for a package whose files sit outside
|
|
4
|
+
* every `node_modules`.
|
|
5
|
+
*
|
|
6
|
+
* The condition cannot be emitted as a `resolve.conditions` entry instead: Vitest turns the server environment's
|
|
7
|
+
* conditions into `--conditions` flags on the worker process, where Node applies them to every package it
|
|
8
|
+
* resolves natively. Node refuses to strip types from a file under `node_modules`, so a dependency declaring a
|
|
9
|
+
* TypeScript `source` entry fails to load as soon as the condition reaches that far.
|
|
10
|
+
*
|
|
11
|
+
* Resolving here instead keeps the condition inside Vite, which transforms what it resolves. A package under
|
|
12
|
+
* `node_modules` resolves through its remaining conditions, whatever its `exports` map declares.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createSourceResolutionPlugin(): Plugin;
|
|
15
|
+
/** What a resolution needs beyond the specifier and its importer: the resolving environment, and the caches. */
|
|
16
|
+
export interface SourceResolutionContext {
|
|
17
|
+
/** The Vite environment resolving the specifier, which decides the environment-specific condition. */
|
|
18
|
+
environmentName: string;
|
|
19
|
+
/** Parsed manifests, keyed by package directory. Absent from a caller that resolves once. */
|
|
20
|
+
manifests?: Map<string, unknown>;
|
|
21
|
+
/** Package directories, keyed by specifier and importer directory. Absent from a caller that resolves once. */
|
|
22
|
+
packageDirs?: Map<string, string | undefined>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolves a bare specifier to the file that its package's `source` condition names, or nothing where the condition
|
|
26
|
+
* does not reach it.
|
|
27
|
+
*
|
|
28
|
+
* Answers a bare specifier naming a package whose real directory sits outside every `node_modules`. Every other
|
|
29
|
+
* specifier returns nothing -- such as a relative or absolute path, a virtual module, a builtin, or a package under
|
|
30
|
+
* `node_modules` -- so Vite resolves the specifier through its own conditions. Throws where the condition reaches no
|
|
31
|
+
* file, because falling through to the build output is the staleness that resolving from source exists to prevent,
|
|
32
|
+
* and nothing in a run reports it.
|
|
33
|
+
*
|
|
34
|
+
* A query or hash suffix is split off before the `exports` lookup and re-appended to the resolved path, as Vite
|
|
35
|
+
* does, so `pkg/icon.svg?raw` matches the `./icon.svg` entry and still reaches the plugin that reads the suffix.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveSourceTarget(specifier: string, importer: string | undefined, context: SourceResolutionContext): string | undefined;
|