@williamthorsen/nmr 0.38.0 → 0.40.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 +212 -4
- package/.readyup/kits/default.js +124 -104
- package/.readyup/manifest.json +12 -7
- package/CHANGELOG.md +108 -0
- package/README.md +41 -47
- package/agents/README.md +8 -8
- package/agents/guidance/rulebooks/nmr.md +8 -8
- package/bin/ensure-prepublish-hooks.js +1 -1
- package/bin/nmr-clean.js +1 -1
- package/bin/nmr-compile.js +1 -1
- package/bin/nmr-fmt.js +1 -1
- package/bin/nmr-report-catalog.js +1 -1
- package/bin/nmr-report-overrides.js +1 -1
- package/bin/nmr-taze.js +1 -1
- package/bin/nmr.js +1 -1
- package/dist/esm/UserError.d.ts +1 -2
- package/dist/esm/check-cache.d.ts +61 -63
- 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 +37 -38
- package/dist/esm/commands/build-output.js +9 -9
- package/dist/esm/commands/build.d.ts +13 -14
- package/dist/esm/commands/build.js +67 -65
- package/dist/esm/commands/clean.d.ts +6 -6
- package/dist/esm/commands/clean.js +11 -11
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +7 -7
- package/dist/esm/commands/ensure-prepublish-hooks.js +41 -41
- package/dist/esm/commands/fmt.d.ts +8 -8
- package/dist/esm/commands/fmt.js +33 -33
- package/dist/esm/commands/report-catalog.d.ts +5 -7
- package/dist/esm/commands/report-catalog.js +10 -8
- package/dist/esm/commands/report-overrides.d.ts +0 -2
- package/dist/esm/commands/report-overrides.js +8 -8
- package/dist/esm/commands/taze.d.ts +9 -8
- package/dist/esm/config.d.ts +5 -5
- package/dist/esm/config.js +27 -27
- package/dist/esm/context.d.ts +1 -2
- package/dist/esm/context.js +8 -8
- package/dist/esm/default-scripts.d.ts +5 -6
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/git-ignored-paths.d.ts +9 -0
- package/dist/esm/git-ignored-paths.js +10 -0
- package/dist/esm/glyphs.d.ts +3 -3
- package/dist/esm/help.d.ts +4 -4
- package/dist/esm/help.js +21 -21
- package/dist/esm/helpers/clampToBytes.d.ts +4 -5
- package/dist/esm/helpers/clampToBytes.js +3 -3
- package/dist/esm/helpers/config-path.d.ts +0 -2
- package/dist/esm/helpers/createBoundedBuffer.d.ts +3 -3
- package/dist/esm/helpers/createBoundedBuffer.js +2 -2
- package/dist/esm/helpers/deriveExcerpt.d.ts +4 -5
- package/dist/esm/helpers/duration.d.ts +10 -11
- package/dist/esm/helpers/duration.js +7 -7
- package/dist/esm/helpers/filter-selection.d.ts +19 -19
- package/dist/esm/helpers/filter-selection.js +12 -12
- package/dist/esm/helpers/findClosestName.d.ts +1 -1
- package/dist/esm/helpers/findClosestName.js +15 -15
- package/dist/esm/helpers/hook-name.d.ts +2 -5
- package/dist/esm/helpers/package-json.d.ts +11 -10
- package/dist/esm/helpers/package-json.js +26 -25
- package/dist/esm/helpers/readStringValues.d.ts +2 -3
- package/dist/esm/helpers/reportClosing.d.ts +8 -9
- package/dist/esm/helpers/transcript.d.ts +15 -15
- package/dist/esm/helpers/transcript.js +14 -14
- package/dist/esm/helpers/type-guards.d.ts +1 -0
- package/dist/esm/output-style.d.ts +12 -13
- package/dist/esm/prettier.d.ts +2 -2
- package/dist/esm/prettier.js +5 -5
- package/dist/esm/recording.d.ts +25 -20
- package/dist/esm/recording.js +28 -21
- package/dist/esm/replay-assembly.d.ts +10 -10
- package/dist/esm/report-format.d.ts +15 -20
- package/dist/esm/report-format.js +7 -7
- package/dist/esm/reportCliFailure.d.ts +2 -3
- package/dist/esm/resolve-scripts.d.ts +4 -5
- package/dist/esm/resolveBinStyles.d.ts +4 -5
- package/dist/esm/resolveBinStyles.js +3 -3
- package/dist/esm/resolver.d.ts +16 -20
- package/dist/esm/resolver.js +15 -15
- package/dist/esm/runCli.d.ts +6 -6
- package/dist/esm/runCli.js +261 -195
- package/dist/esm/runner.d.ts +20 -20
- package/dist/esm/runner.js +18 -18
- package/dist/esm/steps.d.ts +51 -38
- package/dist/esm/steps.js +32 -27
- package/dist/esm/taze.d.ts +1 -1
- package/dist/esm/tests.d.ts +23 -18
- package/dist/esm/tests.js +21 -9
- package/dist/esm/tiers.d.ts +28 -24
- package/dist/esm/tiers.js +13 -9
- package/dist/esm/types.d.ts +5 -5
- package/dist/esm/verbosity.d.ts +23 -21
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +39 -32
- package/dist/esm/verdict.js +63 -50
- package/dist/esm/vitest-source-resolution.d.ts +11 -11
- package/dist/esm/vitest-source-resolution.js +49 -47
- package/dist/esm/vitest.d.ts +27 -26
- package/dist/esm/vitest.js +90 -33
- package/dist/esm/workspace.d.ts +6 -22
- package/dist/esm/workspace.js +9 -67
- package/docs/check-cache.md +57 -57
- package/docs/prettier.md +10 -10
- package/docs/reporting.md +48 -44
- package/docs/scripts.md +29 -29
- package/docs/upgrades.md +18 -18
- package/docs/utilities.md +13 -13
- package/docs/vitest.md +68 -66
- package/docs/workspace.md +27 -4
- package/package.json +6 -6
- package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
- package/dist/esm/helpers/workspace-patterns.js +0 -29
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
/**
|
|
3
|
-
* Builds the plugin that resolves a package's `source` export condition, for a package whose files
|
|
3
|
+
* Builds the plugin that resolves a package's `source` export condition, for a package whose files are outside
|
|
4
4
|
* every `node_modules`.
|
|
5
5
|
*
|
|
6
6
|
* The condition cannot be emitted as a `resolve.conditions` entry instead: Vitest turns the server environment's
|
|
7
|
-
* conditions into `--conditions` flags on the worker process, where Node applies them to every package it
|
|
7
|
+
* conditions into `--conditions` flags on the worker process, where Node applies them to every package that it
|
|
8
8
|
* resolves natively. Node refuses to strip types from a file under `node_modules`, so a dependency declaring a
|
|
9
9
|
* TypeScript `source` entry fails to load as soon as the condition reaches that far.
|
|
10
10
|
*
|
|
@@ -22,16 +22,16 @@ export interface SourceResolutionContext {
|
|
|
22
22
|
packageDirs?: Map<string, string | undefined>;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* Resolves a bare specifier to the file that its package's `source` condition names, or nothing
|
|
26
|
-
* does not reach it.
|
|
25
|
+
* Resolves a bare specifier to the file that its package's `source` condition names, or to nothing when the
|
|
26
|
+
* condition does not reach it.
|
|
27
27
|
*
|
|
28
|
-
* Answers a bare specifier naming a package whose real directory
|
|
29
|
-
*
|
|
30
|
-
* `node_modules` -- so Vite resolves the specifier through its own conditions. Throws
|
|
31
|
-
* file, because falling through to the build output is the staleness that resolving from source exists
|
|
32
|
-
* and nothing in a run reports it.
|
|
28
|
+
* Answers a bare specifier naming a package whose real directory is outside every `node_modules`. Returns nothing
|
|
29
|
+
* for every other specifier -- such as a relative or absolute path, a virtual module, a builtin, or a package under
|
|
30
|
+
* `node_modules` -- so Vite resolves the specifier through its own conditions. Throws when the condition does not
|
|
31
|
+
* reach any file, because falling through to the build output is the staleness that resolving from source exists
|
|
32
|
+
* to prevent, and nothing in a run reports it.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
* does,
|
|
34
|
+
* Because a query or hash suffix is split off before the `exports` lookup and re-appended to the resolved path, as
|
|
35
|
+
* Vite does, `pkg/icon.svg?raw` matches the `./icon.svg` entry and still reaches the plugin that reads the suffix.
|
|
36
36
|
*/
|
|
37
37
|
export declare function resolveSourceTarget(specifier: string, importer: string | undefined, context: SourceResolutionContext): string | undefined;
|
|
@@ -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,11 +112,11 @@ 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 does not reach any file.`,
|
|
116
118
|
`Looked for ${target}, the same path under each of ${TARGET_EXTENSIONS.join(', ')}, and an index under it.`,
|
|
117
|
-
'Point the condition at a file that exists, or remove it so the package resolves through its other conditions.',
|
|
119
|
+
'Point the condition at a file that exists, or remove it so that the package resolves through its other conditions.',
|
|
118
120
|
].join(' ');
|
|
119
121
|
}
|
|
120
122
|
function isFile(candidate) {
|
|
@@ -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
|
@@ -3,8 +3,8 @@ import type { InlineConfig, ProjectConfig } from 'vitest/node';
|
|
|
3
3
|
import { type TierName } from './tiers.js';
|
|
4
4
|
export type { TierName } from './tiers.js';
|
|
5
5
|
/**
|
|
6
|
-
* Test options Vitest honours only at the root of a `projects` config. Derived from Vitest's own types
|
|
7
|
-
*
|
|
6
|
+
* Test options that Vitest honours only at the root of a `projects` config. Derived from Vitest's own types, so an
|
|
7
|
+
* option that changes scope in a later release changes scope here too.
|
|
8
8
|
*/
|
|
9
9
|
export type RootTestOptions = Omit<InlineConfig, keyof ProjectConfig>;
|
|
10
10
|
/** Root-scoped overrides: Vite-level options plus the test options that survive at the root. */
|
|
@@ -16,7 +16,7 @@ export interface VitestConfigOptions {
|
|
|
16
16
|
* Merged into the root config, which every project inherits because each declares `extends: true`.
|
|
17
17
|
*
|
|
18
18
|
* `resolve` is per-environment. Vitest resolves a test's own imports through the server environment, so a
|
|
19
|
-
* condition meant for them goes under `ssr`; a top-level `resolve.conditions` entry
|
|
19
|
+
* condition meant for them goes under `ssr`; a top-level `resolve.conditions` entry applies to the client
|
|
20
20
|
* environment, which only browser-mode tests resolve through.
|
|
21
21
|
*/
|
|
22
22
|
root?: RootOverrides;
|
|
@@ -29,41 +29,42 @@ export interface VitestConfigOptions {
|
|
|
29
29
|
*/
|
|
30
30
|
tiers?: Partial<Record<TierName, ProjectConfig>>;
|
|
31
31
|
/**
|
|
32
|
-
* Directory basenames kept out of every project's collection, additive to the ones the shared config always
|
|
33
|
-
* prunes. Each name is matched at any depth, and every layer's entries are
|
|
34
|
-
* winning, matching the config's rule for arrays.
|
|
32
|
+
* Directory basenames kept out of every project's collection, additive to the ones that the shared config always
|
|
33
|
+
* prunes and to the paths that git ignores. Each name is matched at any depth, and every layer's entries are
|
|
34
|
+
* concatenated rather than the last winning, matching the config's rule for arrays.
|
|
35
35
|
*
|
|
36
36
|
* The same array declares the scope of nmr's exported test-file conventions check, so the sweep and the
|
|
37
37
|
* collection glob cannot drift. Excluding a directory from the sweep alone leaves a test file that runs and
|
|
38
|
-
* reports nothing; excluding it from collection alone leaves a report a consumer cannot act on. A repo wanting
|
|
38
|
+
* reports nothing; excluding it from collection alone leaves a report that a consumer cannot act on. A repo wanting
|
|
39
39
|
* a glob rather than a directory name has the `project` seam's own `exclude`.
|
|
40
40
|
*/
|
|
41
41
|
testCollectionExclude?: readonly string[];
|
|
42
42
|
/**
|
|
43
|
-
* Loads nmr's git-isolation setup file into every project, ahead of any the layers supply. Defaults to `true`.
|
|
44
|
-
* Turn it off only
|
|
43
|
+
* Loads nmr's git-isolation setup file into every project, ahead of any that the layers supply. Defaults to `true`.
|
|
44
|
+
* Turn it off only when a suite is meant to read the developer's own git configuration.
|
|
45
45
|
*/
|
|
46
|
-
|
|
46
|
+
shouldIsolateGit?: boolean;
|
|
47
47
|
/**
|
|
48
|
-
* Resolves a package whose files
|
|
49
|
-
* suite runs without a prior build. Defaults to `true`. A package
|
|
50
|
-
* and so is every package under `node_modules`, which Node resolves and which the condition never
|
|
48
|
+
* Resolves a package whose files are outside every `node_modules` through its `source` export condition, so a
|
|
49
|
+
* suite runs without a prior build. Defaults to `true`. A package that does not declare a `source` condition is
|
|
50
|
+
* unaffected, and so is every package under `node_modules`, which Node resolves and to which the condition never
|
|
51
|
+
* applies.
|
|
51
52
|
*/
|
|
52
|
-
|
|
53
|
+
shouldResolveFromSource?: boolean;
|
|
53
54
|
/**
|
|
54
|
-
* Resolves a specifier through the `paths` a `tsconfig.json` declares,
|
|
55
|
-
*
|
|
55
|
+
* Resolves a test's aliased specifier through the `paths` that a `tsconfig.json` declares, the way `tsc` does.
|
|
56
|
+
* Defaults to `false`, matching Vite, and requires Vite 8.
|
|
56
57
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
58
|
+
* Without it, an alias fails loudly as an unresolved import. With it, a repo that declares `paths` for `tsc` alone
|
|
59
|
+
* changes which module a specifier reaches, and nothing in the run reports the change. A repo that does not declare
|
|
60
|
+
* `paths` is unaffected either way.
|
|
60
61
|
*/
|
|
61
62
|
tsconfigPaths?: boolean;
|
|
62
63
|
}
|
|
63
64
|
export interface RootVitestConfigOptions extends VitestConfigOptions {
|
|
64
65
|
/**
|
|
65
|
-
* An absolute path to the monorepo root, which must
|
|
66
|
-
* directory by construction, so this is `import.meta.dirname`. Stated rather than searched for:
|
|
66
|
+
* An absolute path to the monorepo root, which must contain `pnpm-workspace.yaml`. A root config is in that
|
|
67
|
+
* directory by construction, so this is `import.meta.dirname`. Stated rather than searched for: Resolving it
|
|
67
68
|
* from the working directory would make the config describe whichever monorepo the run happened to start in.
|
|
68
69
|
*/
|
|
69
70
|
monorepoRoot: string;
|
|
@@ -75,18 +76,18 @@ type RootConfigLayers = [...(VitestConfigOptions | undefined)[], RootVitestConfi
|
|
|
75
76
|
* projects. Select them at run time with `--project`, which unions when repeated and accepts negation.
|
|
76
77
|
*
|
|
77
78
|
* Layers fold left to right, later winning and arrays composing, so a config file shares settings by passing a
|
|
78
|
-
* layer ahead of its own. Merging two of this function's *outputs* is not the way:
|
|
79
|
+
* layer ahead of its own. Merging two of this function's *outputs* is not the way: Both declare the same four
|
|
79
80
|
* project names, which Vitest rejects at startup.
|
|
80
81
|
*
|
|
81
|
-
* An `undefined` layer is skipped,
|
|
82
|
+
* An `undefined` layer is skipped, which lets `defineVitestConfig(shared, isCI ? ciLayer : undefined)` compose.
|
|
82
83
|
*/
|
|
83
84
|
export declare function defineVitestConfig(...layers: (VitestConfigOptions | undefined)[]): ViteUserConfig;
|
|
84
85
|
/**
|
|
85
86
|
* Builds the shared Vitest config for repo-root tests. Excludes every workspace package from all projects, and
|
|
86
|
-
*
|
|
87
|
+
* does not report any coverage of its own; packages cover their own sources.
|
|
87
88
|
*
|
|
88
|
-
* `monorepoRoot`
|
|
89
|
+
* `monorepoRoot` is set on the last layer, which is the config file's own: A shared layer describes settings, not
|
|
89
90
|
* which repo they belong to, and only the root config's `import.meta.dirname` states this one. The guard below
|
|
90
|
-
* still catches the JavaScript config that
|
|
91
|
+
* still catches the JavaScript config that type checking never covers.
|
|
91
92
|
*/
|
|
92
93
|
export declare function defineRootVitestConfig(...layers: RootConfigLayers): ViteUserConfig;
|
package/dist/esm/vitest.js
CHANGED
|
@@ -1,21 +1,43 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { defaultExclude, mergeConfig } from 'vitest/config';
|
|
4
|
+
import { listGitIgnoredPaths } from "./git-ignored-paths.js";
|
|
4
5
|
import { isObject } from "./helpers/type-guards.js";
|
|
5
6
|
import { ALL_TEST_PATTERNS, buildTierPatterns, TEST_COLLECTION_EXCLUDE, TIER_NAMES } from "./tiers.js";
|
|
6
7
|
import { createSourceResolutionPlugin } from "./vitest-source-resolution.js";
|
|
7
8
|
import { getWorkspacePackageDirs } from "./workspace.js";
|
|
8
9
|
const [RESIDUAL_TIER, ...NAMED_TIERS] = TIER_NAMES;
|
|
9
10
|
const TIERED_PATTERNS = NAMED_TIERS.flatMap(buildTierPatterns);
|
|
10
|
-
const
|
|
11
|
+
const TIER_TIMEOUT_MS = 30_000;
|
|
11
12
|
const GIT_ISOLATION_SETUP_FILE = resolveGitIsolationSetupFile();
|
|
12
13
|
const CLIENT_CONDITIONS = ['module', 'browser', 'development|production'];
|
|
13
14
|
const SERVER_CONDITIONS = ['module', 'node', 'development|production'];
|
|
14
15
|
const COVERAGE_EXCLUDE = ['**/__{fixtures,mocks,tests}__/**', '**/index.ts', '**/*.d.ts'];
|
|
15
16
|
const PACKAGE_COVERAGE_INCLUDE = ['**/src/**/*.{ts,tsx}'];
|
|
16
|
-
const
|
|
17
|
+
const GLOB_SYNTAX_PATTERN = /[!()*+?@[\]{}]/g;
|
|
18
|
+
const RECOGNIZED_OPTIONS = {
|
|
19
|
+
project: true,
|
|
20
|
+
root: true,
|
|
21
|
+
shouldIsolateGit: true,
|
|
22
|
+
shouldResolveFromSource: true,
|
|
23
|
+
testCollectionExclude: true,
|
|
24
|
+
tiers: true,
|
|
25
|
+
tsconfigPaths: true,
|
|
26
|
+
};
|
|
27
|
+
const RECOGNIZED_ROOT_OPTIONS = {
|
|
28
|
+
...RECOGNIZED_OPTIONS,
|
|
29
|
+
monorepoRoot: true,
|
|
30
|
+
};
|
|
31
|
+
const RECOGNIZED_OPTION_KEYS = Object.keys(RECOGNIZED_OPTIONS);
|
|
32
|
+
const RECOGNIZED_ROOT_OPTION_KEYS = Object.keys(RECOGNIZED_ROOT_OPTIONS);
|
|
33
|
+
const RETIRED_OPTION_KEYS = new Map([
|
|
34
|
+
['isolateGit', 'shouldIsolateGit'],
|
|
35
|
+
['resolveFromSource', 'shouldResolveFromSource'],
|
|
36
|
+
]);
|
|
37
|
+
const MISSING_MONOREPO_ROOT = 'defineRootVitestConfig requires `monorepoRoot`, an absolute path to the directory that contains pnpm-workspace.yaml. Pass `import.meta.dirname` in the last options layer, from the root config.';
|
|
17
38
|
export function defineVitestConfig(...layers) {
|
|
18
|
-
|
|
39
|
+
assertOptionKeys(layers, RECOGNIZED_OPTION_KEYS);
|
|
40
|
+
return buildConfig(layers, { coverageInclude: PACKAGE_COVERAGE_INCLUDE, ignoredPathsRoot: process.cwd() });
|
|
19
41
|
}
|
|
20
42
|
export function defineRootVitestConfig(...layers) {
|
|
21
43
|
const lastLayer = layers.at(-1);
|
|
@@ -23,13 +45,15 @@ export function defineRootVitestConfig(...layers) {
|
|
|
23
45
|
if (typeof monorepoRoot !== 'string' || !path.isAbsolute(monorepoRoot)) {
|
|
24
46
|
throw new TypeError(MISSING_MONOREPO_ROOT);
|
|
25
47
|
}
|
|
48
|
+
assertOptionKeys(layers, RECOGNIZED_ROOT_OPTION_KEYS);
|
|
26
49
|
return buildConfig(layers, {
|
|
27
50
|
coverageInclude: [],
|
|
51
|
+
ignoredPathsRoot: monorepoRoot,
|
|
28
52
|
projectExclude: getWorkspaceExcludePatterns(monorepoRoot),
|
|
29
53
|
projectRoot: monorepoRoot,
|
|
30
54
|
});
|
|
31
55
|
}
|
|
32
|
-
function buildConfig(declaredLayers, { coverageInclude, projectExclude = [], projectRoot }) {
|
|
56
|
+
function buildConfig(declaredLayers, { coverageInclude, ignoredPathsRoot, projectExclude = [], projectRoot }) {
|
|
33
57
|
const layers = declaredLayers.filter((layer) => layer !== undefined);
|
|
34
58
|
assertKnownTiers(layers);
|
|
35
59
|
const resolve = {
|
|
@@ -37,7 +61,7 @@ function buildConfig(declaredLayers, { coverageInclude, projectExclude = [], pro
|
|
|
37
61
|
...(resolveFlag(layers, 'tsconfigPaths', false) && { tsconfigPaths: true }),
|
|
38
62
|
};
|
|
39
63
|
const config = {
|
|
40
|
-
...(resolveFlag(layers, '
|
|
64
|
+
...(resolveFlag(layers, 'shouldResolveFromSource', true) && { plugins: [createSourceResolutionPlugin()] }),
|
|
41
65
|
resolve,
|
|
42
66
|
ssr: { resolve: { conditions: SERVER_CONDITIONS } },
|
|
43
67
|
test: {
|
|
@@ -48,31 +72,31 @@ function buildConfig(declaredLayers, { coverageInclude, projectExclude = [], pro
|
|
|
48
72
|
provider: 'v8',
|
|
49
73
|
},
|
|
50
74
|
passWithNoTests: true,
|
|
51
|
-
projects: buildProjects(layers, projectExclude, projectRoot),
|
|
75
|
+
projects: buildProjects(layers, projectExclude, projectRoot, ignoredPathsRoot),
|
|
52
76
|
silent: 'passed-only',
|
|
53
77
|
watch: false,
|
|
54
78
|
},
|
|
55
79
|
};
|
|
56
|
-
let
|
|
80
|
+
let mergedConfig = config;
|
|
57
81
|
for (const { root } of layers) {
|
|
58
82
|
if (root)
|
|
59
|
-
|
|
83
|
+
mergedConfig = mergeConfig(mergedConfig, root);
|
|
60
84
|
}
|
|
61
|
-
return
|
|
85
|
+
return mergedConfig;
|
|
62
86
|
}
|
|
63
|
-
function buildProjects(layers, extraExclude, projectRoot) {
|
|
87
|
+
function buildProjects(layers, extraExclude, projectRoot, ignoredPathsRoot) {
|
|
64
88
|
const projectTiers = [
|
|
65
89
|
{ exclude: TIERED_PATTERNS, include: ALL_TEST_PATTERNS, name: RESIDUAL_TIER },
|
|
66
90
|
...NAMED_TIERS.map((tier) => ({
|
|
67
91
|
exclude: [],
|
|
68
92
|
include: buildTierPatterns(tier),
|
|
69
93
|
name: tier,
|
|
70
|
-
|
|
94
|
+
timeoutMs: TIER_TIMEOUT_MS,
|
|
71
95
|
})),
|
|
72
96
|
];
|
|
73
|
-
const collectionExclude = buildCollectionExclude(layers);
|
|
74
|
-
const
|
|
75
|
-
return projectTiers.map(({ exclude, include, name,
|
|
97
|
+
const collectionExclude = buildCollectionExclude(layers, ignoredPathsRoot);
|
|
98
|
+
const shouldIsolateGit = resolveFlag(layers, 'shouldIsolateGit', true);
|
|
99
|
+
return projectTiers.map(({ exclude, include, name, timeoutMs }) => {
|
|
76
100
|
const project = {
|
|
77
101
|
extends: true,
|
|
78
102
|
...(projectRoot !== undefined && { root: projectRoot }),
|
|
@@ -80,51 +104,84 @@ function buildProjects(layers, extraExclude, projectRoot) {
|
|
|
80
104
|
exclude: [...collectionExclude, ...exclude, ...extraExclude],
|
|
81
105
|
include,
|
|
82
106
|
name,
|
|
83
|
-
...(
|
|
84
|
-
...(
|
|
107
|
+
...(shouldIsolateGit && { setupFiles: [GIT_ISOLATION_SETUP_FILE] }),
|
|
108
|
+
...(timeoutMs !== undefined && { hookTimeout: timeoutMs, testTimeout: timeoutMs }),
|
|
85
109
|
},
|
|
86
110
|
};
|
|
87
|
-
let
|
|
111
|
+
let mergedProject = project;
|
|
88
112
|
for (const layer of layers) {
|
|
89
|
-
|
|
113
|
+
mergedProject = applyLayer(mergedProject, layer, name);
|
|
90
114
|
}
|
|
91
|
-
return
|
|
115
|
+
return mergedProject;
|
|
92
116
|
});
|
|
93
117
|
}
|
|
94
118
|
function applyLayer(project, layer, tier) {
|
|
95
119
|
const withProject = layer.project ? mergeConfig(project, { test: layer.project }) : project;
|
|
96
|
-
const
|
|
97
|
-
return
|
|
120
|
+
const targetedOverrides = layer.tiers?.[tier];
|
|
121
|
+
return targetedOverrides ? mergeConfig(withProject, { test: targetedOverrides }) : withProject;
|
|
98
122
|
}
|
|
99
123
|
function assertKnownTiers(layers) {
|
|
100
|
-
const
|
|
124
|
+
const knownTiers = TIER_NAMES;
|
|
101
125
|
for (const { tiers } of layers) {
|
|
102
126
|
const declaredTiers = Object.keys(tiers ?? {});
|
|
103
127
|
for (const name of declaredTiers) {
|
|
104
|
-
if (!
|
|
128
|
+
if (!knownTiers.includes(name)) {
|
|
105
129
|
throw new TypeError(`Unknown tier "${name}" in \`tiers\`. Valid tiers: ${TIER_NAMES.join(', ')}.`);
|
|
106
130
|
}
|
|
107
131
|
}
|
|
108
132
|
}
|
|
109
133
|
}
|
|
110
|
-
function
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
function assertNoRetiredOptions(layer) {
|
|
135
|
+
for (const key of Object.keys(layer)) {
|
|
136
|
+
const replacement = RETIRED_OPTION_KEYS.get(key);
|
|
137
|
+
if (replacement !== undefined) {
|
|
138
|
+
throw new TypeError(`Invalid Vitest config: \`${key}\` was renamed to \`${replacement}\`.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function assertOptionKeys(layers, lastLayerKeys) {
|
|
143
|
+
const lastIndex = layers.length - 1;
|
|
144
|
+
layers.forEach((layer, index) => {
|
|
145
|
+
if (layer === undefined)
|
|
146
|
+
return;
|
|
147
|
+
assertNoRetiredOptions(layer);
|
|
148
|
+
const recognizedKeys = index === lastIndex ? lastLayerKeys : RECOGNIZED_OPTION_KEYS;
|
|
149
|
+
const unrecognizedKeys = Object.keys(layer).filter((key) => !recognizedKeys.includes(key));
|
|
150
|
+
if (unrecognizedKeys.length === 0)
|
|
151
|
+
return;
|
|
152
|
+
throw new TypeError(`Invalid Vitest config: unrecognized ${unrecognizedKeys.length === 1 ? 'option' : 'options'} ` +
|
|
153
|
+
`${formatOptionKeys(unrecognizedKeys)}. Recognized: ${formatOptionKeys(recognizedKeys)}.`);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function buildCollectionExclude(layers, ignoredPathsRoot) {
|
|
157
|
+
const declaredDirs = layers.flatMap((layer) => layer.testCollectionExclude ?? []);
|
|
158
|
+
const globs = [...TEST_COLLECTION_EXCLUDE, ...declaredDirs].map((dir) => `**/${dir}/**`);
|
|
159
|
+
const ignoredGlobs = listGitIgnoredPaths(ignoredPathsRoot).map(buildIgnoredPathGlob);
|
|
160
|
+
return [...new Set([...defaultExclude, ...globs, ...ignoredGlobs])];
|
|
161
|
+
}
|
|
162
|
+
function buildIgnoredPathGlob(ignoredPath) {
|
|
163
|
+
const escapedPath = ignoredPath.replaceAll(GLOB_SYNTAX_PATTERN, String.raw `\$&`);
|
|
164
|
+
return escapedPath.endsWith('/') ? `${escapedPath}**` : escapedPath;
|
|
165
|
+
}
|
|
166
|
+
function formatOptionKeys(keys) {
|
|
167
|
+
return keys
|
|
168
|
+
.toSorted()
|
|
169
|
+
.map((key) => `\`${key}\``)
|
|
170
|
+
.join(', ');
|
|
114
171
|
}
|
|
115
172
|
function getWorkspaceExcludePatterns(monorepoRoot) {
|
|
116
173
|
return getWorkspacePackageDirs(monorepoRoot)
|
|
117
174
|
.map((dir) => `${path.relative(monorepoRoot, dir).split(path.sep).join('/')}/**`)
|
|
118
175
|
.toSorted();
|
|
119
176
|
}
|
|
120
|
-
function resolveFlag(layers, name,
|
|
121
|
-
let
|
|
177
|
+
function resolveFlag(layers, name, isOnByDefault) {
|
|
178
|
+
let resolvedFlag = isOnByDefault;
|
|
122
179
|
for (const layer of layers) {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
|
|
180
|
+
const declaredFlag = layer[name];
|
|
181
|
+
if (declaredFlag !== undefined)
|
|
182
|
+
resolvedFlag = declaredFlag;
|
|
126
183
|
}
|
|
127
|
-
return
|
|
184
|
+
return resolvedFlag;
|
|
128
185
|
}
|
|
129
186
|
function resolveGitIsolationSetupFile() {
|
|
130
187
|
const thisFile = fileURLToPath(import.meta.url);
|
package/dist/esm/workspace.d.ts
CHANGED
|
@@ -1,31 +1,15 @@
|
|
|
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
|
-
* Throws if
|
|
5
|
+
* Throws if it does not find a workspace root in `startDir` or any of its parent directories.
|
|
6
6
|
*/
|
|
7
7
|
export declare function findMonorepoRoot(startDir?: string): string;
|
|
8
8
|
/**
|
|
9
9
|
* Reads the workspace patterns from `pnpm-workspace.yaml` and resolves them to absolute package
|
|
10
|
-
* directories, applying pnpm's pattern semantics
|
|
10
|
+
* directories, applying pnpm's pattern semantics, including `!`-prefixed exclusions.
|
|
11
11
|
*
|
|
12
|
-
* Returns an empty array when the manifest
|
|
13
|
-
* `monorepoRoot`
|
|
12
|
+
* Returns an empty array when the manifest does not declare a usable `packages` list, and throws when
|
|
13
|
+
* `monorepoRoot` does not contain a manifest at all.
|
|
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[];
|