@williamthorsen/nmr 0.37.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 +114 -0
- package/.readyup/kits/default.js +92 -87
- package/.readyup/manifest.json +8 -8
- package/CHANGELOG.md +89 -0
- package/README.md +37 -28
- package/agents/guidance/rulebooks/nmr.md +2 -2
- package/dist/esm/check-cache.d.ts +4 -3
- package/dist/esm/check-cache.js +28 -28
- package/dist/esm/cli-build.js +3 -0
- package/dist/esm/cli-clean.js +3 -1
- package/dist/esm/cli-ensure-prepublish-hooks.js +7 -5
- package/dist/esm/cli-report-catalog.js +2 -1
- package/dist/esm/cli-report-overrides.js +2 -1
- package/dist/esm/commands/build-output.d.ts +2 -2
- package/dist/esm/commands/build-output.js +9 -9
- package/dist/esm/commands/build.d.ts +6 -1
- package/dist/esm/commands/build.js +73 -72
- package/dist/esm/commands/clean.d.ts +3 -2
- package/dist/esm/commands/clean.js +25 -24
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +5 -4
- package/dist/esm/commands/ensure-prepublish-hooks.js +44 -42
- package/dist/esm/commands/fmt.js +33 -33
- package/dist/esm/commands/report-catalog.d.ts +2 -1
- package/dist/esm/commands/report-catalog.js +14 -11
- package/dist/esm/commands/report-overrides.d.ts +2 -1
- package/dist/esm/commands/report-overrides.js +11 -10
- package/dist/esm/config.js +39 -29
- package/dist/esm/context.js +8 -8
- package/dist/esm/default-scripts.d.ts +2 -2
- package/dist/esm/default-scripts.js +5 -5
- package/dist/esm/glyphs.d.ts +9 -0
- package/dist/esm/glyphs.js +9 -0
- package/dist/esm/help.d.ts +2 -2
- package/dist/esm/help.js +36 -30
- 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/output-style.d.ts +41 -0
- package/dist/esm/output-style.js +17 -0
- package/dist/esm/prettier.js +5 -5
- package/dist/esm/recording.d.ts +8 -5
- package/dist/esm/recording.js +27 -17
- package/dist/esm/report-format.d.ts +1 -1
- package/dist/esm/report-format.js +7 -7
- package/dist/esm/resolveBinStyles.d.ts +12 -0
- package/dist/esm/resolveBinStyles.js +16 -0
- package/dist/esm/resolver.d.ts +2 -2
- package/dist/esm/resolver.js +16 -16
- package/dist/esm/runCli.js +309 -221
- package/dist/esm/runner.d.ts +1 -1
- package/dist/esm/runner.js +18 -18
- package/dist/esm/steps.d.ts +17 -5
- package/dist/esm/steps.js +37 -28
- 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 +15 -4
- package/dist/esm/verdict.js +78 -59
- 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/check-cache.md +5 -5
- package/docs/reporting.md +20 -2
- package/docs/scripts.md +2 -2
- package/docs/vitest.md +6 -6
- package/docs/workspace.md +24 -1
- package/package.json +9 -10
- 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
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A structural step's argv leads with the file to spawn, so the runner has one to hand `spawn` without a shell.
|
|
6
6
|
*
|
|
7
|
-
* `
|
|
7
|
+
* `shouldDeclineArguments` travels from the composite element that composed the step to the one reader that acts on it,
|
|
8
8
|
* the binding of the invocation's trailing arguments. Every stage between the two -- the devBin substitution,
|
|
9
9
|
* the chain rendering, the replay assembly, the runner -- passes it through and asks nothing of it.
|
|
10
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 `
|
|
12
|
+
* than nmr's own. Like `shouldDeclineArguments`, it is set only where it holds and leaves the rendered chain unchanged.
|
|
13
13
|
*/
|
|
14
14
|
export type Step = {
|
|
15
15
|
kind: 'opaque';
|
|
@@ -17,7 +17,7 @@ export type Step = {
|
|
|
17
17
|
} | {
|
|
18
18
|
kind: 'structural';
|
|
19
19
|
argv: readonly [string, ...(readonly string[])];
|
|
20
|
-
|
|
20
|
+
shouldDeclineArguments?: boolean;
|
|
21
21
|
shouldWithholdInput?: boolean;
|
|
22
22
|
};
|
|
23
23
|
/** What a structural step asks of the nmr process it spawns. */
|
|
@@ -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
|
}
|
|
@@ -39,10 +44,17 @@ export type SelfReference = 'chained' | 'sole';
|
|
|
39
44
|
* The element tokenizes on whitespace, so it may carry nmr's own flags but cannot carry a space-bearing token.
|
|
40
45
|
* `-w` is prepended as its own token, so the child selects the root registry on its own.
|
|
41
46
|
*
|
|
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,9 +11,16 @@ 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 =
|
|
16
|
-
return {
|
|
14
|
+
export function composeNmrStep(element, isWorkspaceRoot, shouldDeclineArguments = false) {
|
|
15
|
+
const flags = isWorkspaceRoot ? ['-w'] : [];
|
|
16
|
+
return {
|
|
17
|
+
kind: 'structural',
|
|
18
|
+
argv: ['nmr', ...flags, ...tokenize(element)],
|
|
19
|
+
...(shouldDeclineArguments && { shouldDeclineArguments }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function dropRecursiveSteps(steps) {
|
|
23
|
+
return steps.filter((step) => readNmrStep(step)?.isRecursive !== true);
|
|
17
24
|
}
|
|
18
25
|
export function findNmrCrossing(steps) {
|
|
19
26
|
for (const step of steps) {
|
|
@@ -36,12 +43,12 @@ export function readNmrStep(step) {
|
|
|
36
43
|
export function readSelfReference(options) {
|
|
37
44
|
const { anchoredAtRoot, commandName, script } = options;
|
|
38
45
|
const segments = splitSegments(script).filter((segment) => segment.trim() !== '');
|
|
39
|
-
const
|
|
46
|
+
const doesReenter = segments.some((segment) => {
|
|
40
47
|
const tail = readNmrTail(segment);
|
|
41
48
|
const target = tail === undefined ? undefined : readNmrTarget(tail);
|
|
42
49
|
return target?.command === commandName && !target.isDelegate && (anchoredAtRoot || !target.isWorkspaceRoot);
|
|
43
50
|
});
|
|
44
|
-
if (!
|
|
51
|
+
if (!doesReenter) {
|
|
45
52
|
return undefined;
|
|
46
53
|
}
|
|
47
54
|
return segments.length === 1 ? 'sole' : 'chained';
|
|
@@ -86,6 +93,7 @@ function readNmrTail(segment) {
|
|
|
86
93
|
}
|
|
87
94
|
function readNmrTarget(tokens) {
|
|
88
95
|
let isDelegate = false;
|
|
96
|
+
let isRecursive = false;
|
|
89
97
|
let isWorkspaceRoot = false;
|
|
90
98
|
let index = 0;
|
|
91
99
|
while (index < tokens.length) {
|
|
@@ -99,6 +107,7 @@ function readNmrTarget(tokens) {
|
|
|
99
107
|
case '-R':
|
|
100
108
|
case '--recursive':
|
|
101
109
|
isDelegate = true;
|
|
110
|
+
isRecursive = true;
|
|
102
111
|
index += 1;
|
|
103
112
|
break;
|
|
104
113
|
case '-w':
|
|
@@ -108,7 +117,7 @@ function readNmrTarget(tokens) {
|
|
|
108
117
|
break;
|
|
109
118
|
default:
|
|
110
119
|
if (!token.startsWith('-')) {
|
|
111
|
-
return { command: token, isDelegate, isWorkspaceRoot };
|
|
120
|
+
return { command: token, isDelegate, isRecursive, isWorkspaceRoot };
|
|
112
121
|
}
|
|
113
122
|
index += 1;
|
|
114
123
|
}
|
|
@@ -120,49 +129,49 @@ function renderStep(step) {
|
|
|
120
129
|
}
|
|
121
130
|
function splitSegments(command) {
|
|
122
131
|
const segments = [];
|
|
123
|
-
let
|
|
132
|
+
let currentSegment = '';
|
|
124
133
|
let quote;
|
|
125
134
|
let index = 0;
|
|
126
135
|
while (index < command.length) {
|
|
127
136
|
const char = command[index] ?? '';
|
|
128
137
|
if (quote !== undefined) {
|
|
129
138
|
if (char === '\\' && quote === '"') {
|
|
130
|
-
|
|
139
|
+
currentSegment += char + (command[index + 1] ?? '');
|
|
131
140
|
index += 2;
|
|
132
141
|
continue;
|
|
133
142
|
}
|
|
134
|
-
|
|
143
|
+
currentSegment += char;
|
|
135
144
|
if (char === quote)
|
|
136
145
|
quote = undefined;
|
|
137
146
|
index += 1;
|
|
138
147
|
continue;
|
|
139
148
|
}
|
|
140
149
|
if (char === '\\') {
|
|
141
|
-
|
|
150
|
+
currentSegment += char + (command[index + 1] ?? '');
|
|
142
151
|
index += 2;
|
|
143
152
|
continue;
|
|
144
153
|
}
|
|
145
154
|
if (QUOTES.has(char)) {
|
|
146
155
|
quote = char;
|
|
147
|
-
|
|
156
|
+
currentSegment += char;
|
|
148
157
|
index += 1;
|
|
149
158
|
continue;
|
|
150
159
|
}
|
|
151
160
|
if (SEGMENT_SEPARATORS.has(char)) {
|
|
152
|
-
if (isRedirectionOperator(char,
|
|
153
|
-
|
|
161
|
+
if (isRedirectionOperator(char, currentSegment, command[index + 1])) {
|
|
162
|
+
currentSegment += char;
|
|
154
163
|
index += 1;
|
|
155
164
|
continue;
|
|
156
165
|
}
|
|
157
166
|
index += command[index + 1] === char ? 2 : 1;
|
|
158
|
-
segments.push(
|
|
159
|
-
|
|
167
|
+
segments.push(currentSegment);
|
|
168
|
+
currentSegment = '';
|
|
160
169
|
continue;
|
|
161
170
|
}
|
|
162
|
-
|
|
171
|
+
currentSegment += char;
|
|
163
172
|
index += 1;
|
|
164
173
|
}
|
|
165
|
-
segments.push(
|
|
174
|
+
segments.push(currentSegment);
|
|
166
175
|
return segments;
|
|
167
176
|
}
|
|
168
177
|
function tokenize(element) {
|
|
@@ -170,47 +179,47 @@ function tokenize(element) {
|
|
|
170
179
|
}
|
|
171
180
|
function tokenizeSegment(segment) {
|
|
172
181
|
const tokens = [];
|
|
173
|
-
let
|
|
182
|
+
let currentToken = '';
|
|
174
183
|
let quote;
|
|
175
184
|
let index = 0;
|
|
176
185
|
while (index < segment.length) {
|
|
177
186
|
const char = segment[index] ?? '';
|
|
178
187
|
if (quote !== undefined) {
|
|
179
188
|
if (char === '\\' && quote === '"') {
|
|
180
|
-
|
|
189
|
+
currentToken += char + (segment[index + 1] ?? '');
|
|
181
190
|
index += 2;
|
|
182
191
|
continue;
|
|
183
192
|
}
|
|
184
|
-
|
|
193
|
+
currentToken += char;
|
|
185
194
|
if (char === quote)
|
|
186
195
|
quote = undefined;
|
|
187
196
|
index += 1;
|
|
188
197
|
continue;
|
|
189
198
|
}
|
|
190
199
|
if (char === '\\') {
|
|
191
|
-
|
|
200
|
+
currentToken += char + (segment[index + 1] ?? '');
|
|
192
201
|
index += 2;
|
|
193
202
|
continue;
|
|
194
203
|
}
|
|
195
204
|
if (QUOTES.has(char)) {
|
|
196
205
|
quote = char;
|
|
197
|
-
|
|
206
|
+
currentToken += char;
|
|
198
207
|
index += 1;
|
|
199
208
|
continue;
|
|
200
209
|
}
|
|
201
210
|
if (TOKEN_SEPARATORS.has(char)) {
|
|
202
|
-
if (
|
|
203
|
-
tokens.push(
|
|
211
|
+
if (currentToken !== '') {
|
|
212
|
+
tokens.push(currentToken);
|
|
204
213
|
}
|
|
205
|
-
|
|
214
|
+
currentToken = '';
|
|
206
215
|
index += 1;
|
|
207
216
|
continue;
|
|
208
217
|
}
|
|
209
|
-
|
|
218
|
+
currentToken += char;
|
|
210
219
|
index += 1;
|
|
211
220
|
}
|
|
212
|
-
if (
|
|
213
|
-
tokens.push(
|
|
221
|
+
if (currentToken !== '') {
|
|
222
|
+
tokens.push(currentToken);
|
|
214
223
|
}
|
|
215
224
|
return tokens;
|
|
216
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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Writable } from 'node:stream';
|
|
2
|
+
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
2
3
|
import type { ReplayLine } from './check-cache.js';
|
|
3
4
|
import type { ReportFormat } from './report-format.js';
|
|
4
5
|
/**
|
|
@@ -10,7 +11,14 @@ import type { ReportFormat } from './report-format.js';
|
|
|
10
11
|
* truncation point from one machine to the next. The ceiling governs pipes: a terminal offers no such
|
|
11
12
|
* guarantee at any size.
|
|
12
13
|
*/
|
|
13
|
-
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';
|
|
14
22
|
/**
|
|
15
23
|
* What a command nmr ran came to, holding the facts a reporting line is rendered from rather than the line
|
|
16
24
|
* itself, so a machine-readable rendering spends the same record a human-readable one does.
|
|
@@ -46,7 +54,7 @@ export type VerdictOutcome = {
|
|
|
46
54
|
replay?: ReplayLine[];
|
|
47
55
|
} | {
|
|
48
56
|
outcome: 'no-op';
|
|
49
|
-
reason:
|
|
57
|
+
reason: NoOpReason;
|
|
50
58
|
});
|
|
51
59
|
/**
|
|
52
60
|
* Renders a verdict as the line nmr reports it on, without the newline that terminates it.
|
|
@@ -54,8 +62,11 @@ export type VerdictOutcome = {
|
|
|
54
62
|
* The line ends without terminal punctuation and reserves its tail for `detail`, so a later change appends to
|
|
55
63
|
* the grammar rather than rewriting it. A detail carrying nothing but line breaks takes its whole clause with
|
|
56
64
|
* it, rather than leaving a separator pointing at nothing.
|
|
65
|
+
*
|
|
66
|
+
* The marker takes no padding column: every plain marker a verdict reaches for is four characters, so the
|
|
67
|
+
* lines align by construction. `formatStatusLine` pads to a column that `BLOCK` widens, and no verdict is one.
|
|
57
68
|
*/
|
|
58
|
-
export declare function renderVerdict(verdict: Verdict): string;
|
|
69
|
+
export declare function renderVerdict(verdict: Verdict, style: OutputStyle): string;
|
|
59
70
|
/**
|
|
60
71
|
* Renders a verdict as the JSON object a machine consumer reads, without the newline that terminates it.
|
|
61
72
|
*
|
|
@@ -71,4 +82,4 @@ export declare function serializeVerdict(verdict: Verdict): string;
|
|
|
71
82
|
* share one descriptor. Both renderings spend the one record, so neither can come to report what the other
|
|
72
83
|
* does not.
|
|
73
84
|
*/
|
|
74
|
-
export declare function writeVerdict(verdict: Verdict, stream: Writable, format: ReportFormat): void;
|
|
85
|
+
export declare function writeVerdict(verdict: Verdict, stream: Writable, format: ReportFormat, style: OutputStyle): void;
|