@williamthorsen/nmr 0.38.0 → 0.39.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 (64) hide show
  1. package/.meta/changelog.json +84 -0
  2. package/.readyup/kits/default.js +84 -79
  3. package/.readyup/manifest.json +5 -5
  4. package/CHANGELOG.md +70 -0
  5. package/README.md +37 -11
  6. package/dist/esm/check-cache.d.ts +2 -2
  7. package/dist/esm/check-cache.js +24 -24
  8. package/dist/esm/cli-ensure-prepublish-hooks.js +4 -4
  9. package/dist/esm/commands/build-output.d.ts +2 -2
  10. package/dist/esm/commands/build-output.js +9 -9
  11. package/dist/esm/commands/build.js +65 -63
  12. package/dist/esm/commands/clean.js +11 -11
  13. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +2 -2
  14. package/dist/esm/commands/ensure-prepublish-hooks.js +41 -41
  15. package/dist/esm/commands/fmt.js +33 -33
  16. package/dist/esm/commands/report-catalog.js +9 -7
  17. package/dist/esm/commands/report-overrides.js +7 -7
  18. package/dist/esm/config.js +25 -25
  19. package/dist/esm/context.js +8 -8
  20. package/dist/esm/help.d.ts +2 -2
  21. package/dist/esm/help.js +21 -21
  22. package/dist/esm/helpers/clampToBytes.js +3 -3
  23. package/dist/esm/helpers/createBoundedBuffer.d.ts +1 -1
  24. package/dist/esm/helpers/createBoundedBuffer.js +2 -2
  25. package/dist/esm/helpers/duration.d.ts +2 -2
  26. package/dist/esm/helpers/duration.js +7 -7
  27. package/dist/esm/helpers/filter-selection.js +12 -12
  28. package/dist/esm/helpers/findClosestName.js +15 -15
  29. package/dist/esm/helpers/package-json.d.ts +2 -2
  30. package/dist/esm/helpers/package-json.js +26 -25
  31. package/dist/esm/helpers/transcript.d.ts +2 -2
  32. package/dist/esm/helpers/transcript.js +14 -14
  33. package/dist/esm/prettier.js +5 -5
  34. package/dist/esm/recording.d.ts +5 -5
  35. package/dist/esm/recording.js +21 -14
  36. package/dist/esm/report-format.d.ts +1 -1
  37. package/dist/esm/report-format.js +7 -7
  38. package/dist/esm/resolveBinStyles.js +3 -3
  39. package/dist/esm/resolver.d.ts +2 -2
  40. package/dist/esm/resolver.js +15 -15
  41. package/dist/esm/runCli.js +252 -186
  42. package/dist/esm/runner.d.ts +1 -1
  43. package/dist/esm/runner.js +18 -18
  44. package/dist/esm/steps.d.ts +13 -1
  45. package/dist/esm/steps.js +32 -27
  46. package/dist/esm/tests.d.ts +7 -7
  47. package/dist/esm/tests.js +20 -8
  48. package/dist/esm/tiers.d.ts +1 -1
  49. package/dist/esm/tiers.js +8 -8
  50. package/dist/esm/verbosity.d.ts +1 -1
  51. package/dist/esm/verbosity.js +7 -7
  52. package/dist/esm/verdict.d.ts +9 -2
  53. package/dist/esm/verdict.js +63 -50
  54. package/dist/esm/vitest-source-resolution.js +48 -46
  55. package/dist/esm/vitest.d.ts +2 -2
  56. package/dist/esm/vitest.js +74 -25
  57. package/dist/esm/workspace.d.ts +2 -18
  58. package/dist/esm/workspace.js +9 -67
  59. package/docs/reporting.md +6 -2
  60. package/docs/vitest.md +6 -6
  61. package/docs/workspace.md +24 -1
  62. package/package.json +3 -4
  63. package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
  64. package/dist/esm/helpers/workspace-patterns.js +0 -29
@@ -32,7 +32,7 @@ export interface RunStepsResult {
32
32
  * What the list's opaque steps produced, absent unless every one of them was captured whole and there was
33
33
  * at least one to capture.
34
34
  */
35
- retained?: RetainedOutput;
35
+ retainedOutput?: RetainedOutput;
36
36
  }
37
37
  /** How a command ended, and the exit code that carries that ending to nmr's own caller. */
38
38
  type CommandCompletion = {
@@ -14,7 +14,7 @@ export function resolveInheritedChannel(stream) {
14
14
  return hasDescriptor(stream) ? stream.fd : 'pipe';
15
15
  }
16
16
  export async function runSteps(steps, cwd, options) {
17
- const resolved = {
17
+ const resolvedOptions = {
18
18
  ...options,
19
19
  quiet: options.quiet === true,
20
20
  stdout: options.stdout ?? process.stdout,
@@ -23,7 +23,7 @@ export async function runSteps(steps, cwd, options) {
23
23
  const parts = [];
24
24
  let isCaptureWhole = true;
25
25
  for (const step of steps) {
26
- const result = await runStep(step, cwd, resolved);
26
+ const result = await runStep(step, cwd, resolvedOptions);
27
27
  if (step.kind === 'opaque') {
28
28
  if (result.stderr === undefined || result.stdout === undefined) {
29
29
  isCaptureWhole = false;
@@ -39,31 +39,31 @@ 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, input: 'inherit', useShell: true }, cwd, options);
42
+ return runSpawned({ args: [], file: command, input: 'inherit', shouldUseShell: true }, cwd, options);
43
43
  }
44
- async function runSpawned({ args, file, input, useShell }, cwd, options) {
44
+ async function runSpawned({ args, file, input, shouldUseShell }, 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
- shell: useShell,
50
+ shell: shouldUseShell,
51
51
  stdio: [input, options.channels.stdout, options.channels.stderr],
52
52
  cwd,
53
53
  env,
54
54
  });
55
55
  const stdoutCapture = child.stdout === null ? undefined : captureStream(child.stdout, quiet ? undefined : stdout);
56
56
  const stderrCapture = child.stderr === null ? undefined : captureStream(child.stderr, quiet ? undefined : stderr);
57
- const completion = await awaitCompletion(child, () => {
57
+ const commandCompletion = await awaitCompletion(child, () => {
58
58
  stdoutCapture?.stopReading();
59
59
  stderrCapture?.stopReading();
60
60
  });
61
61
  stdoutCapture?.detach();
62
62
  stderrCapture?.detach();
63
63
  const result = {
64
- ...completion,
65
- stdout: stdoutCapture?.toBuffer(),
66
- stderr: stderrCapture?.toBuffer(),
64
+ ...commandCompletion,
65
+ stdout: stdoutCapture?.readBuffer(),
66
+ stderr: stderrCapture?.readBuffer(),
67
67
  };
68
68
  if (result.outcome === 'spawn-failed') {
69
69
  reportError(result.error.message, stderr);
@@ -78,14 +78,14 @@ async function runSpawned({ args, file, input, 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, input: 'inherit', useShell: true }, cwd, {
81
+ return runSpawned({ args: [], file: step.command, input: 'inherit', shouldUseShell: 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
+ return runSpawned({ args, file, input, shouldUseShell: false }, cwd, {
89
89
  ...options,
90
90
  quiet: false,
91
91
  channels: { stderr: resolveInheritedChannel(stderr), stdout: resolveInheritedChannel(stdout) },
@@ -96,14 +96,14 @@ function composeRetainedOutput(parts, isCaptureWhole) {
96
96
  return {};
97
97
  }
98
98
  return {
99
- retained: {
99
+ retainedOutput: {
100
100
  stderr: Buffer.concat(parts.map((part) => part.stderr)),
101
101
  stdout: Buffer.concat(parts.map((part) => part.stdout)),
102
102
  },
103
103
  };
104
104
  }
105
105
  function captureStream(source, destination) {
106
- const retained = createBoundedBuffer();
106
+ const retainedCopy = createBoundedBuffer();
107
107
  let isComplete = true;
108
108
  function stopReading() {
109
109
  if (destination !== undefined)
@@ -119,9 +119,9 @@ function captureStream(source, destination) {
119
119
  destination.on('error', abandon);
120
120
  source.pipe(destination, { end: false });
121
121
  }
122
- source.on('data', (chunk) => retained.append(chunk));
122
+ source.on('data', (chunk) => retainedCopy.append(chunk));
123
123
  return {
124
- toBuffer: () => (isComplete ? retained.toBuffer() : undefined),
124
+ readBuffer: () => (isComplete ? retainedCopy.readBuffer() : undefined),
125
125
  detach: () => {
126
126
  destination?.off('error', abandon);
127
127
  },
@@ -145,11 +145,11 @@ function awaitCompletion(child, stopReading) {
145
145
  settle({ outcome: 'spawn-failed', exitCode: 1, error });
146
146
  });
147
147
  child.on('exit', (code, signal) => {
148
- const completion = describeExit(code, signal);
149
- exitCompletion = completion;
148
+ const exitDescription = describeExit(code, signal);
149
+ exitCompletion = exitDescription;
150
150
  graceTimer = setTimeout(() => {
151
151
  stopReading();
152
- settle(completion);
152
+ settle(exitDescription);
153
153
  }, PIPE_DRAIN_GRACE_MS);
154
154
  });
155
155
  child.on('close', () => {
@@ -25,6 +25,11 @@ export interface NmrStepTarget {
25
25
  command: string;
26
26
  /** Whether the command is fanned out to other scopes, where a `-R` or `-F` sends it. */
27
27
  isDelegate: boolean;
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.
31
+ */
32
+ isRecursive: boolean;
28
33
  /** Whether the command runs against the root registry, where a `-w` anchors it. */
29
34
  isWorkspaceRoot: boolean;
30
35
  }
@@ -42,7 +47,14 @@ export type SelfReference = 'chained' | 'sole';
42
47
  * `shouldDeclineArguments` is set only where it holds, so a step that takes the trailing arguments renders and compares
43
48
  * exactly as it did before any element declared anything.
44
49
  */
45
- export declare function composeNmrStep(element: string, workspaceRoot: boolean, shouldDeclineArguments?: boolean): Step;
50
+ export declare function composeNmrStep(element: string, isWorkspaceRoot: boolean, shouldDeclineArguments?: boolean): Step;
51
+ /**
52
+ * Returns the steps that still run in a workspace holding no package: every step but the ones a `-R` fans out.
53
+ *
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.
56
+ */
57
+ export declare function dropRecursiveSteps(steps: readonly Step[]): Step[];
46
58
  /**
47
59
  * Returns the text of the first opaque step that reaches nmr through a shell, or `undefined` when none does.
48
60
  *
package/dist/esm/steps.js CHANGED
@@ -11,14 +11,17 @@ 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, shouldDeclineArguments = false) {
15
- const flags = workspaceRoot ? ['-w'] : [];
14
+ export function composeNmrStep(element, isWorkspaceRoot, shouldDeclineArguments = false) {
15
+ const flags = isWorkspaceRoot ? ['-w'] : [];
16
16
  return {
17
17
  kind: 'structural',
18
18
  argv: ['nmr', ...flags, ...tokenize(element)],
19
19
  ...(shouldDeclineArguments && { shouldDeclineArguments }),
20
20
  };
21
21
  }
22
+ export function dropRecursiveSteps(steps) {
23
+ return steps.filter((step) => readNmrStep(step)?.isRecursive !== true);
24
+ }
22
25
  export function findNmrCrossing(steps) {
23
26
  for (const step of steps) {
24
27
  if (step.kind === 'opaque' && splitSegments(step.command).some((segment) => readNmrTail(segment) !== undefined)) {
@@ -40,12 +43,12 @@ export function readNmrStep(step) {
40
43
  export function readSelfReference(options) {
41
44
  const { anchoredAtRoot, commandName, script } = options;
42
45
  const segments = splitSegments(script).filter((segment) => segment.trim() !== '');
43
- const reenters = segments.some((segment) => {
46
+ const doesReenter = segments.some((segment) => {
44
47
  const tail = readNmrTail(segment);
45
48
  const target = tail === undefined ? undefined : readNmrTarget(tail);
46
49
  return target?.command === commandName && !target.isDelegate && (anchoredAtRoot || !target.isWorkspaceRoot);
47
50
  });
48
- if (!reenters) {
51
+ if (!doesReenter) {
49
52
  return undefined;
50
53
  }
51
54
  return segments.length === 1 ? 'sole' : 'chained';
@@ -90,6 +93,7 @@ function readNmrTail(segment) {
90
93
  }
91
94
  function readNmrTarget(tokens) {
92
95
  let isDelegate = false;
96
+ let isRecursive = false;
93
97
  let isWorkspaceRoot = false;
94
98
  let index = 0;
95
99
  while (index < tokens.length) {
@@ -103,6 +107,7 @@ function readNmrTarget(tokens) {
103
107
  case '-R':
104
108
  case '--recursive':
105
109
  isDelegate = true;
110
+ isRecursive = true;
106
111
  index += 1;
107
112
  break;
108
113
  case '-w':
@@ -112,7 +117,7 @@ function readNmrTarget(tokens) {
112
117
  break;
113
118
  default:
114
119
  if (!token.startsWith('-')) {
115
- return { command: token, isDelegate, isWorkspaceRoot };
120
+ return { command: token, isDelegate, isRecursive, isWorkspaceRoot };
116
121
  }
117
122
  index += 1;
118
123
  }
@@ -124,49 +129,49 @@ function renderStep(step) {
124
129
  }
125
130
  function splitSegments(command) {
126
131
  const segments = [];
127
- let current = '';
132
+ let currentSegment = '';
128
133
  let quote;
129
134
  let index = 0;
130
135
  while (index < command.length) {
131
136
  const char = command[index] ?? '';
132
137
  if (quote !== undefined) {
133
138
  if (char === '\\' && quote === '"') {
134
- current += char + (command[index + 1] ?? '');
139
+ currentSegment += char + (command[index + 1] ?? '');
135
140
  index += 2;
136
141
  continue;
137
142
  }
138
- current += char;
143
+ currentSegment += char;
139
144
  if (char === quote)
140
145
  quote = undefined;
141
146
  index += 1;
142
147
  continue;
143
148
  }
144
149
  if (char === '\\') {
145
- current += char + (command[index + 1] ?? '');
150
+ currentSegment += char + (command[index + 1] ?? '');
146
151
  index += 2;
147
152
  continue;
148
153
  }
149
154
  if (QUOTES.has(char)) {
150
155
  quote = char;
151
- current += char;
156
+ currentSegment += char;
152
157
  index += 1;
153
158
  continue;
154
159
  }
155
160
  if (SEGMENT_SEPARATORS.has(char)) {
156
- if (isRedirectionOperator(char, current, command[index + 1])) {
157
- current += char;
161
+ if (isRedirectionOperator(char, currentSegment, command[index + 1])) {
162
+ currentSegment += char;
158
163
  index += 1;
159
164
  continue;
160
165
  }
161
166
  index += command[index + 1] === char ? 2 : 1;
162
- segments.push(current);
163
- current = '';
167
+ segments.push(currentSegment);
168
+ currentSegment = '';
164
169
  continue;
165
170
  }
166
- current += char;
171
+ currentSegment += char;
167
172
  index += 1;
168
173
  }
169
- segments.push(current);
174
+ segments.push(currentSegment);
170
175
  return segments;
171
176
  }
172
177
  function tokenize(element) {
@@ -174,47 +179,47 @@ function tokenize(element) {
174
179
  }
175
180
  function tokenizeSegment(segment) {
176
181
  const tokens = [];
177
- let current = '';
182
+ let currentToken = '';
178
183
  let quote;
179
184
  let index = 0;
180
185
  while (index < segment.length) {
181
186
  const char = segment[index] ?? '';
182
187
  if (quote !== undefined) {
183
188
  if (char === '\\' && quote === '"') {
184
- current += char + (segment[index + 1] ?? '');
189
+ currentToken += char + (segment[index + 1] ?? '');
185
190
  index += 2;
186
191
  continue;
187
192
  }
188
- current += char;
193
+ currentToken += char;
189
194
  if (char === quote)
190
195
  quote = undefined;
191
196
  index += 1;
192
197
  continue;
193
198
  }
194
199
  if (char === '\\') {
195
- current += char + (segment[index + 1] ?? '');
200
+ currentToken += char + (segment[index + 1] ?? '');
196
201
  index += 2;
197
202
  continue;
198
203
  }
199
204
  if (QUOTES.has(char)) {
200
205
  quote = char;
201
- current += char;
206
+ currentToken += char;
202
207
  index += 1;
203
208
  continue;
204
209
  }
205
210
  if (TOKEN_SEPARATORS.has(char)) {
206
- if (current !== '') {
207
- tokens.push(current);
211
+ if (currentToken !== '') {
212
+ tokens.push(currentToken);
208
213
  }
209
- current = '';
214
+ currentToken = '';
210
215
  index += 1;
211
216
  continue;
212
217
  }
213
- current += char;
218
+ currentToken += char;
214
219
  index += 1;
215
220
  }
216
- if (current !== '') {
217
- tokens.push(current);
221
+ if (currentToken !== '') {
222
+ tokens.push(currentToken);
218
223
  }
219
224
  return tokens;
220
225
  }
@@ -6,9 +6,9 @@
6
6
  * scopes what the sweep covers. Both halves are silent without it: `unit` is the residual project, so an untiered
7
7
  * file runs and passes, and a file outside `__tests__` is collected by nothing at all.
8
8
  *
9
- * `exclude` names directory basenames the sweep prunes at any depth, additive to the ones nmr always prunes. Pass
10
- * the same array to `defineVitestConfig`'s `testCollectionExclude`, or the sweep and the collection glob describe
11
- * different trees: a directory pruned here alone still runs the files this suite stopped reporting.
9
+ * `excludedBasenames` names directory basenames the sweep prunes at any depth, additive to the ones nmr always
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.
12
12
  */
13
13
  export declare function checkTestFileConventions(options?: TestFileConventionsOptions): void;
14
14
  /**
@@ -19,13 +19,13 @@ export declare function checkTestFileConventions(options?: TestFileConventionsOp
19
19
  *
20
20
  * @internal - Exported only to enable testing
21
21
  */
22
- export declare function reportTestFileConventions({ exclude, rootDir, }?: TestFileConventionsOptions): TestFileConventionsReport;
22
+ export declare function reportTestFileConventions(options?: TestFileConventionsOptions): TestFileConventionsReport;
23
23
  export interface TestFileConventionsOptions {
24
24
  /**
25
25
  * Directory basenames the sweep prunes at any depth, additive to the ones nmr always prunes. The array a repo
26
26
  * passes here is the array it passes to `defineVitestConfig`'s `testCollectionExclude`.
27
27
  */
28
- exclude?: readonly string[];
28
+ excludedBasenames?: readonly string[];
29
29
  /**
30
30
  * The directory to sweep. Defaults to the monorepo root found from the working directory, so one call covers the
31
31
  * whole repo from whichever package Vitest started in.
@@ -34,7 +34,7 @@ export interface TestFileConventionsOptions {
34
34
  }
35
35
  /** What one sweep found, and the root it found it in. Paths are relative to that root and POSIX-separated. */
36
36
  export interface TestFileConventionsReport {
37
- misplaced: string[];
37
+ misplacedFiles: string[];
38
38
  rootDir: string;
39
- untiered: string[];
39
+ untieredFiles: string[];
40
40
  }
package/dist/esm/tests.js CHANGED
@@ -1,25 +1,37 @@
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 `exclude`.';
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`.';
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
+ const RETIRED_OPTION_KEYS = new Map([['exclude', 'excludedBasenames']]);
6
7
  export function checkTestFileConventions(options = {}) {
8
+ assertNoRetiredOptions(options);
7
9
  describe('test file conventions', () => {
8
- const { misplaced, rootDir, untiered } = reportTestFileConventions(options);
10
+ const { misplacedFiles, rootDir, untieredFiles } = reportTestFileConventions(options);
9
11
  it('every collected test file names its tier', () => {
10
- expect(untiered, `${UNTIERED_REMEDY} Swept from ${rootDir}.`).toStrictEqual([]);
12
+ expect(untieredFiles, `${UNTIERED_REMEDY} Swept from ${rootDir}.`).toStrictEqual([]);
11
13
  });
12
14
  it('every test file sits under a __tests__ directory', () => {
13
- expect(misplaced, `${MISPLACED_REMEDY} Swept from ${rootDir}.`).toStrictEqual([]);
15
+ expect(misplacedFiles, `${MISPLACED_REMEDY} Swept from ${rootDir}.`).toStrictEqual([]);
14
16
  });
15
17
  });
16
18
  }
17
- export function reportTestFileConventions({ exclude, rootDir, } = {}) {
19
+ export function reportTestFileConventions(options = {}) {
20
+ assertNoRetiredOptions(options);
21
+ const { excludedBasenames, rootDir } = options;
18
22
  const sweptRoot = rootDir ?? findMonorepoRoot();
19
- const scanOptions = { ...(exclude !== undefined && { exclude }) };
23
+ const scanOptions = { ...(excludedBasenames !== undefined && { excludedBasenames }) };
20
24
  return {
21
- misplaced: findMisplacedTestFiles(sweptRoot, scanOptions),
25
+ misplacedFiles: findMisplacedTestFiles(sweptRoot, scanOptions),
22
26
  rootDir: sweptRoot,
23
- untiered: findUntieredTestFiles(sweptRoot, scanOptions),
27
+ untieredFiles: findUntieredTestFiles(sweptRoot, scanOptions),
24
28
  };
25
29
  }
30
+ function assertNoRetiredOptions(options) {
31
+ for (const key of Object.keys(options)) {
32
+ const replacement = RETIRED_OPTION_KEYS.get(key);
33
+ if (replacement !== undefined) {
34
+ throw new TypeError(`Invalid test-file-conventions options: \`${key}\` was renamed to \`${replacement}\`.`);
35
+ }
36
+ }
37
+ }
@@ -50,7 +50,7 @@ export interface TestFileScanOptions {
50
50
  * config's `testCollectionExclude`. A directory pruned from the sweep but still collected by Vitest is the
51
51
  * silence these sweeps exist to end.
52
52
  */
53
- exclude?: readonly string[];
53
+ excludedBasenames?: readonly string[];
54
54
  }
55
55
  /**
56
56
  * The isolation ladder, ordered by the furthest thing a test reaches. A tier names what a test reaches, never how it
package/dist/esm/tiers.js CHANGED
@@ -23,26 +23,26 @@ export function hasTierInfix(filePath) {
23
23
  }
24
24
  export const TEST_COLLECTION_EXCLUDE = ['.git', 'coverage', 'dist', 'node_modules'];
25
25
  export const TIER_NAMES = ['unit', 'tool', 'localhost', 'remote'];
26
- function collectTestFiles(dir, relativeDir, inTestDir, context) {
26
+ function collectTestFiles(dir, relativeDir, isInTestDir, context) {
27
27
  const entries = readdirSync(dir, { withFileTypes: true });
28
28
  for (const entry of entries) {
29
29
  const relativePath = relativeDir === '' ? entry.name : `${relativeDir}/${entry.name}`;
30
30
  if (entry.isDirectory()) {
31
31
  if (context.pruned.has(entry.name))
32
32
  continue;
33
- collectTestFiles(path.join(dir, entry.name), relativePath, inTestDir || entry.name === TEST_DIR, context);
33
+ collectTestFiles(path.join(dir, entry.name), relativePath, isInTestDir || entry.name === TEST_DIR, context);
34
34
  }
35
- else if (inTestDir !== context.misplaced && TEST_FILE_PATTERN.test(entry.name)) {
36
- context.found.push(relativePath);
35
+ else if (isInTestDir !== context.misplaced && TEST_FILE_PATTERN.test(entry.name)) {
36
+ context.foundPaths.push(relativePath);
37
37
  }
38
38
  }
39
39
  }
40
- function walkTestFiles(rootDir, { exclude = [] }, misplaced) {
40
+ function walkTestFiles(rootDir, { excludedBasenames = [] }, misplaced) {
41
41
  const context = {
42
- found: [],
42
+ foundPaths: [],
43
43
  misplaced,
44
- pruned: new Set([...TEST_COLLECTION_EXCLUDE, ...exclude]),
44
+ pruned: new Set([...TEST_COLLECTION_EXCLUDE, ...excludedBasenames]),
45
45
  };
46
46
  collectTestFiles(rootDir, '', false, context);
47
- return context.found.toSorted();
47
+ return context.foundPaths.toSorted();
48
48
  }
@@ -38,8 +38,8 @@ export interface ResolveVerbosityOptions {
38
38
  /** Read for the detection level, and for that alone: the variable this module owns arrives as `envVerbosity`. */
39
39
  env: NodeJS.ProcessEnv;
40
40
  envVerbosity: CommandVerbosity | undefined;
41
+ hasQuietFlag: boolean;
41
42
  output: OutputConfig | undefined;
42
- quietFlag: boolean;
43
43
  }
44
44
  /**
45
45
  * Resolves the verbosity this process runs at: the flag, then the environment, then the repo's config, then
@@ -9,18 +9,18 @@ export function isCommandVerbosity(value) {
9
9
  return names.includes(value);
10
10
  }
11
11
  export function readVerbosityEnv(env) {
12
- const raw = env[COMMAND_VERBOSITY_ENV_VAR];
13
- if (raw === undefined || raw === '') {
12
+ const rawValue = env[COMMAND_VERBOSITY_ENV_VAR];
13
+ if (rawValue === undefined || rawValue === '') {
14
14
  return { ok: true };
15
15
  }
16
- if (!isCommandVerbosity(raw)) {
17
- return { ok: false, error: formatVerbosityRejection(COMMAND_VERBOSITY_ENV_VAR, raw) };
16
+ if (!isCommandVerbosity(rawValue)) {
17
+ return { ok: false, error: formatVerbosityRejection(COMMAND_VERBOSITY_ENV_VAR, rawValue) };
18
18
  }
19
- return { ok: true, verbosity: raw };
19
+ return { ok: true, verbosity: rawValue };
20
20
  }
21
21
  export function resolveVerbosity(options) {
22
- const { env, envVerbosity, output, quietFlag } = options;
23
- if (quietFlag)
22
+ const { env, envVerbosity, hasQuietFlag, output } = options;
23
+ if (hasQuietFlag)
24
24
  return 'quiet';
25
25
  if (envVerbosity !== undefined)
26
26
  return envVerbosity;
@@ -11,7 +11,14 @@ import type { ReportFormat } from './report-format.js';
11
11
  * truncation point from one machine to the next. The ceiling governs pipes: a terminal offers no such
12
12
  * guarantee at any size.
13
13
  */
14
- export declare const VERDICT_LINE_LIMIT = 512;
14
+ export declare const VERDICT_LINE_LIMIT_BYTES = 512;
15
+ /**
16
+ * Why a command ran nothing, which the `no-op` outcome carries and `--json` serializes.
17
+ *
18
+ * `empty-workspace` and `empty-override` both leave a chain with no step in it, so the reason is what tells
19
+ * a command emptied by the workspace apart from one emptied by an override.
20
+ */
21
+ export type NoOpReason = 'empty-override' | 'empty-workspace' | 'noop-override';
15
22
  /**
16
23
  * What a command nmr ran came to, holding the facts a reporting line is rendered from rather than the line
17
24
  * itself, so a machine-readable rendering spends the same record a human-readable one does.
@@ -47,7 +54,7 @@ export type VerdictOutcome = {
47
54
  replay?: ReplayLine[];
48
55
  } | {
49
56
  outcome: 'no-op';
50
- reason: 'empty-override' | 'noop-override';
57
+ reason: NoOpReason;
51
58
  });
52
59
  /**
53
60
  * Renders a verdict as the line nmr reports it on, without the newline that terminates it.