@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/verdict.js
CHANGED
|
@@ -1,46 +1,63 @@
|
|
|
1
|
+
import { STATUS_GLYPHS } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { NMR_GLYPHS } from "./glyphs.js";
|
|
1
3
|
import { clampToBytes, TRUNCATION_MARK } from "./helpers/clampToBytes.js";
|
|
2
4
|
import { formatDuration, formatSaving } from "./helpers/duration.js";
|
|
3
|
-
export const
|
|
4
|
-
export function renderVerdict(verdict) {
|
|
5
|
-
const {
|
|
5
|
+
export const VERDICT_LINE_LIMIT_BYTES = 512;
|
|
6
|
+
export function renderVerdict(verdict, style) {
|
|
7
|
+
const { marker, phrase } = describeOutcome(verdict, style);
|
|
6
8
|
const detail = flattenDetail(verdict.detail ?? renderReplay(verdict) ?? '');
|
|
7
9
|
const detailClause = detail === '' ? '' : ` — ${detail}`;
|
|
8
|
-
return clampToBytes(`${
|
|
10
|
+
return clampToBytes(`${marker} ${verdict.scope}: ${verdict.command}: ${phrase}${detailClause}`, LINE_BUDGET_BYTES);
|
|
9
11
|
}
|
|
10
12
|
export function serializeVerdict(verdict) {
|
|
11
|
-
const
|
|
12
|
-
return isWithinBudget(
|
|
13
|
+
const renderedLine = JSON.stringify(verdict);
|
|
14
|
+
return isWithinBudget(renderedLine) ? renderedLine : renderWithinBudget(verdict);
|
|
13
15
|
}
|
|
14
|
-
export function writeVerdict(verdict, stream, format) {
|
|
15
|
-
const line = format === 'json' ? serializeVerdict(verdict) : renderVerdict(verdict);
|
|
16
|
+
export function writeVerdict(verdict, stream, format, style) {
|
|
17
|
+
const line = format === 'json' ? serializeVerdict(verdict) : renderVerdict(verdict, style);
|
|
16
18
|
stream.write(`${line}\n`);
|
|
17
19
|
}
|
|
18
20
|
const NEWLINE_BYTES = 1;
|
|
19
|
-
const LINE_BUDGET_BYTES =
|
|
21
|
+
const LINE_BUDGET_BYTES = VERDICT_LINE_LIMIT_BYTES - NEWLINE_BYTES;
|
|
20
22
|
const MIN_CUT_BYTES = Buffer.byteLength(TRUNCATION_MARK);
|
|
21
23
|
const REPLAY_MARKER = 'replayed:';
|
|
22
|
-
function describeOutcome(verdict) {
|
|
24
|
+
function describeOutcome(verdict, style) {
|
|
25
|
+
const statuses = STATUS_GLYPHS[style];
|
|
23
26
|
switch (verdict.outcome) {
|
|
24
27
|
case 'passed':
|
|
25
|
-
return {
|
|
28
|
+
return { marker: statuses.passed.text, phrase: `passed in ${formatDuration(verdict.durationMs)}` };
|
|
26
29
|
case 'failed':
|
|
27
30
|
return {
|
|
28
|
-
|
|
31
|
+
marker: statuses.failed.text,
|
|
29
32
|
phrase: `failed in ${formatDuration(verdict.durationMs)} (exit ${verdict.exitCode})`,
|
|
30
33
|
};
|
|
31
34
|
case 'recalled': {
|
|
32
35
|
const saving = formatSaving(verdict.savedMs);
|
|
33
36
|
const savingClause = saving === undefined ? '' : `, ${saving}`;
|
|
34
|
-
return { icon: '⏭️', phrase: `passed ${formatDuration(verdict.ageMs)} ago on this tree${savingClause}` };
|
|
35
|
-
}
|
|
36
|
-
case 'no-op':
|
|
37
37
|
return {
|
|
38
|
-
|
|
39
|
-
phrase: `
|
|
38
|
+
marker: statuses.skipped.text,
|
|
39
|
+
phrase: `passed ${formatDuration(verdict.ageMs)} ago on this tree${savingClause}`,
|
|
40
40
|
};
|
|
41
|
+
}
|
|
42
|
+
case 'no-op':
|
|
43
|
+
return { marker: NMR_GLYPHS[style].noop.text, phrase: `skipped, ${describeNoOpReason(verdict.reason)}` };
|
|
44
|
+
default: {
|
|
45
|
+
const unhandledVerdict = verdict;
|
|
46
|
+
throw new Error(`Unhandled verdict outcome: ${JSON.stringify(unhandledVerdict)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function describeNoOpReason(reason) {
|
|
51
|
+
switch (reason) {
|
|
52
|
+
case 'empty-override':
|
|
53
|
+
return 'the override is empty';
|
|
54
|
+
case 'empty-workspace':
|
|
55
|
+
return 'the workspace declares no package';
|
|
56
|
+
case 'noop-override':
|
|
57
|
+
return 'the override is a no-op';
|
|
41
58
|
default: {
|
|
42
|
-
const
|
|
43
|
-
throw new Error(`Unhandled
|
|
59
|
+
const unhandledReason = reason;
|
|
60
|
+
throw new Error(`Unhandled no-op reason: ${String(unhandledReason)}`);
|
|
44
61
|
}
|
|
45
62
|
}
|
|
46
63
|
}
|
|
@@ -58,14 +75,16 @@ function renderReplay(verdict) {
|
|
|
58
75
|
function flattenDetail(detail) {
|
|
59
76
|
return detail.replaceAll(/[\r\n]+/gu, ' ').trim();
|
|
60
77
|
}
|
|
61
|
-
function findCutTarget(
|
|
62
|
-
const
|
|
63
|
-
const share = Math.ceil(overrunBytes /
|
|
64
|
-
const target =
|
|
78
|
+
function findCutTarget(sizesBytes, longestSizeBytes, overrunBytes) {
|
|
79
|
+
const smallerSizesBytes = sizesBytes.filter((size) => size < longestSizeBytes && size > MIN_CUT_BYTES);
|
|
80
|
+
const share = Math.ceil(overrunBytes / sizesBytes.filter((size) => size === longestSizeBytes).length);
|
|
81
|
+
const target = smallerSizesBytes.length === 0
|
|
82
|
+
? longestSizeBytes - share
|
|
83
|
+
: Math.max(...smallerSizesBytes, longestSizeBytes - overrunBytes);
|
|
65
84
|
return Math.max(MIN_CUT_BYTES, target);
|
|
66
85
|
}
|
|
67
|
-
function isWithinBudget(
|
|
68
|
-
return Buffer.byteLength(
|
|
86
|
+
function isWithinBudget(renderedLine) {
|
|
87
|
+
return Buffer.byteLength(renderedLine) <= LINE_BUDGET_BYTES;
|
|
69
88
|
}
|
|
70
89
|
function readAttribution(verdict) {
|
|
71
90
|
if (verdict.outcome !== 'recalled' || verdict.replay === undefined) {
|
|
@@ -78,64 +97,64 @@ function readCuttableText(verdict) {
|
|
|
78
97
|
return [verdict.detail ?? '', ...excerpts];
|
|
79
98
|
}
|
|
80
99
|
function renderClamped(record) {
|
|
81
|
-
const
|
|
82
|
-
let
|
|
83
|
-
while (!isWithinBudget(
|
|
100
|
+
const clampedRecord = { ...record };
|
|
101
|
+
let renderedLine = JSON.stringify(clampedRecord);
|
|
102
|
+
while (!isWithinBudget(renderedLine)) {
|
|
84
103
|
const fields = ['command', 'scope'];
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
return
|
|
104
|
+
const sizesBytes = fields.map((field) => Buffer.byteLength(typeof clampedRecord[field] === 'string' ? clampedRecord[field] : ''));
|
|
105
|
+
const longestSizeBytes = Math.max(...sizesBytes);
|
|
106
|
+
if (longestSizeBytes <= MIN_CUT_BYTES) {
|
|
107
|
+
return renderedLine;
|
|
89
108
|
}
|
|
90
|
-
const target = findCutTarget(
|
|
91
|
-
const field = fields[
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
const target = findCutTarget(sizesBytes, longestSizeBytes, Buffer.byteLength(renderedLine) - LINE_BUDGET_BYTES);
|
|
110
|
+
const field = fields[sizesBytes.indexOf(longestSizeBytes)] ?? 'command';
|
|
111
|
+
clampedRecord[field] = clampToBytes(typeof clampedRecord[field] === 'string' ? clampedRecord[field] : '', target);
|
|
112
|
+
renderedLine = JSON.stringify(clampedRecord);
|
|
94
113
|
}
|
|
95
|
-
return
|
|
114
|
+
return renderedLine;
|
|
96
115
|
}
|
|
97
116
|
function renderWithinBudget(verdict) {
|
|
98
|
-
const
|
|
99
|
-
const shortenedLine = JSON.stringify(
|
|
117
|
+
const shortenedVerdict = shortenCuttableText(verdict);
|
|
118
|
+
const shortenedLine = JSON.stringify(shortenedVerdict);
|
|
100
119
|
if (isWithinBudget(shortenedLine)) {
|
|
101
120
|
return shortenedLine;
|
|
102
121
|
}
|
|
103
|
-
const record = { ...
|
|
122
|
+
const record = { ...shortenedVerdict };
|
|
104
123
|
delete record['detail'];
|
|
105
|
-
const attribution = readAttribution(
|
|
106
|
-
for (let
|
|
107
|
-
record['replay'] = attribution.slice(0,
|
|
108
|
-
const
|
|
109
|
-
if (isWithinBudget(
|
|
110
|
-
return
|
|
124
|
+
const attribution = readAttribution(shortenedVerdict);
|
|
125
|
+
for (let keptCount = attribution.length; keptCount > 0; keptCount--) {
|
|
126
|
+
record['replay'] = attribution.slice(0, keptCount);
|
|
127
|
+
const renderedLine = JSON.stringify(record);
|
|
128
|
+
if (isWithinBudget(renderedLine)) {
|
|
129
|
+
return renderedLine;
|
|
111
130
|
}
|
|
112
131
|
}
|
|
113
132
|
delete record['replay'];
|
|
114
|
-
const
|
|
115
|
-
return isWithinBudget(
|
|
133
|
+
const bareLine = JSON.stringify(record);
|
|
134
|
+
return isWithinBudget(bareLine) ? bareLine : renderClamped(record);
|
|
116
135
|
}
|
|
117
136
|
function shortenCuttableText(verdict) {
|
|
118
137
|
let candidate = verdict;
|
|
119
|
-
let
|
|
120
|
-
while (!isWithinBudget(
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
138
|
+
let renderedLine = JSON.stringify(candidate);
|
|
139
|
+
while (!isWithinBudget(renderedLine)) {
|
|
140
|
+
const shortenedVerdict = shortenLongestText(candidate, Buffer.byteLength(renderedLine) - LINE_BUDGET_BYTES);
|
|
141
|
+
if (shortenedVerdict === undefined) {
|
|
123
142
|
return candidate;
|
|
124
143
|
}
|
|
125
|
-
candidate =
|
|
126
|
-
|
|
144
|
+
candidate = shortenedVerdict;
|
|
145
|
+
renderedLine = JSON.stringify(candidate);
|
|
127
146
|
}
|
|
128
147
|
return candidate;
|
|
129
148
|
}
|
|
130
149
|
function shortenLongestText(verdict, overrunBytes) {
|
|
131
150
|
const texts = readCuttableText(verdict);
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
151
|
+
const sizesBytes = texts.map((text) => Buffer.byteLength(text));
|
|
152
|
+
const longestSizeBytes = Math.max(...sizesBytes);
|
|
153
|
+
if (longestSizeBytes <= MIN_CUT_BYTES) {
|
|
135
154
|
return undefined;
|
|
136
155
|
}
|
|
137
|
-
const index =
|
|
138
|
-
const target = findCutTarget(
|
|
156
|
+
const index = sizesBytes.indexOf(longestSizeBytes);
|
|
157
|
+
const target = findCutTarget(sizesBytes, longestSizeBytes, overrunBytes);
|
|
139
158
|
return writeCuttableText(verdict, index, clampToBytes(texts[index] ?? '', target));
|
|
140
159
|
}
|
|
141
160
|
function writeCuttableText(verdict, index, value) {
|
|
@@ -24,44 +24,44 @@ export function createSourceResolutionPlugin() {
|
|
|
24
24
|
export function resolveSourceTarget(specifier, importer, context) {
|
|
25
25
|
if (importer === undefined)
|
|
26
26
|
return undefined;
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
27
|
+
const parsedSpecifier = parseSpecifier(specifier);
|
|
28
|
+
if (!parsedSpecifier)
|
|
29
29
|
return undefined;
|
|
30
|
-
const packageDir = findPackageDir(
|
|
30
|
+
const packageDir = findPackageDir(parsedSpecifier.name, path.dirname(importer), context.packageDirs);
|
|
31
31
|
if (packageDir === undefined || isInsideNodeModules(packageDir))
|
|
32
32
|
return undefined;
|
|
33
33
|
const manifest = readManifest(packageDir, context.manifests);
|
|
34
34
|
if (!isObject(manifest))
|
|
35
35
|
return undefined;
|
|
36
|
-
const
|
|
37
|
-
if (!
|
|
36
|
+
const matchedSubpath = matchSubpath(manifest['exports'], parsedSpecifier.subpath);
|
|
37
|
+
if (!matchedSubpath)
|
|
38
38
|
return undefined;
|
|
39
39
|
const environmentCondition = context.environmentName === 'client' ? CLIENT_CONDITION : SERVER_CONDITION;
|
|
40
40
|
const conditions = new Set([environmentCondition, ...SHARED_CONDITIONS]);
|
|
41
|
-
const
|
|
42
|
-
if (!
|
|
41
|
+
const selectedTarget = selectTarget(matchedSubpath.value, conditions);
|
|
42
|
+
if (!selectedTarget?.didUseSource)
|
|
43
43
|
return undefined;
|
|
44
|
-
const wildcard =
|
|
45
|
-
const
|
|
46
|
-
if (!
|
|
44
|
+
const wildcard = matchedSubpath.wildcard;
|
|
45
|
+
const declaredTarget = wildcard === undefined ? selectedTarget.target : selectedTarget.target.replaceAll('*', () => wildcard);
|
|
46
|
+
if (!declaredTarget.startsWith('./'))
|
|
47
47
|
return undefined;
|
|
48
|
-
const declaredPath = path.resolve(packageDir,
|
|
48
|
+
const declaredPath = path.resolve(packageDir, declaredTarget);
|
|
49
49
|
const target = findTargetFile(declaredPath);
|
|
50
50
|
if (target === undefined) {
|
|
51
|
-
throw new UserError(formatMissingTarget(
|
|
51
|
+
throw new UserError(formatMissingTarget(parsedSpecifier.name, parsedSpecifier.subpath, declaredTarget, declaredPath));
|
|
52
52
|
}
|
|
53
|
-
return `${target}${
|
|
53
|
+
return `${target}${parsedSpecifier.postfix}`;
|
|
54
54
|
}
|
|
55
55
|
function findPackageDir(name, fromDir, cache) {
|
|
56
56
|
const key = `${fromDir}\0${name}`;
|
|
57
57
|
if (cache?.has(key))
|
|
58
58
|
return cache.get(key);
|
|
59
|
-
let
|
|
59
|
+
let foundDir = findSelfReferenceDir(name, fromDir);
|
|
60
60
|
let dir = fromDir;
|
|
61
|
-
while (
|
|
61
|
+
while (foundDir === undefined) {
|
|
62
62
|
const candidate = path.join(dir, NODE_MODULES, name);
|
|
63
63
|
if (existsSync(path.join(candidate, 'package.json'))) {
|
|
64
|
-
|
|
64
|
+
foundDir = realpathSync(candidate);
|
|
65
65
|
break;
|
|
66
66
|
}
|
|
67
67
|
const parent = path.dirname(dir);
|
|
@@ -69,16 +69,18 @@ function findPackageDir(name, fromDir, cache) {
|
|
|
69
69
|
break;
|
|
70
70
|
dir = parent;
|
|
71
71
|
}
|
|
72
|
-
cache?.set(key,
|
|
73
|
-
return
|
|
72
|
+
cache?.set(key, foundDir);
|
|
73
|
+
return foundDir;
|
|
74
74
|
}
|
|
75
75
|
function findSelfReferenceDir(name, fromDir) {
|
|
76
76
|
let dir = fromDir;
|
|
77
77
|
for (;;) {
|
|
78
78
|
const file = path.join(dir, 'package.json');
|
|
79
79
|
if (existsSync(file)) {
|
|
80
|
-
const
|
|
81
|
-
return isObject(
|
|
80
|
+
const parsedManifest = parsePackageJson(readFileSync(file, 'utf8'), file);
|
|
81
|
+
return isObject(parsedManifest) && parsedManifest['name'] === name && parsedManifest['exports'] !== undefined
|
|
82
|
+
? dir
|
|
83
|
+
: undefined;
|
|
82
84
|
}
|
|
83
85
|
const parent = path.dirname(dir);
|
|
84
86
|
if (parent === dir)
|
|
@@ -110,9 +112,9 @@ function findTargetFile(declaredPath) {
|
|
|
110
112
|
}
|
|
111
113
|
return undefined;
|
|
112
114
|
}
|
|
113
|
-
function formatMissingTarget(name, subpath,
|
|
115
|
+
function formatMissingTarget(name, subpath, declaredTarget, target) {
|
|
114
116
|
return [
|
|
115
|
-
`Package "${name}" declares a \`source\` export for "${subpath}" at "${
|
|
117
|
+
`Package "${name}" declares a \`source\` export for "${subpath}" at "${declaredTarget}", which reaches no file.`,
|
|
116
118
|
`Looked for ${target}, the same path under each of ${TARGET_EXTENSIONS.join(', ')}, and an index under it.`,
|
|
117
119
|
'Point the condition at a file that exists, or remove it so the package resolves through its other conditions.',
|
|
118
120
|
].join(' ');
|
|
@@ -129,10 +131,10 @@ function matchSubpath(exportsValue, subpath) {
|
|
|
129
131
|
if (!isObject(exportsValue) || Object.keys(exportsValue).every((key) => !key.startsWith('.'))) {
|
|
130
132
|
return subpath === '.' ? { value: exportsValue } : undefined;
|
|
131
133
|
}
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
return { value:
|
|
135
|
-
let
|
|
134
|
+
const exactValue = exportsValue[subpath];
|
|
135
|
+
if (exactValue !== undefined)
|
|
136
|
+
return { value: exactValue };
|
|
137
|
+
let bestMatch;
|
|
136
138
|
for (const [key, value] of Object.entries(exportsValue)) {
|
|
137
139
|
const star = key.indexOf('*');
|
|
138
140
|
if (star === -1)
|
|
@@ -143,21 +145,21 @@ function matchSubpath(exportsValue, subpath) {
|
|
|
143
145
|
continue;
|
|
144
146
|
if (subpath.length < base.length + suffix.length)
|
|
145
147
|
continue;
|
|
146
|
-
if (
|
|
148
|
+
if (bestMatch !== undefined && bestMatch.base.length >= base.length)
|
|
147
149
|
continue;
|
|
148
|
-
|
|
150
|
+
bestMatch = { base, value, wildcard: subpath.slice(base.length, subpath.length - suffix.length) };
|
|
149
151
|
}
|
|
150
|
-
return
|
|
152
|
+
return bestMatch && { value: bestMatch.value, wildcard: bestMatch.wildcard };
|
|
151
153
|
}
|
|
152
154
|
function parseSpecifier(specifier) {
|
|
153
155
|
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('\0'))
|
|
154
156
|
return undefined;
|
|
155
157
|
const suffixIndex = findSuffixIndex(specifier);
|
|
156
|
-
const
|
|
157
|
-
if (
|
|
158
|
+
const bareSpecifier = suffixIndex === -1 ? specifier : specifier.slice(0, suffixIndex);
|
|
159
|
+
if (bareSpecifier.includes(':') || isBuiltin(bareSpecifier))
|
|
158
160
|
return undefined;
|
|
159
|
-
const segments =
|
|
160
|
-
const nameLength =
|
|
161
|
+
const segments = bareSpecifier.split('/');
|
|
162
|
+
const nameLength = bareSpecifier.startsWith('@') ? 2 : 1;
|
|
161
163
|
if (segments.length < nameLength || segments.includes(''))
|
|
162
164
|
return undefined;
|
|
163
165
|
const rest = segments.slice(nameLength);
|
|
@@ -171,18 +173,18 @@ function readManifest(packageDir, cache) {
|
|
|
171
173
|
if (cache?.has(packageDir))
|
|
172
174
|
return cache.get(packageDir);
|
|
173
175
|
const file = path.join(packageDir, 'package.json');
|
|
174
|
-
const
|
|
175
|
-
cache?.set(packageDir,
|
|
176
|
-
return
|
|
176
|
+
const parsedManifest = parsePackageJson(readFileSync(file, 'utf8'), file);
|
|
177
|
+
cache?.set(packageDir, parsedManifest);
|
|
178
|
+
return parsedManifest;
|
|
177
179
|
}
|
|
178
180
|
function selectTarget(value, conditions) {
|
|
179
181
|
if (typeof value === 'string')
|
|
180
|
-
return { target: value,
|
|
182
|
+
return { target: value, didUseSource: false };
|
|
181
183
|
if (Array.isArray(value)) {
|
|
182
184
|
for (const entry of value) {
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
185
|
-
return
|
|
185
|
+
const selectedTarget = selectTarget(entry, conditions);
|
|
186
|
+
if (selectedTarget)
|
|
187
|
+
return selectedTarget;
|
|
186
188
|
}
|
|
187
189
|
return undefined;
|
|
188
190
|
}
|
|
@@ -190,16 +192,16 @@ function selectTarget(value, conditions) {
|
|
|
190
192
|
return undefined;
|
|
191
193
|
const source = value[SOURCE_CONDITION];
|
|
192
194
|
if (source !== undefined) {
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
195
|
-
return { target:
|
|
195
|
+
const selectedTarget = selectTarget(source, conditions);
|
|
196
|
+
if (selectedTarget)
|
|
197
|
+
return { target: selectedTarget.target, didUseSource: true };
|
|
196
198
|
}
|
|
197
199
|
for (const [key, entry] of Object.entries(value)) {
|
|
198
200
|
if (key === SOURCE_CONDITION || !conditions.has(key))
|
|
199
201
|
continue;
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
202
|
-
return
|
|
202
|
+
const selectedTarget = selectTarget(entry, conditions);
|
|
203
|
+
if (selectedTarget)
|
|
204
|
+
return selectedTarget;
|
|
203
205
|
}
|
|
204
206
|
return undefined;
|
|
205
207
|
}
|
package/dist/esm/vitest.d.ts
CHANGED
|
@@ -43,13 +43,13 @@ export interface VitestConfigOptions {
|
|
|
43
43
|
* Loads nmr's git-isolation setup file into every project, ahead of any the layers supply. Defaults to `true`.
|
|
44
44
|
* Turn it off only where a suite is meant to read the developer's own git configuration.
|
|
45
45
|
*/
|
|
46
|
-
|
|
46
|
+
shouldIsolateGit?: boolean;
|
|
47
47
|
/**
|
|
48
48
|
* Resolves a package whose files sit outside every `node_modules` through its `source` export condition, so a
|
|
49
49
|
* suite runs without a prior build. Defaults to `true`. A package declaring no such condition is unaffected,
|
|
50
50
|
* and so is every package under `node_modules`, which Node resolves and which the condition never reaches.
|
|
51
51
|
*/
|
|
52
|
-
|
|
52
|
+
shouldResolveFromSource?: boolean;
|
|
53
53
|
/**
|
|
54
54
|
* Resolves a specifier through the `paths` a `tsconfig.json` declares, so a test reaches an alias the way `tsc`
|
|
55
55
|
* does. Defaults to `false`, matching Vite, and requires Vite 8.
|
package/dist/esm/vitest.js
CHANGED
|
@@ -7,14 +7,34 @@ import { createSourceResolutionPlugin } from "./vitest-source-resolution.js";
|
|
|
7
7
|
import { getWorkspacePackageDirs } from "./workspace.js";
|
|
8
8
|
const [RESIDUAL_TIER, ...NAMED_TIERS] = TIER_NAMES;
|
|
9
9
|
const TIERED_PATTERNS = NAMED_TIERS.flatMap(buildTierPatterns);
|
|
10
|
-
const
|
|
10
|
+
const TIER_TIMEOUT_MS = 30_000;
|
|
11
11
|
const GIT_ISOLATION_SETUP_FILE = resolveGitIsolationSetupFile();
|
|
12
12
|
const CLIENT_CONDITIONS = ['module', 'browser', 'development|production'];
|
|
13
13
|
const SERVER_CONDITIONS = ['module', 'node', 'development|production'];
|
|
14
14
|
const COVERAGE_EXCLUDE = ['**/__{fixtures,mocks,tests}__/**', '**/index.ts', '**/*.d.ts'];
|
|
15
15
|
const PACKAGE_COVERAGE_INCLUDE = ['**/src/**/*.{ts,tsx}'];
|
|
16
|
+
const RECOGNIZED_OPTIONS = {
|
|
17
|
+
project: true,
|
|
18
|
+
root: true,
|
|
19
|
+
shouldIsolateGit: true,
|
|
20
|
+
shouldResolveFromSource: true,
|
|
21
|
+
testCollectionExclude: true,
|
|
22
|
+
tiers: true,
|
|
23
|
+
tsconfigPaths: true,
|
|
24
|
+
};
|
|
25
|
+
const RECOGNIZED_ROOT_OPTIONS = {
|
|
26
|
+
...RECOGNIZED_OPTIONS,
|
|
27
|
+
monorepoRoot: true,
|
|
28
|
+
};
|
|
29
|
+
const RECOGNIZED_OPTION_KEYS = Object.keys(RECOGNIZED_OPTIONS);
|
|
30
|
+
const RECOGNIZED_ROOT_OPTION_KEYS = Object.keys(RECOGNIZED_ROOT_OPTIONS);
|
|
31
|
+
const RETIRED_OPTION_KEYS = new Map([
|
|
32
|
+
['isolateGit', 'shouldIsolateGit'],
|
|
33
|
+
['resolveFromSource', 'shouldResolveFromSource'],
|
|
34
|
+
]);
|
|
16
35
|
const MISSING_MONOREPO_ROOT = 'defineRootVitestConfig requires `monorepoRoot`, an absolute path to the directory holding pnpm-workspace.yaml. Pass `import.meta.dirname` in the last options layer, from the root config.';
|
|
17
36
|
export function defineVitestConfig(...layers) {
|
|
37
|
+
assertOptionKeys(layers, RECOGNIZED_OPTION_KEYS);
|
|
18
38
|
return buildConfig(layers, { coverageInclude: PACKAGE_COVERAGE_INCLUDE });
|
|
19
39
|
}
|
|
20
40
|
export function defineRootVitestConfig(...layers) {
|
|
@@ -23,6 +43,7 @@ export function defineRootVitestConfig(...layers) {
|
|
|
23
43
|
if (typeof monorepoRoot !== 'string' || !path.isAbsolute(monorepoRoot)) {
|
|
24
44
|
throw new TypeError(MISSING_MONOREPO_ROOT);
|
|
25
45
|
}
|
|
46
|
+
assertOptionKeys(layers, RECOGNIZED_ROOT_OPTION_KEYS);
|
|
26
47
|
return buildConfig(layers, {
|
|
27
48
|
coverageInclude: [],
|
|
28
49
|
projectExclude: getWorkspaceExcludePatterns(monorepoRoot),
|
|
@@ -37,7 +58,7 @@ function buildConfig(declaredLayers, { coverageInclude, projectExclude = [], pro
|
|
|
37
58
|
...(resolveFlag(layers, 'tsconfigPaths', false) && { tsconfigPaths: true }),
|
|
38
59
|
};
|
|
39
60
|
const config = {
|
|
40
|
-
...(resolveFlag(layers, '
|
|
61
|
+
...(resolveFlag(layers, 'shouldResolveFromSource', true) && { plugins: [createSourceResolutionPlugin()] }),
|
|
41
62
|
resolve,
|
|
42
63
|
ssr: { resolve: { conditions: SERVER_CONDITIONS } },
|
|
43
64
|
test: {
|
|
@@ -53,12 +74,12 @@ function buildConfig(declaredLayers, { coverageInclude, projectExclude = [], pro
|
|
|
53
74
|
watch: false,
|
|
54
75
|
},
|
|
55
76
|
};
|
|
56
|
-
let
|
|
77
|
+
let mergedConfig = config;
|
|
57
78
|
for (const { root } of layers) {
|
|
58
79
|
if (root)
|
|
59
|
-
|
|
80
|
+
mergedConfig = mergeConfig(mergedConfig, root);
|
|
60
81
|
}
|
|
61
|
-
return
|
|
82
|
+
return mergedConfig;
|
|
62
83
|
}
|
|
63
84
|
function buildProjects(layers, extraExclude, projectRoot) {
|
|
64
85
|
const projectTiers = [
|
|
@@ -67,12 +88,12 @@ function buildProjects(layers, extraExclude, projectRoot) {
|
|
|
67
88
|
exclude: [],
|
|
68
89
|
include: buildTierPatterns(tier),
|
|
69
90
|
name: tier,
|
|
70
|
-
|
|
91
|
+
timeoutMs: TIER_TIMEOUT_MS,
|
|
71
92
|
})),
|
|
72
93
|
];
|
|
73
94
|
const collectionExclude = buildCollectionExclude(layers);
|
|
74
|
-
const
|
|
75
|
-
return projectTiers.map(({ exclude, include, name,
|
|
95
|
+
const shouldIsolateGit = resolveFlag(layers, 'shouldIsolateGit', true);
|
|
96
|
+
return projectTiers.map(({ exclude, include, name, timeoutMs }) => {
|
|
76
97
|
const project = {
|
|
77
98
|
extends: true,
|
|
78
99
|
...(projectRoot !== undefined && { root: projectRoot }),
|
|
@@ -80,51 +101,79 @@ function buildProjects(layers, extraExclude, projectRoot) {
|
|
|
80
101
|
exclude: [...collectionExclude, ...exclude, ...extraExclude],
|
|
81
102
|
include,
|
|
82
103
|
name,
|
|
83
|
-
...(
|
|
84
|
-
...(
|
|
104
|
+
...(shouldIsolateGit && { setupFiles: [GIT_ISOLATION_SETUP_FILE] }),
|
|
105
|
+
...(timeoutMs !== undefined && { hookTimeout: timeoutMs, testTimeout: timeoutMs }),
|
|
85
106
|
},
|
|
86
107
|
};
|
|
87
|
-
let
|
|
108
|
+
let mergedProject = project;
|
|
88
109
|
for (const layer of layers) {
|
|
89
|
-
|
|
110
|
+
mergedProject = applyLayer(mergedProject, layer, name);
|
|
90
111
|
}
|
|
91
|
-
return
|
|
112
|
+
return mergedProject;
|
|
92
113
|
});
|
|
93
114
|
}
|
|
94
115
|
function applyLayer(project, layer, tier) {
|
|
95
116
|
const withProject = layer.project ? mergeConfig(project, { test: layer.project }) : project;
|
|
96
|
-
const
|
|
97
|
-
return
|
|
117
|
+
const targetedOverrides = layer.tiers?.[tier];
|
|
118
|
+
return targetedOverrides ? mergeConfig(withProject, { test: targetedOverrides }) : withProject;
|
|
98
119
|
}
|
|
99
120
|
function assertKnownTiers(layers) {
|
|
100
|
-
const
|
|
121
|
+
const knownTiers = TIER_NAMES;
|
|
101
122
|
for (const { tiers } of layers) {
|
|
102
123
|
const declaredTiers = Object.keys(tiers ?? {});
|
|
103
124
|
for (const name of declaredTiers) {
|
|
104
|
-
if (!
|
|
125
|
+
if (!knownTiers.includes(name)) {
|
|
105
126
|
throw new TypeError(`Unknown tier "${name}" in \`tiers\`. Valid tiers: ${TIER_NAMES.join(', ')}.`);
|
|
106
127
|
}
|
|
107
128
|
}
|
|
108
129
|
}
|
|
109
130
|
}
|
|
131
|
+
function assertNoRetiredOptions(layer) {
|
|
132
|
+
for (const key of Object.keys(layer)) {
|
|
133
|
+
const replacement = RETIRED_OPTION_KEYS.get(key);
|
|
134
|
+
if (replacement !== undefined) {
|
|
135
|
+
throw new TypeError(`Invalid Vitest config: \`${key}\` was renamed to \`${replacement}\`.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function assertOptionKeys(layers, lastLayerKeys) {
|
|
140
|
+
const lastIndex = layers.length - 1;
|
|
141
|
+
layers.forEach((layer, index) => {
|
|
142
|
+
if (layer === undefined)
|
|
143
|
+
return;
|
|
144
|
+
assertNoRetiredOptions(layer);
|
|
145
|
+
const recognizedKeys = index === lastIndex ? lastLayerKeys : RECOGNIZED_OPTION_KEYS;
|
|
146
|
+
const unrecognizedKeys = Object.keys(layer).filter((key) => !recognizedKeys.includes(key));
|
|
147
|
+
if (unrecognizedKeys.length === 0)
|
|
148
|
+
return;
|
|
149
|
+
throw new TypeError(`Invalid Vitest config: unrecognized ${unrecognizedKeys.length === 1 ? 'option' : 'options'} ` +
|
|
150
|
+
`${formatOptionKeys(unrecognizedKeys)}. Recognized: ${formatOptionKeys(recognizedKeys)}.`);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
110
153
|
function buildCollectionExclude(layers) {
|
|
111
|
-
const
|
|
112
|
-
const globs = [...TEST_COLLECTION_EXCLUDE, ...
|
|
154
|
+
const declaredDirs = layers.flatMap((layer) => layer.testCollectionExclude ?? []);
|
|
155
|
+
const globs = [...TEST_COLLECTION_EXCLUDE, ...declaredDirs].map((dir) => `**/${dir}/**`);
|
|
113
156
|
return [...new Set([...defaultExclude, ...globs])];
|
|
114
157
|
}
|
|
158
|
+
function formatOptionKeys(keys) {
|
|
159
|
+
return keys
|
|
160
|
+
.toSorted()
|
|
161
|
+
.map((key) => `\`${key}\``)
|
|
162
|
+
.join(', ');
|
|
163
|
+
}
|
|
115
164
|
function getWorkspaceExcludePatterns(monorepoRoot) {
|
|
116
165
|
return getWorkspacePackageDirs(monorepoRoot)
|
|
117
166
|
.map((dir) => `${path.relative(monorepoRoot, dir).split(path.sep).join('/')}/**`)
|
|
118
167
|
.toSorted();
|
|
119
168
|
}
|
|
120
|
-
function resolveFlag(layers, name,
|
|
121
|
-
let
|
|
169
|
+
function resolveFlag(layers, name, isOnByDefault) {
|
|
170
|
+
let resolvedFlag = isOnByDefault;
|
|
122
171
|
for (const layer of layers) {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
|
|
172
|
+
const declaredFlag = layer[name];
|
|
173
|
+
if (declaredFlag !== undefined)
|
|
174
|
+
resolvedFlag = declaredFlag;
|
|
126
175
|
}
|
|
127
|
-
return
|
|
176
|
+
return resolvedFlag;
|
|
128
177
|
}
|
|
129
178
|
function resolveGitIsolationSetupFile() {
|
|
130
179
|
const thisFile = fileURLToPath(import.meta.url);
|
package/dist/esm/workspace.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
export type { EmptyWorkspaceCause, WorkspaceResolution } from '@williamthorsen/nmr-core/workspace';
|
|
2
|
+
export { isMonorepoRoot, readWorkspaceOverrides, readWorkspacePackageNames, resolveWorkspace, } from '@williamthorsen/nmr-core/workspace';
|
|
3
3
|
/**
|
|
4
4
|
* Finds the monorepo root by walking up from `startDir` to find `pnpm-workspace.yaml`.
|
|
5
5
|
* Throws if no workspace root is found.
|
|
@@ -13,19 +13,3 @@ export declare function findMonorepoRoot(startDir?: string): string;
|
|
|
13
13
|
* `monorepoRoot` holds no manifest at all — the caller named a directory that is not a monorepo root.
|
|
14
14
|
*/
|
|
15
15
|
export declare function getWorkspacePackageDirs(monorepoRoot: string): string[];
|
|
16
|
-
/**
|
|
17
|
-
* Reads the `overrides` block from the monorepo root's `pnpm-workspace.yaml`, the site pnpm reads an override
|
|
18
|
-
* from.
|
|
19
|
-
*
|
|
20
|
-
* Returns nothing when the manifest or the block is missing, and drops an entry whose value is not a string.
|
|
21
|
-
*/
|
|
22
|
-
export declare function readWorkspaceOverrides(monorepoRoot: string): Record<string, string> | undefined;
|
|
23
|
-
/**
|
|
24
|
-
* Reads the manifest `name` each of the given package directories declares, which is what a `-F` pattern
|
|
25
|
-
* matches.
|
|
26
|
-
*
|
|
27
|
-
* A directory whose manifest is missing, unparseable, or nameless contributes nothing. The sweep runs for a
|
|
28
|
-
* diagnostic's sake, so one malformed manifest elsewhere in the workspace must not replace the diagnostic the
|
|
29
|
-
* reader asked for with a failure of its own.
|
|
30
|
-
*/
|
|
31
|
-
export declare function readWorkspacePackageNames(packageDirs: readonly string[]): string[];
|