@williamthorsen/nmr 0.39.0 → 0.41.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 (89) hide show
  1. package/.meta/changelog.json +145 -4
  2. package/.readyup/kits/default.js +47 -32
  3. package/.readyup/manifest.json +12 -7
  4. package/CHANGELOG.md +44 -0
  5. package/README.md +36 -75
  6. package/agents/README.md +8 -8
  7. package/agents/guidance/rulebooks/nmr.md +8 -8
  8. package/bin/ensure-prepublish-hooks.js +1 -1
  9. package/bin/nmr-clean.js +1 -1
  10. package/bin/nmr-compile.js +1 -1
  11. package/bin/nmr-fmt.js +1 -1
  12. package/bin/nmr-report-catalog.js +1 -1
  13. package/bin/nmr-report-overrides.js +1 -1
  14. package/bin/nmr-taze.js +1 -1
  15. package/bin/nmr.js +1 -1
  16. package/dist/esm/UserError.d.ts +1 -2
  17. package/dist/esm/check-cache.d.ts +59 -61
  18. package/dist/esm/commands/build-output.d.ts +35 -36
  19. package/dist/esm/commands/build.d.ts +13 -14
  20. package/dist/esm/commands/build.js +3 -3
  21. package/dist/esm/commands/clean.d.ts +6 -6
  22. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +5 -5
  23. package/dist/esm/commands/fmt.d.ts +8 -8
  24. package/dist/esm/commands/fmt.js +1 -1
  25. package/dist/esm/commands/report-catalog.d.ts +5 -7
  26. package/dist/esm/commands/report-catalog.js +1 -1
  27. package/dist/esm/commands/report-overrides.d.ts +0 -2
  28. package/dist/esm/commands/report-overrides.js +1 -1
  29. package/dist/esm/commands/taze.d.ts +9 -8
  30. package/dist/esm/config.d.ts +5 -5
  31. package/dist/esm/config.js +3 -3
  32. package/dist/esm/context.d.ts +1 -2
  33. package/dist/esm/default-scripts.d.ts +5 -6
  34. package/dist/esm/defineConfig.d.ts +1 -1
  35. package/dist/esm/git-ignored-paths.d.ts +9 -0
  36. package/dist/esm/git-ignored-paths.js +10 -0
  37. package/dist/esm/glyphs.d.ts +3 -3
  38. package/dist/esm/help.d.ts +2 -2
  39. package/dist/esm/helpers/clampToBytes.d.ts +4 -5
  40. package/dist/esm/helpers/config-path.d.ts +0 -2
  41. package/dist/esm/helpers/createBoundedBuffer.d.ts +2 -2
  42. package/dist/esm/helpers/deriveExcerpt.d.ts +4 -5
  43. package/dist/esm/helpers/duration.d.ts +8 -9
  44. package/dist/esm/helpers/filter-selection.d.ts +19 -19
  45. package/dist/esm/helpers/findClosestName.d.ts +1 -1
  46. package/dist/esm/helpers/hook-name.d.ts +2 -5
  47. package/dist/esm/helpers/package-json.d.ts +9 -8
  48. package/dist/esm/helpers/readStringValues.d.ts +2 -3
  49. package/dist/esm/helpers/reportClosing.d.ts +8 -9
  50. package/dist/esm/helpers/transcript.d.ts +13 -13
  51. package/dist/esm/helpers/type-guards.d.ts +1 -0
  52. package/dist/esm/output-style.d.ts +12 -13
  53. package/dist/esm/prettier.d.ts +2 -2
  54. package/dist/esm/recording.d.ts +20 -15
  55. package/dist/esm/recording.js +7 -7
  56. package/dist/esm/replay-assembly.d.ts +10 -10
  57. package/dist/esm/report-format.d.ts +14 -19
  58. package/dist/esm/reportCliFailure.d.ts +2 -3
  59. package/dist/esm/resolve-scripts.d.ts +4 -5
  60. package/dist/esm/resolveBinStyles.d.ts +4 -5
  61. package/dist/esm/resolver.d.ts +14 -18
  62. package/dist/esm/runCli.d.ts +6 -6
  63. package/dist/esm/runCli.js +104 -27
  64. package/dist/esm/runner.d.ts +19 -19
  65. package/dist/esm/steps.d.ts +45 -42
  66. package/dist/esm/steps.js +6 -6
  67. package/dist/esm/taze.d.ts +1 -1
  68. package/dist/esm/tests.d.ts +18 -13
  69. package/dist/esm/tests.js +2 -2
  70. package/dist/esm/tiers.d.ts +27 -23
  71. package/dist/esm/tiers.js +6 -2
  72. package/dist/esm/types.d.ts +5 -5
  73. package/dist/esm/verbosity.d.ts +22 -20
  74. package/dist/esm/verdict.d.ts +33 -33
  75. package/dist/esm/verdict.js +1 -1
  76. package/dist/esm/vitest-source-resolution.d.ts +11 -11
  77. package/dist/esm/vitest-source-resolution.js +2 -2
  78. package/dist/esm/vitest.d.ts +25 -24
  79. package/dist/esm/vitest.js +16 -8
  80. package/dist/esm/workspace.d.ts +4 -4
  81. package/docs/check-cache.md +57 -57
  82. package/docs/prettier.md +10 -10
  83. package/docs/reporting.md +46 -46
  84. package/docs/scripts.md +29 -29
  85. package/docs/upgrades.md +18 -18
  86. package/docs/utilities.md +13 -13
  87. package/docs/vitest.md +64 -62
  88. package/docs/workspace.md +19 -19
  89. package/package.json +6 -5
@@ -16,16 +16,16 @@ import { isTerminalStream, OUTPUT_STYLE_ENV_VAR, OUTPUT_STYLE_FLAG, resolveOutpu
16
16
  import { renderRecording, renderRefusal, resolveRecording } from "./recording.js";
17
17
  import { assembleReplay } from "./replay-assembly.js";
18
18
  import { readReportFormatEnv, REPORT_FORMAT_ENV_VAR, resolveReportFormat } from "./report-format.js";
19
- import { applyDevBinToSteps, buildRootRegistry, buildWorkspaceRegistry, expandScript, findChainedSelfReference, resolveScript, } from "./resolver.js";
19
+ import { applyDevBinToSteps, buildRootRegistry, buildWorkspaceRegistry, expandScript, findChainedSelfReference, readPackageJsonScripts, resolveScript, } from "./resolver.js";
20
20
  import { resolveChannel, runSteps } from "./runner.js";
21
- import { composeNmrStep, dropRecursiveSteps, findNmrCrossing, renderChain } from "./steps.js";
21
+ import { composeNmrStep, dropRecursiveSteps, findNmrCrossing, quoteToken, renderChain } from "./steps.js";
22
22
  import { UserError } from "./UserError.js";
23
23
  import { COMMAND_VERBOSITY_ENV_VAR, readVerbosityEnv, resolveVerbosity, } from "./verbosity.js";
24
24
  import { writeVerdict } from "./verdict.js";
25
25
  import { readWorkspacePackageNames, resolveWorkspace } from "./workspace.js";
26
26
  const VERSION = readPackageVersion(import.meta.url);
27
27
  const CROSSING_CONSEQUENCE = "so nmr handles the nested run's output as a tool's.";
28
- const RECURSIVE_REJECTION = '-R/--recursive matched no workspace:';
28
+ const RECURSIVE_REJECTION = '-R/--recursive did not match any workspace:';
29
29
  const LIST_CEILING = 10;
30
30
  const NAMED_ESCAPES = new Map([
31
31
  ['\n', String.raw `\n`],
@@ -34,7 +34,7 @@ const NAMED_ESCAPES = new Map([
34
34
  ]);
35
35
  export const RUN_IF_PRESENT_ENV_VAR = 'NMR_RUN_IF_PRESENT';
36
36
  export async function runCli(options) {
37
- const { args, cwd, env, stdout, stderr } = options;
37
+ const { args, env, stdout, stderr } = options;
38
38
  const parseResult = parseArgs(args);
39
39
  if (!parseResult.ok) {
40
40
  reportError(parseResult.error, stderr);
@@ -52,6 +52,18 @@ export async function runCli(options) {
52
52
  return { exitCode: 1 };
53
53
  }
54
54
  const { styles } = presentationRead;
55
+ const { exitCode, isUnknownCommand = false } = await runInvocation({ ...options, parsedArgs, presentationRead });
56
+ if (exitCode !== 0 && !isUnknownCommand && isTopLevelInvocation(env)) {
57
+ const hint = formatMisplacedFlagHint(args, parsedArgs.passthrough, styles.stderr);
58
+ if (hint !== undefined) {
59
+ stderr.write(`${hint}\n`);
60
+ }
61
+ }
62
+ return { exitCode };
63
+ }
64
+ async function runInvocation(options) {
65
+ const { cwd, env, parsedArgs, presentationRead, stderr, stdout } = options;
66
+ const { styles } = presentationRead;
55
67
  if (parsedArgs.shouldShowVersion) {
56
68
  stdout.write(`${VERSION}\n`);
57
69
  return { exitCode: 0 };
@@ -103,7 +115,8 @@ export async function runCli(options) {
103
115
  };
104
116
  const delegation = composeDelegation({ childEnv, command, parsedArgs });
105
117
  if (delegation !== undefined) {
106
- return runDelegation({ context, delegation, parsedArgs, runOptions, stderr });
118
+ const result = await runDelegation({ context, delegation, parsedArgs, runOptions, stderr });
119
+ return { ...result, isUnknownCommand: result.exitCode !== 0 && !isDefinedInAnyScope(command, context) };
107
120
  }
108
121
  const registry = shouldUseRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
109
122
  assertNoSelfReference({
@@ -120,7 +133,7 @@ export async function runCli(options) {
120
133
  return { exitCode: 0 };
121
134
  }
122
135
  reportError(`Unknown command: ${command}`, stderr);
123
- return { exitCode: 1 };
136
+ return { exitCode: 1, isUnknownCommand: true };
124
137
  }
125
138
  const { isEmptiedByWorkspace, steps: runnableSteps } = readRunnableSteps(resolvedScript, context.workspacePackageDirs);
126
139
  const resolvedCommand = renderChain(runnableSteps);
@@ -200,6 +213,7 @@ export async function runCli(options) {
200
213
  reportVerdict({ command, scope, ...outcome }, stdout, format, styles.stdout);
201
214
  return { exitCode };
202
215
  }
216
+ const FILTER_FLAGS = new Set(['-F', '--filter']);
203
217
  const BOOLEAN_FLAGS = new Map([
204
218
  ['-?', 'shouldShowHelp'],
205
219
  ['--help', 'shouldShowHelp'],
@@ -249,7 +263,7 @@ function bindPassthrough(steps, passthrough, command) {
249
263
  return { ok: true, steps: boundSteps };
250
264
  }
251
265
  function formatUnroutableArgumentsError(command) {
252
- return (`\`${command}\` takes no trailing arguments: every step of its chain declines them. ` +
266
+ return (`\`${command}\` does not take trailing arguments: every step of its chain declines them. ` +
253
267
  'Running it unnarrowed is not what the arguments asked for, so nothing ran.');
254
268
  }
255
269
  function composeDelegation(options) {
@@ -320,7 +334,7 @@ function describeCrossingRemedy(options) {
320
334
  }
321
335
  function describeStepDestination(key) {
322
336
  if (isHookName(key)) {
323
- return 'a script of their own that the step list names, because a hook has no `:pre` or `:post` of its own';
337
+ return 'a script of their own that the step list names, because a hook does not have a `:pre` or `:post` of its own';
324
338
  }
325
339
  return `a \`${key}:pre\` or \`${key}:post\` script`;
326
340
  }
@@ -357,7 +371,7 @@ function formatOverrideNotice(resolvedScript, registry, command, anchorDir, quie
357
371
  function formatSelfReferenceRemedy(options) {
358
372
  const { command, isWorkspaceRoot, monorepoRoot, registry, script } = options;
359
373
  if (isHookName(command)) {
360
- return `Delete the re-invocation: \`${command}\` runs the steps standing beside it.`;
374
+ return `Delete the re-invocation: \`${command}\` runs the steps beside it.`;
361
375
  }
362
376
  const configSite = path.relative(monorepoRoot, resolveConfigPath(monorepoRoot));
363
377
  return formatPackageRemedy({ configSite, crossingStep: script, isWorkspaceRoot, key: command, registry });
@@ -366,14 +380,14 @@ function formatPackageRemedy(options) {
366
380
  const { configSite, crossingStep, isWorkspaceRoot, key, registry } = options;
367
381
  const registryEntry = Object.hasOwn(registry, key) ? registry[key] : undefined;
368
382
  if (registryEntry === undefined) {
369
- return (`A \`package.json\` script holds no step list: define \`${key}\` in \`${configSite}\` and move the ` +
383
+ return (`A \`package.json\` script cannot contain a step list: define \`${key}\` in \`${configSite}\` and move the ` +
370
384
  `package-specific steps to ${describeStepDestination(key)}.`);
371
385
  }
372
386
  const registryChain = renderChain(expandScript(registryEntry, isWorkspaceRoot));
373
387
  if (registryChain === crossingStep) {
374
388
  return `Delete the entry: nmr's own \`${key}\` already runs \`${escapeControlCharacters(registryChain)}\`.`;
375
389
  }
376
- return `Delete the entry and move the steps it adds to ${describeStepDestination(key)}.`;
390
+ return `Delete the entry and move the steps that it adds to ${describeStepDestination(key)}.`;
377
391
  }
378
392
  function findArgError(parsedArgs) {
379
393
  if (parsedArgs.shouldPrintLog &&
@@ -401,15 +415,15 @@ function findEmptySelectionRefusal(options) {
401
415
  function formatEmptyFilterError(pattern, names) {
402
416
  const rejection = formatFilterRejection(pattern);
403
417
  if (pattern.startsWith('!')) {
404
- return `${rejection} A pattern beginning with \`!\` excludes what it matches, and this one leaves no package standing.`;
418
+ return `${rejection} A pattern beginning with \`!\` excludes what it matches, and this one excludes every package.`;
405
419
  }
406
420
  if (pattern.includes('[')) {
407
- return `${rejection} A pattern carrying \`[<ref>]\` selects the packages changed since a git ref, and none has changed.`;
421
+ return `${rejection} A pattern containing \`[<ref>]\` selects the packages changed since a git ref, and none has changed.`;
408
422
  }
409
423
  if (pattern.startsWith('.') || pattern.startsWith('/') || pattern.startsWith('{')) {
410
424
  return (`${rejection} ` +
411
425
  'A pattern written `./dir`, `/dir`, or `{dir}` selects the packages under a directory, ' +
412
- 'and no package sits under this one.');
426
+ 'and this directory does not contain any package.');
413
427
  }
414
428
  return (`${rejection} A pattern matches a package's manifest \`name\`, ` +
415
429
  `not its directory name.${suggestWorkspaceNames(pattern, names)}`);
@@ -426,23 +440,23 @@ function describeEmptyWorkspace(monorepoRoot) {
426
440
  return (`pnpm-workspace.yaml ${declaredClause}, whose \`!\` entries exclude every directory matched by the positive ` +
427
441
  'patterns. Drop or narrow the exclusion.');
428
442
  case 'no-package':
429
- return (`pnpm-workspace.yaml ${declaredClause}, and the matcher found no directory holding a \`package.json\`. ` +
430
- 'nmr counts a directory as a package only where it holds `package.json`; unlike pnpm, it recognizes ' +
443
+ return (`pnpm-workspace.yaml ${declaredClause}, and the matcher did not find any directory containing a \`package.json\`. ` +
444
+ 'nmr counts a directory as a package only when it contains `package.json`; unlike pnpm, it recognizes ' +
431
445
  'neither `package.yaml` nor `package.json5`. Add a `package.json` to the directory that should be a ' +
432
- 'package, or declare a pattern reaching a directory that holds one.');
446
+ 'package, or declare a pattern reaching a directory that contains one.');
433
447
  case 'no-packages-list':
434
- return (`pnpm-workspace.yaml at ${monorepoRoot} declares no \`packages\` list, so the workspace holds the root ` +
448
+ return (`pnpm-workspace.yaml at ${monorepoRoot} does not declare a \`packages\` list, so the workspace contains the root ` +
435
449
  'package alone. Declare a positive pattern such as `packages/*` to reach the packages beneath it.');
436
450
  case 'no-pattern':
437
- return (`pnpm-workspace.yaml ${declaredClause}, so no pattern reaches the matcher. Declare a positive pattern such ` +
438
- 'as `packages/*`, and quote any `!` entry, which YAML reads as a tag rather than a string where it ' +
439
- 'stands bare.');
451
+ return (`pnpm-workspace.yaml ${declaredClause}, so the matcher does not receive any pattern. Declare a positive ` +
452
+ 'pattern such as `packages/*`, and quote any `!` entry, which YAML reads as a tag rather than a string ' +
453
+ 'when it is unquoted.');
440
454
  case 'unreadable-packages':
441
455
  return (`pnpm-workspace.yaml at ${monorepoRoot} declares a \`packages\` value that is not a list of strings, so ` +
442
456
  'nothing it declares reaches the matcher. Make it a list whose every entry is a quoted pattern; a bare ' +
443
457
  'value and an entry YAML read as a number or a map are the usual ones.');
444
458
  case 'unreadable-manifest':
445
- return (`pnpm-workspace.yaml at ${monorepoRoot} holds no valid YAML, so nothing it declares reaches the matcher. ` +
459
+ return (`pnpm-workspace.yaml at ${monorepoRoot} is not valid YAML, so the matcher does not receive anything that it declares. ` +
446
460
  'Repair the syntax error and run the command again. An unterminated quoted string and a mis-indented ' +
447
461
  'entry are the usual ones.');
448
462
  default: {
@@ -463,7 +477,7 @@ function describeDeclaredPatterns(patterns) {
463
477
  : `declares ${renderQuotedList(quotablePatterns)}, beside ${emptiedClause}`;
464
478
  }
465
479
  function formatFilterRejection(pattern) {
466
- return `-F/--filter matched no workspace: \`${pattern}\`.`;
480
+ return `-F/--filter did not match any workspace: \`${pattern}\`.`;
467
481
  }
468
482
  function suggestWorkspaceNames(pattern, names) {
469
483
  if (names.length === 0) {
@@ -511,7 +525,7 @@ function assertNoSelfReference(options) {
511
525
  const remedy = formatSelfReferenceRemedy({ command, isWorkspaceRoot, monorepoRoot, registry, script });
512
526
  const site = path.relative(monorepoRoot, resolvePackageJsonPath(anchorDir));
513
527
  throw new UserError(`${site}: \`scripts.${command}\` re-invokes \`nmr ${command}\` (\`${escapeControlCharacters(script)}\`), ` +
514
- `so nmr cannot run the steps it chains. ${remedy}`);
528
+ `so nmr cannot run the steps that it chains. ${remedy}`);
515
529
  }
516
530
  function hasRunnableHook(hookName, registry, anchorDir, isWorkspaceRoot) {
517
531
  if (findChainedSelfReference(anchorDir, hookName) !== undefined) {
@@ -536,7 +550,7 @@ async function lookUpRecordedPass(options) {
536
550
  }
537
551
  const [missingPackage] = buildOutput.missingPackages;
538
552
  if (missingPackage !== undefined) {
539
- writeDebugNote(`running ${command}: ${missingPackage} has no build output`, env, stderr);
553
+ writeDebugNote(`running ${command}: ${missingPackage} does not have build output`, env, stderr);
540
554
  return undefined;
541
555
  }
542
556
  const stalePackage = findStaleBuildOutput(entry.buildDigests, buildOutput.digests);
@@ -587,7 +601,7 @@ function parseArgs(args) {
587
601
  const arg = args[index];
588
602
  if (arg === undefined)
589
603
  break;
590
- if (arg === '-F' || arg === '--filter') {
604
+ if (FILTER_FLAGS.has(arg)) {
591
605
  index++;
592
606
  const filterValue = args[index];
593
607
  if (!filterValue) {
@@ -715,7 +729,7 @@ async function recordPass(options) {
715
729
  const output = await readBuildOutputState(monorepoRoot, options.config);
716
730
  const [missingPackage] = output.missingPackages;
717
731
  if (missingPackage !== undefined) {
718
- writeDebugNote(`not recording ${command}: ${missingPackage} has no build output`, env, stderr);
732
+ writeDebugNote(`not recording ${command}: ${missingPackage} does not have build output`, env, stderr);
719
733
  return;
720
734
  }
721
735
  const changedPackage = findStaleBuildOutput(options.buildOutputBefore.digests, output.digests);
@@ -862,6 +876,69 @@ async function runGated(options) {
862
876
  outcome: exitCode === 0 ? { outcome: 'passed', durationMs } : { outcome: 'failed', durationMs, exitCode },
863
877
  };
864
878
  }
879
+ function formatMisplacedFlagHint(args, passthrough, style) {
880
+ const { flagNames, movedTokens, remainingTokens } = splitMisplacedFlags(passthrough);
881
+ if (flagNames.length === 0) {
882
+ return undefined;
883
+ }
884
+ const commandIndex = args.length - passthrough.length - 1;
885
+ const correctedTokens = [
886
+ 'nmr',
887
+ ...args.slice(0, commandIndex),
888
+ ...movedTokens,
889
+ ...args.slice(commandIndex, commandIndex + 1),
890
+ ...remainingTokens,
891
+ ];
892
+ const subject = flagNames.length === 1 ? 'is an nmr flag' : 'are nmr flags';
893
+ return (`${STATUS_GLYPHS[style].warning.text} ${renderQuotedList(flagNames)} ${subject}; ` +
894
+ `nmr reads its flags only before the command: \`${correctedTokens.map(quoteToken).join(' ')}\``);
895
+ }
896
+ function isDefinedInAnyScope(command, context) {
897
+ const registries = [buildRootRegistry(context.config), buildWorkspaceRegistry(context.config)];
898
+ if (registries.some((registry) => Object.hasOwn(registry, command))) {
899
+ return true;
900
+ }
901
+ return [context.monorepoRoot, ...context.workspacePackageDirs].some((dir) => Object.hasOwn(readPackageJsonScripts(dir) ?? {}, command));
902
+ }
903
+ function isTopLevelInvocation(env) {
904
+ const runId = env[RUN_ID_ENV_VAR];
905
+ return runId === undefined || runId === '';
906
+ }
907
+ function readMisplacedFlagLength(args, index) {
908
+ const arg = args[index] ?? '';
909
+ if (FILTER_FLAGS.has(arg)) {
910
+ return (args[index + 1] ?? '') === '' ? undefined : 2;
911
+ }
912
+ const booleanFlag = BOOLEAN_FLAGS.get(arg);
913
+ if (booleanFlag !== undefined) {
914
+ return booleanFlag === 'shouldShowHelp' || booleanFlag === 'shouldShowVersion' ? undefined : 1;
915
+ }
916
+ const styleArgument = readOutputStyleArgument(args, index);
917
+ return styleArgument?.ok === true ? styleArgument.consumedCount : undefined;
918
+ }
919
+ function splitMisplacedFlags(passthrough) {
920
+ const flagNames = new Set();
921
+ const movedTokens = [];
922
+ const remainingTokens = [];
923
+ let index = 0;
924
+ while (index < passthrough.length) {
925
+ const arg = passthrough[index] ?? '';
926
+ if (arg === '--') {
927
+ remainingTokens.push(...passthrough.slice(index));
928
+ break;
929
+ }
930
+ const flagLength = readMisplacedFlagLength(passthrough, index);
931
+ if (flagLength === undefined) {
932
+ remainingTokens.push(arg);
933
+ index++;
934
+ continue;
935
+ }
936
+ flagNames.add(arg.startsWith(`${OUTPUT_STYLE_FLAG}=`) ? OUTPUT_STYLE_FLAG : arg);
937
+ movedTokens.push(...passthrough.slice(index, index + flagLength));
938
+ index += flagLength;
939
+ }
940
+ return { flagNames: [...flagNames], movedTokens, remainingTokens };
941
+ }
865
942
  function quoteForShell(arg) {
866
943
  return "'" + arg.replace(/'/g, String.raw `'\''`) + "'";
867
944
  }
@@ -1,16 +1,16 @@
1
1
  import type { Writable } from 'node:stream';
2
2
  import type { Step } from './steps.js';
3
- /** Where one of a child's output streams goes: a descriptor the child writes to, or a pipe nmr reads. */
3
+ /** Where one of a child's output streams goes: a descriptor to which the child writes, or a pipe read by nmr. */
4
4
  export type OutputChannel = number | 'pipe';
5
- /** The channel each of a child's output streams runs on. */
5
+ /** The channel on which each of a child's output streams runs. */
6
6
  export interface OutputChannels {
7
7
  stderr: OutputChannel;
8
8
  stdout: OutputChannel;
9
9
  }
10
10
  export interface RunCommandOptions {
11
- /** The channel each of the child's output streams runs on. */
11
+ /** The channel on which each of the child's output streams runs. */
12
12
  channels: OutputChannels;
13
- /** When true, suppress output on success and write captured output to stderr on failure. */
13
+ /** When true, suppresses output on success and writes captured output to stderr on failure. */
14
14
  quiet?: boolean;
15
15
  /** Stream that subprocess stdout flows to in non-quiet mode. Defaults to `process.stdout`. */
16
16
  stdout?: Writable;
@@ -34,7 +34,7 @@ export interface RunStepsResult {
34
34
  */
35
35
  retainedOutput?: RetainedOutput;
36
36
  }
37
- /** How a command ended, and the exit code that carries that ending to nmr's own caller. */
37
+ /** How a command ended, and the exit code that reports that ending to nmr's own caller. */
38
38
  type CommandCompletion = {
39
39
  outcome: 'exited';
40
40
  exitCode: number;
@@ -50,33 +50,33 @@ type CommandCompletion = {
50
50
  export type RunCommandResult = CommandCompletion & {
51
51
  /**
52
52
  * Output retained while the command ran, elided in the middle when it overran the bound.
53
- * `undefined` where the stream was handed to the child directly, or where capture stopped early
53
+ * `undefined` when the stream was handed to the child directly, or when capture stopped early
54
54
  * and the retained copy would understate what the command produced.
55
55
  */
56
56
  stdout: Buffer | undefined;
57
57
  stderr: Buffer | undefined;
58
58
  };
59
59
  /**
60
- * Returns the channel a stream runs on: the stream's own descriptor when it is a terminal and output is not
61
- * being withheld, so the child's terminal detection matches nmr's, and a pipe everywhere else.
60
+ * Returns the channel on which a stream runs: the stream's own descriptor when it is a terminal and output is
61
+ * not being withheld, so that the child's terminal detection matches nmr's, and a pipe everywhere else.
62
62
  */
63
63
  export declare function resolveChannel(stream: Writable, quiet: boolean): OutputChannel;
64
64
  /**
65
- * Returns the channel a step running an nmr process below this one runs on: nmr's own descriptor whenever it
66
- * has one, so the child writes where nmr writes and no ancestor stands between them relaying bytes. A stream
67
- * carrying no descriptor, which is the one a test injects, falls back to a pipe.
65
+ * Returns the channel for a step that runs an nmr process below this one: nmr's own descriptor whenever it has
66
+ * one, so that the child writes where nmr writes, without an ancestor between them relaying bytes. A stream
67
+ * without a descriptor falls back to a pipe.
68
68
  */
69
69
  export declare function resolveInheritedChannel(stream: Writable): OutputChannel;
70
70
  /**
71
- * Runs a step list in order and resolves once one fails or all have run, carrying the `&&` semantics nmr
72
- * promises: the first non-zero exit ends the sequence and is the code returned.
71
+ * Runs a step list in order and resolves once one fails or all have run, implementing the `&&` semantics that
72
+ * nmr promises: The first non-zero exit ends the sequence and is the code returned.
73
73
  *
74
- * Sequencing here rather than in a spawned shell is what lets each step run on the channels its kind calls
75
- * for, and what makes a signal to nmr end the run: the steps after it are never reached, where a shell would
76
- * have gone on running them unsupervised.
74
+ * Sequencing here rather than in a spawned shell lets each step run on the channels that its kind calls for, and
75
+ * makes a signal to nmr end the run: The steps after it are never reached, whereas a shell would have gone on
76
+ * running them unsupervised.
77
77
  *
78
- * Carries back a bounded copy of what the list's own commands wrote, so a caller can retain it. Only opaque
79
- * steps contribute: a structural step is another nmr process reporting for the subtree beneath it, and its
78
+ * Returns a bounded copy of what the list's own commands wrote, so that a caller can retain it. Only opaque
79
+ * steps contribute: A structural step is another nmr process reporting for the subtree beneath it, and its
80
80
  * output is that subtree's to account for.
81
81
  */
82
82
  export declare function runSteps(steps: readonly Step[], cwd: string | undefined, options: RunStepsOptions): Promise<RunStepsResult>;
@@ -89,7 +89,7 @@ export declare function runSteps(steps: readonly Step[], cwd: string | undefined
89
89
  * discarding the retained copy on success and writing it to `options.stderr` on failure, so it has nothing to
90
90
  * report unless the caller chose a pipe on both streams.
91
91
  *
92
- * Piped stdout and stderr are ordered by arrival rather than by a shared descriptor, so their interleaving
92
+ * Because piped stdout and stderr are ordered by arrival rather than by a shared descriptor, their interleaving
93
93
  * is approximate.
94
94
  */
95
95
  export declare function runCommand(command: string, cwd: string | undefined, options: RunCommandOptions): Promise<RunCommandResult>;
@@ -1,15 +1,15 @@
1
1
  /**
2
- * One element of a resolved command chain, carrying how it was composed rather than how its text reads.
3
- * A `structural` step is nmr's own composition, held as argv; an `opaque` step is a command nmr does not parse.
2
+ * One element of a resolved command chain, recording how it was composed rather than how its text reads.
3
+ * A `structural` step is nmr's own composition, stored as argv; an `opaque` step is a command that nmr does not
4
+ * parse.
4
5
  *
5
6
  * A structural step's argv leads with the file to spawn, so the runner has one to hand `spawn` without a shell.
6
7
  *
7
- * `shouldDeclineArguments` travels from the composite element that composed the step to the one reader that acts on it,
8
- * the binding of the invocation's trailing arguments. Every stage between the two -- the devBin substitution,
9
- * the chain rendering, the replay assembly, the runner -- passes it through and asks nothing of it.
8
+ * `shouldDeclineArguments` is passed from the composite element that composed the step to the one reader that acts
9
+ * on it, the binding of the invocation's trailing arguments. Every stage between the two passes it through unread.
10
10
  *
11
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.
12
+ * than nmr's own. Like `shouldDeclineArguments`, it is set only when it holds and leaves the rendered chain unchanged.
13
13
  */
14
14
  export type Step = {
15
15
  kind: 'opaque';
@@ -20,81 +20,84 @@ export type Step = {
20
20
  shouldDeclineArguments?: boolean;
21
21
  shouldWithholdInput?: boolean;
22
22
  };
23
- /** What a structural step asks of the nmr process it spawns. */
23
+ /** What a structural step asks of the nmr process that it spawns. */
24
24
  export interface NmrStepTarget {
25
25
  command: string;
26
- /** Whether the command is fanned out to other scopes, where a `-R` or `-F` sends it. */
26
+ /** Whether the command is fanned out to other scopes, as it is when a `-R` or `-F` sends it. */
27
27
  isDelegate: boolean;
28
28
  /**
29
- * Whether the command is fanned out to every package, where a `-R` sends it. A separate field rather than a
30
- * narrowing of `isDelegate`, whose reader surveys the scopes a `-F` may also have reached.
29
+ * Whether the command is fanned out to every package, as it is when a `-R` sends it. A separate field rather
30
+ * than a narrowing of `isDelegate`, whose reader surveys the scopes that a `-F` may also have reached.
31
31
  */
32
32
  isRecursive: boolean;
33
- /** Whether the command runs against the root registry, where a `-w` anchors it. */
33
+ /** Whether the command runs against the root registry, as it does when a `-w` anchors it. */
34
34
  isWorkspaceRoot: boolean;
35
35
  }
36
36
  /**
37
- * How a `package.json` entry names the command it is declared under: as its whole value, or alongside other
38
- * steps. `sole` covers an entry carrying trailing arguments, which declare no step of their own.
37
+ * How a `package.json` entry names the command under which it is declared: as its whole value, or alongside
38
+ * other steps. `sole` covers an entry with trailing arguments, which do not declare a step of their own.
39
39
  */
40
40
  export type SelfReference = 'chained' | 'sole';
41
41
  /**
42
42
  * Composes the structural step that re-invokes nmr for one composite element.
43
43
  *
44
- * The element tokenizes on whitespace, so it may carry nmr's own flags but cannot carry a space-bearing token.
45
- * `-w` is prepended as its own token, so the child selects the root registry on its own.
44
+ * The element tokenizes on whitespace, so it may contain nmr's own flags but cannot contain a space-bearing
45
+ * token. `-w` is prepended as its own token: The child selects the root registry on its own.
46
46
  *
47
- * `shouldDeclineArguments` is set only where it holds, so a step that takes the trailing arguments renders and compares
48
- * exactly as it did before any element declared anything.
47
+ * Because `shouldDeclineArguments` is set only when it holds, a step that takes the trailing arguments compares
48
+ * equal to one composed without the option.
49
49
  */
50
50
  export declare function composeNmrStep(element: string, isWorkspaceRoot: boolean, shouldDeclineArguments?: boolean): Step;
51
51
  /**
52
- * Returns the steps that still run in a workspace holding no package: every step but the ones a `-R` fans out.
52
+ * Returns the steps that still run in a workspace without any package: every step but the ones that a `-R` fans
53
+ * out.
53
54
  *
54
- * A `-F` step is left standing. It names one package, and its absence is a real error whoever composed the
55
- * step, where a `-R` asked for every package and a workspace with none has nothing for it to do.
55
+ * A `-F` step is kept. It names one package, and its absence is a real error whoever composed the step, whereas
56
+ * a `-R` asked for every package and a workspace with none has nothing for it to do.
56
57
  */
57
58
  export declare function dropRecursiveSteps(steps: readonly Step[]): Step[];
58
59
  /**
59
- * Returns the text of the first opaque step that reaches nmr through a shell, or `undefined` when none does.
60
+ * Returns the text of the first opaque step that runs nmr through a shell, or `undefined` when none does.
60
61
  *
61
62
  * Recognizes nmr in command position: at the start of the step, after `&&`, `||`, `;`, `|`, or a newline, past
62
63
  * any leading environment assignments, and behind a launcher such as `npx` or `pnpm exec`. A separator inside
63
- * quotes opens no position, so a command merely naming nmr in an argument is not a crossing.
64
+ * quotes does not open a command position, so a command merely naming nmr in an argument is not a crossing.
64
65
  *
65
- * Partial by construction, and partial in stated ways rather than arbitrary ones: a value-taking flag standing
66
+ * Partial by construction, and partial in stated ways rather than arbitrary ones: A value-taking flag standing
66
67
  * immediately before the program name hides it (`npx -p foo nmr`), and a launcher outside `LAUNCHERS` goes
67
- * unreported. What it finds is the boundary below which nmr cannot tell its own processes from the tools it
68
- * runs.
68
+ * unreported. What it finds is the boundary below which nmr cannot tell its own processes from the tools that
69
+ * it runs.
69
70
  */
70
71
  export declare function findNmrCrossing(steps: readonly Step[]): string | undefined;
71
72
  /**
72
73
  * Returns the first token of a composite element that falls outside the grammar, or `undefined` when the
73
- * element is a command name optionally preceded by nmr's own flags. A token the shell would not read
74
- * literally is one the rendering would quote whole, turning the element into a command nobody wrote.
74
+ * element is a command name optionally preceded by nmr's own flags. A token that the shell would not read
75
+ * literally would be quoted whole by the rendering, turning the element into a command that nobody wrote.
75
76
  */
76
77
  export declare function findUnexpressibleToken(element: string): string | undefined;
78
+ /** Quotes a token that the shell would not read literally, and leaves every other token bare. */
79
+ export declare function quoteToken(token: string): string;
77
80
  /**
78
- * Returns the command a structural step's nmr process runs, whether the step hands that command to other
79
- * scopes rather than running it where it stands, and whether it anchors the command at the monorepo root.
80
- * Reports nothing for a step that names no nmr command.
81
+ * Returns the command that a structural step's nmr process runs, whether the step hands that command to other
82
+ * scopes rather than running it in its own scope, and whether it anchors the command at the monorepo root.
83
+ * Reports nothing for a step that does not name an nmr command.
81
84
  *
82
- * The inverse of `composeNmrStep`, and beside it because the two share one grammar: an element may lead with
85
+ * The inverse of `composeNmrStep`, and beside it because the two share one grammar: An element may lead with
83
86
  * nmr's own flags, and the command is the first token that is not one.
84
87
  */
85
88
  export declare function readNmrStep(step: Step): NmrStepTarget | undefined;
86
89
  /**
87
- * Reports whether a `package.json` entry re-invokes the command it is declared under, and whether it declares
88
- * anything besides. Reports nothing for an entry that names another command, or none.
90
+ * Reports whether a `package.json` entry re-invokes the command under which it is declared, and whether it
91
+ * declares anything besides. Reports nothing for an entry that names another command, or none.
89
92
  *
90
- * Honouring such an entry would spawn a shell running the same command in the same directory, reaching the
91
- * same entry again without bound, so resolution discards it however it reads. `chained` is what separates an
92
- * entry that thereby loses steps from one that declares nothing to lose.
93
+ * Honouring such an entry would spawn a shell running the same command in the same directory, which reads the
94
+ * same entry again without bound, so resolution discards it however it reads. `chained` separates an entry that
95
+ * thereby loses steps from one that declares nothing to lose.
93
96
  *
94
- * A segment that delegates carries the command to other scopes rather than back to this one. `-w` re-enters
95
- * only from the root, a package's entry reaching the root's registry and `package.json` instead of its own.
97
+ * A segment that delegates sends the command to other scopes rather than back to this one. `-w` re-enters
98
+ * only from the root, a package's `-w` entry resolving against the root's registry and `package.json` instead of its own.
96
99
  *
97
- * Partial in the same ways `findNmrCrossing` is, and for the same reason: what goes unrecognized re-enters
100
+ * Partial in the same ways `findNmrCrossing` is, and for the same reason: What goes unrecognized re-enters
98
101
  * without bound, which hangs rather than passing quietly.
99
102
  */
100
103
  export declare function readSelfReference(options: {
@@ -103,9 +106,9 @@ export declare function readSelfReference(options: {
103
106
  script: string;
104
107
  }): SelfReference | undefined;
105
108
  /**
106
- * Renders a step list as the `&&` chain a shell runs.
109
+ * Renders a step list as the `&&` chain that a shell runs.
107
110
  *
108
- * The sole producer of a chain string: the check-result cache keys on that string, so a change to the rendering
109
- * invalidates every recorded pass by construction rather than by anyone remembering to.
111
+ * The sole producer of a chain string: The check-result cache keys on that string, so a change to the rendering
112
+ * invalidates every recorded pass.
110
113
  */
111
114
  export declare function renderChain(steps: readonly Step[]): string;
package/dist/esm/steps.js CHANGED
@@ -33,6 +33,12 @@ export function findNmrCrossing(steps) {
33
33
  export function findUnexpressibleToken(element) {
34
34
  return tokenize(element).find((token) => !SHELL_SAFE_TOKEN.test(token));
35
35
  }
36
+ export function quoteToken(token) {
37
+ if (SHELL_SAFE_TOKEN.test(token)) {
38
+ return token;
39
+ }
40
+ return "'" + token.replaceAll("'", String.raw `'\''`) + "'";
41
+ }
36
42
  export function readNmrStep(step) {
37
43
  if (step.kind !== 'structural') {
38
44
  return undefined;
@@ -66,12 +72,6 @@ function isRedirectionOperator(char, precedingText, nextChar) {
66
72
  }
67
73
  return (char === '&' && nextChar === '>') || /[<>]\s*$/.test(precedingText);
68
74
  }
69
- function quoteToken(token) {
70
- if (SHELL_SAFE_TOKEN.test(token)) {
71
- return token;
72
- }
73
- return "'" + token.replaceAll("'", String.raw `'\''`) + "'";
74
- }
75
75
  function readNmrTail(segment) {
76
76
  const [head, ...rest] = dropLeadingAssignments(tokenizeSegment(segment));
77
77
  if (head === undefined) {
@@ -12,7 +12,7 @@ export type TazeConfig = {
12
12
  /**
13
13
  * Builds a taze configuration from nmr's shared upgrade policy and a repo's own settings.
14
14
  *
15
- * Any property the caller supplies wins, including `0` and an explicit `undefined` — the latter restores
15
+ * Any property that the caller supplies wins, including `0` and an explicit `undefined`. The latter restores
16
16
  * taze's inheritance of pnpm's `minimumReleaseAge`, which only applies while `maturityPeriod` is unset.
17
17
  *
18
18
  * The merge is shallow, which suits a policy of scalar defaults. A nested default would clobber rather
@@ -1,29 +1,34 @@
1
1
  /**
2
- * Declares a suite asserting that a repo's test files hold to nmr's conventions: every collected file names an
3
- * isolation tier, and every test file sits under a `__tests__` directory.
2
+ * Declares a suite asserting that a repo's test files follow nmr's conventions: Every collected file names an
3
+ * isolation tier, and every test file is under a `__tests__` directory.
4
4
  *
5
- * Wire it from a one-line test of the repo's own, so the file stays under the repo's `__tests__` and the repo
6
- * scopes what the sweep covers. Both halves are silent without it: `unit` is the residual project, so an untiered
7
- * file runs and passes, and a file outside `__tests__` is collected by nothing at all.
5
+ * Call it from a one-line test of the repo's own, so that the file stays under the repo's `__tests__` and the repo
6
+ * scopes what the sweep covers. Without it, a violation of either half goes unreported: `unit` is the residual
7
+ * project, so an untiered file runs and passes, and a file outside `__tests__` is collected by nothing at all.
8
8
  *
9
- * `excludedBasenames` names directory basenames the sweep prunes at any depth, additive to the ones nmr always
9
+ * `excludedBasenames` names directory basenames that the sweep prunes at any depth, additive to the ones nmr always
10
10
  * prunes. Pass the same array to `defineVitestConfig`'s `testCollectionExclude`, or the sweep and the collection
11
- * glob describe different trees: a directory pruned here alone still runs the files this suite stopped reporting.
11
+ * glob describe different trees: Vitest still runs the files in a directory pruned here alone, and this suite no
12
+ * longer reports them.
13
+ *
14
+ * Because untracked paths that git ignores are out of scope for the sweep and the shared configs' collection alike,
15
+ * a directory that `.gitignore` covers does not need an entry in `excludedBasenames`. Outside a git repository, only
16
+ * the named directories are pruned.
12
17
  */
13
18
  export declare function checkTestFileConventions(options?: TestFileConventionsOptions): void;
14
19
  /**
15
- * Sweeps a repo for both halves of the convention, reporting the root it swept alongside what it found.
20
+ * Sweeps a repo for both halves of the convention, reporting the root that it swept alongside what it found.
16
21
  *
17
- * The root is what makes the result trustworthy, and it is the one thing an assertion cannot check: a sweep
18
- * started from the directory Vitest supplies covers one package, finds nothing, and passes.
22
+ * The root makes the result trustworthy, and it is the one thing that an assertion cannot check: A sweep started
23
+ * from the directory that Vitest supplies covers one package, finds nothing, and passes.
19
24
  *
20
25
  * @internal - Exported only to enable testing
21
26
  */
22
27
  export declare function reportTestFileConventions(options?: TestFileConventionsOptions): TestFileConventionsReport;
23
28
  export interface TestFileConventionsOptions {
24
29
  /**
25
- * Directory basenames the sweep prunes at any depth, additive to the ones nmr always prunes. The array a repo
26
- * passes here is the array it passes to `defineVitestConfig`'s `testCollectionExclude`.
30
+ * Directory basenames that the sweep prunes at any depth, additive to the ones nmr always prunes and to the paths
31
+ * that git ignores. A repo passes the same array here and to `defineVitestConfig`'s `testCollectionExclude`.
27
32
  */
28
33
  excludedBasenames?: readonly string[];
29
34
  /**
@@ -32,7 +37,7 @@ export interface TestFileConventionsOptions {
32
37
  */
33
38
  rootDir?: string;
34
39
  }
35
- /** What one sweep found, and the root it found it in. Paths are relative to that root and POSIX-separated. */
40
+ /** What one sweep found, and the root in which it found it. Paths are relative to that root and POSIX-separated. */
36
41
  export interface TestFileConventionsReport {
37
42
  misplacedFiles: string[];
38
43
  rootDir: string;
package/dist/esm/tests.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { findMisplacedTestFiles, findUntieredTestFiles, TIER_NAMES } from "./tiers.js";
3
3
  import { findMonorepoRoot } from "./workspace.js";
4
- const MISPLACED_REMEDY = 'Every test file must sit under a `__tests__` directory, the only place the shared Vitest config collects from. Move each file below into one, or name its directory in `excludedBasenames`.';
4
+ const MISPLACED_REMEDY = 'Every test file must be under a `__tests__` directory, the only place from which the shared Vitest config collects. Move each file below into one, or name its directory in `excludedBasenames`.';
5
5
  const UNTIERED_REMEDY = `Every collected test file must name its isolation tier in the segment before \`.test.\`, one of: ${TIER_NAMES.join(', ')}. Rename each file below to <subject>[.<aspect>].<tier>.test.ts.`;
6
6
  const RETIRED_OPTION_KEYS = new Map([['exclude', 'excludedBasenames']]);
7
7
  export function checkTestFileConventions(options = {}) {
@@ -11,7 +11,7 @@ export function checkTestFileConventions(options = {}) {
11
11
  it('every collected test file names its tier', () => {
12
12
  expect(untieredFiles, `${UNTIERED_REMEDY} Swept from ${rootDir}.`).toStrictEqual([]);
13
13
  });
14
- it('every test file sits under a __tests__ directory', () => {
14
+ it('every test file is under a __tests__ directory', () => {
15
15
  expect(misplacedFiles, `${MISPLACED_REMEDY} Swept from ${rootDir}.`).toStrictEqual([]);
16
16
  });
17
17
  });