@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.
- package/.meta/changelog.json +84 -0
- package/.readyup/kits/default.js +84 -79
- package/.readyup/manifest.json +5 -5
- package/CHANGELOG.md +70 -0
- package/README.md +37 -11
- package/dist/esm/check-cache.d.ts +2 -2
- package/dist/esm/check-cache.js +24 -24
- package/dist/esm/cli-ensure-prepublish-hooks.js +4 -4
- package/dist/esm/commands/build-output.d.ts +2 -2
- package/dist/esm/commands/build-output.js +9 -9
- package/dist/esm/commands/build.js +65 -63
- package/dist/esm/commands/clean.js +11 -11
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +2 -2
- package/dist/esm/commands/ensure-prepublish-hooks.js +41 -41
- package/dist/esm/commands/fmt.js +33 -33
- package/dist/esm/commands/report-catalog.js +9 -7
- package/dist/esm/commands/report-overrides.js +7 -7
- package/dist/esm/config.js +25 -25
- package/dist/esm/context.js +8 -8
- package/dist/esm/help.d.ts +2 -2
- package/dist/esm/help.js +21 -21
- package/dist/esm/helpers/clampToBytes.js +3 -3
- package/dist/esm/helpers/createBoundedBuffer.d.ts +1 -1
- package/dist/esm/helpers/createBoundedBuffer.js +2 -2
- package/dist/esm/helpers/duration.d.ts +2 -2
- package/dist/esm/helpers/duration.js +7 -7
- package/dist/esm/helpers/filter-selection.js +12 -12
- package/dist/esm/helpers/findClosestName.js +15 -15
- package/dist/esm/helpers/package-json.d.ts +2 -2
- package/dist/esm/helpers/package-json.js +26 -25
- package/dist/esm/helpers/transcript.d.ts +2 -2
- package/dist/esm/helpers/transcript.js +14 -14
- package/dist/esm/prettier.js +5 -5
- package/dist/esm/recording.d.ts +5 -5
- package/dist/esm/recording.js +21 -14
- package/dist/esm/report-format.d.ts +1 -1
- package/dist/esm/report-format.js +7 -7
- package/dist/esm/resolveBinStyles.js +3 -3
- package/dist/esm/resolver.d.ts +2 -2
- package/dist/esm/resolver.js +15 -15
- package/dist/esm/runCli.js +252 -186
- package/dist/esm/runner.d.ts +1 -1
- package/dist/esm/runner.js +18 -18
- package/dist/esm/steps.d.ts +13 -1
- package/dist/esm/steps.js +32 -27
- package/dist/esm/tests.d.ts +7 -7
- package/dist/esm/tests.js +20 -8
- package/dist/esm/tiers.d.ts +1 -1
- package/dist/esm/tiers.js +8 -8
- package/dist/esm/verbosity.d.ts +1 -1
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +9 -2
- package/dist/esm/verdict.js +63 -50
- package/dist/esm/vitest-source-resolution.js +48 -46
- package/dist/esm/vitest.d.ts +2 -2
- package/dist/esm/vitest.js +74 -25
- package/dist/esm/workspace.d.ts +2 -18
- package/dist/esm/workspace.js +9 -67
- package/docs/reporting.md +6 -2
- package/docs/vitest.md +6 -6
- package/docs/workspace.md +24 -1
- package/package.json +3 -4
- package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
- package/dist/esm/helpers/workspace-patterns.js +0 -29
package/dist/esm/runner.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 = {
|
package/dist/esm/runner.js
CHANGED
|
@@ -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
|
|
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,
|
|
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',
|
|
42
|
+
return runSpawned({ args: [], file: command, input: 'inherit', shouldUseShell: true }, cwd, options);
|
|
43
43
|
}
|
|
44
|
-
async function runSpawned({ args, file, input,
|
|
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:
|
|
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
|
|
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
|
-
...
|
|
65
|
-
stdout: stdoutCapture?.
|
|
66
|
-
stderr: stderrCapture?.
|
|
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',
|
|
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,
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
122
|
+
source.on('data', (chunk) => retainedCopy.append(chunk));
|
|
123
123
|
return {
|
|
124
|
-
|
|
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
|
|
149
|
-
exitCompletion =
|
|
148
|
+
const exitDescription = describeExit(code, signal);
|
|
149
|
+
exitCompletion = exitDescription;
|
|
150
150
|
graceTimer = setTimeout(() => {
|
|
151
151
|
stopReading();
|
|
152
|
-
settle(
|
|
152
|
+
settle(exitDescription);
|
|
153
153
|
}, PIPE_DRAIN_GRACE_MS);
|
|
154
154
|
});
|
|
155
155
|
child.on('close', () => {
|
package/dist/esm/steps.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
15
|
-
const flags =
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
139
|
+
currentSegment += char + (command[index + 1] ?? '');
|
|
135
140
|
index += 2;
|
|
136
141
|
continue;
|
|
137
142
|
}
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
+
currentSegment += char;
|
|
152
157
|
index += 1;
|
|
153
158
|
continue;
|
|
154
159
|
}
|
|
155
160
|
if (SEGMENT_SEPARATORS.has(char)) {
|
|
156
|
-
if (isRedirectionOperator(char,
|
|
157
|
-
|
|
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(
|
|
163
|
-
|
|
167
|
+
segments.push(currentSegment);
|
|
168
|
+
currentSegment = '';
|
|
164
169
|
continue;
|
|
165
170
|
}
|
|
166
|
-
|
|
171
|
+
currentSegment += char;
|
|
167
172
|
index += 1;
|
|
168
173
|
}
|
|
169
|
-
segments.push(
|
|
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
|
|
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
|
-
|
|
189
|
+
currentToken += char + (segment[index + 1] ?? '');
|
|
185
190
|
index += 2;
|
|
186
191
|
continue;
|
|
187
192
|
}
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
+
currentToken += char;
|
|
202
207
|
index += 1;
|
|
203
208
|
continue;
|
|
204
209
|
}
|
|
205
210
|
if (TOKEN_SEPARATORS.has(char)) {
|
|
206
|
-
if (
|
|
207
|
-
tokens.push(
|
|
211
|
+
if (currentToken !== '') {
|
|
212
|
+
tokens.push(currentToken);
|
|
208
213
|
}
|
|
209
|
-
|
|
214
|
+
currentToken = '';
|
|
210
215
|
index += 1;
|
|
211
216
|
continue;
|
|
212
217
|
}
|
|
213
|
-
|
|
218
|
+
currentToken += char;
|
|
214
219
|
index += 1;
|
|
215
220
|
}
|
|
216
|
-
if (
|
|
217
|
-
tokens.push(
|
|
221
|
+
if (currentToken !== '') {
|
|
222
|
+
tokens.push(currentToken);
|
|
218
223
|
}
|
|
219
224
|
return tokens;
|
|
220
225
|
}
|
package/dist/esm/tests.d.ts
CHANGED
|
@@ -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
|
-
* `
|
|
10
|
-
* the same array to `defineVitestConfig`'s `testCollectionExclude`, or the sweep and the collection
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
37
|
+
misplacedFiles: string[];
|
|
38
38
|
rootDir: string;
|
|
39
|
-
|
|
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 `
|
|
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 {
|
|
10
|
+
const { misplacedFiles, rootDir, untieredFiles } = reportTestFileConventions(options);
|
|
9
11
|
it('every collected test file names its tier', () => {
|
|
10
|
-
expect(
|
|
12
|
+
expect(untieredFiles, `${UNTIERED_REMEDY} Swept from ${rootDir}.`).toStrictEqual([]);
|
|
11
13
|
});
|
|
12
14
|
it('every test file sits under a __tests__ directory', () => {
|
|
13
|
-
expect(
|
|
15
|
+
expect(misplacedFiles, `${MISPLACED_REMEDY} Swept from ${rootDir}.`).toStrictEqual([]);
|
|
14
16
|
});
|
|
15
17
|
});
|
|
16
18
|
}
|
|
17
|
-
export function reportTestFileConventions(
|
|
19
|
+
export function reportTestFileConventions(options = {}) {
|
|
20
|
+
assertNoRetiredOptions(options);
|
|
21
|
+
const { excludedBasenames, rootDir } = options;
|
|
18
22
|
const sweptRoot = rootDir ?? findMonorepoRoot();
|
|
19
|
-
const scanOptions = { ...(
|
|
23
|
+
const scanOptions = { ...(excludedBasenames !== undefined && { excludedBasenames }) };
|
|
20
24
|
return {
|
|
21
|
-
|
|
25
|
+
misplacedFiles: findMisplacedTestFiles(sweptRoot, scanOptions),
|
|
22
26
|
rootDir: sweptRoot,
|
|
23
|
-
|
|
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
|
+
}
|
package/dist/esm/tiers.d.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
33
|
+
collectTestFiles(path.join(dir, entry.name), relativePath, isInTestDir || entry.name === TEST_DIR, context);
|
|
34
34
|
}
|
|
35
|
-
else if (
|
|
36
|
-
context.
|
|
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, {
|
|
40
|
+
function walkTestFiles(rootDir, { excludedBasenames = [] }, misplaced) {
|
|
41
41
|
const context = {
|
|
42
|
-
|
|
42
|
+
foundPaths: [],
|
|
43
43
|
misplaced,
|
|
44
|
-
pruned: new Set([...TEST_COLLECTION_EXCLUDE, ...
|
|
44
|
+
pruned: new Set([...TEST_COLLECTION_EXCLUDE, ...excludedBasenames]),
|
|
45
45
|
};
|
|
46
46
|
collectTestFiles(rootDir, '', false, context);
|
|
47
|
-
return context.
|
|
47
|
+
return context.foundPaths.toSorted();
|
|
48
48
|
}
|
package/dist/esm/verbosity.d.ts
CHANGED
|
@@ -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
|
package/dist/esm/verbosity.js
CHANGED
|
@@ -9,18 +9,18 @@ export function isCommandVerbosity(value) {
|
|
|
9
9
|
return names.includes(value);
|
|
10
10
|
}
|
|
11
11
|
export function readVerbosityEnv(env) {
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
12
|
+
const rawValue = env[COMMAND_VERBOSITY_ENV_VAR];
|
|
13
|
+
if (rawValue === undefined || rawValue === '') {
|
|
14
14
|
return { ok: true };
|
|
15
15
|
}
|
|
16
|
-
if (!isCommandVerbosity(
|
|
17
|
-
return { ok: false, error: formatVerbosityRejection(COMMAND_VERBOSITY_ENV_VAR,
|
|
16
|
+
if (!isCommandVerbosity(rawValue)) {
|
|
17
|
+
return { ok: false, error: formatVerbosityRejection(COMMAND_VERBOSITY_ENV_VAR, rawValue) };
|
|
18
18
|
}
|
|
19
|
-
return { ok: true, verbosity:
|
|
19
|
+
return { ok: true, verbosity: rawValue };
|
|
20
20
|
}
|
|
21
21
|
export function resolveVerbosity(options) {
|
|
22
|
-
const { env, envVerbosity,
|
|
23
|
-
if (
|
|
22
|
+
const { env, envVerbosity, hasQuietFlag, output } = options;
|
|
23
|
+
if (hasQuietFlag)
|
|
24
24
|
return 'quiet';
|
|
25
25
|
if (envVerbosity !== undefined)
|
|
26
26
|
return envVerbosity;
|
package/dist/esm/verdict.d.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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.
|