@williamthorsen/nmr 0.34.0 → 0.35.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/.readyup/kits/default.js +92 -19
- package/.readyup/manifest.json +11 -8
- package/README.md +83 -17
- package/agents/guidance/rulebooks/nmr.md +1 -1
- package/dist/esm/UserError.d.ts +6 -0
- package/dist/esm/check-cache.d.ts +145 -0
- package/dist/esm/commands/build-output.d.ts +64 -0
- package/dist/esm/commands/build.d.ts +29 -1
- package/dist/esm/commands/build.js +25 -5
- package/dist/esm/commands/clean.d.ts +15 -0
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +11 -0
- package/dist/esm/commands/fmt.d.ts +32 -0
- package/dist/esm/commands/report-catalog.d.ts +10 -0
- package/dist/esm/commands/report-overrides.d.ts +6 -0
- package/dist/esm/commands/taze.d.ts +18 -0
- package/dist/esm/config.d.ts +25 -0
- package/dist/esm/context.d.ts +11 -0
- package/dist/esm/default-scripts.d.ts +15 -0
- package/dist/esm/defineConfig.d.ts +9 -0
- package/dist/esm/help.d.ts +13 -0
- package/dist/esm/helpers/clampToBytes.d.ts +12 -0
- package/dist/esm/helpers/createBoundedBuffer.d.ts +7 -0
- package/dist/esm/helpers/deriveExcerpt.d.ts +10 -0
- package/dist/esm/helpers/duration.d.ts +15 -0
- package/dist/esm/helpers/hook-name.d.ts +8 -0
- package/dist/esm/helpers/package-json.d.ts +25 -0
- package/dist/esm/helpers/readStringValues.d.ts +7 -0
- package/dist/esm/helpers/reportClosing.d.ts +15 -0
- package/dist/esm/helpers/transcript.d.ts +25 -0
- package/dist/esm/helpers/type-guards.d.ts +1 -0
- package/dist/esm/helpers/workspace-patterns.d.ts +7 -0
- package/dist/esm/prettier.d.ts +8 -0
- package/dist/esm/recording.d.ts +39 -1
- package/dist/esm/replay-assembly.d.ts +18 -2
- package/dist/esm/report-format.d.ts +33 -0
- package/dist/esm/reportCliFailure.d.ts +8 -0
- package/dist/esm/resolve-scripts.d.ts +10 -1
- package/dist/esm/resolver.d.ts +67 -2
- package/dist/esm/resolver.js +1 -1
- package/dist/esm/runCli.d.ts +17 -0
- package/dist/esm/runCli.js +1 -1
- package/dist/esm/runner.d.ts +52 -0
- package/dist/esm/scripts.d.ts +1 -0
- package/dist/esm/steps.d.ts +71 -0
- package/dist/esm/taze.d.ts +16 -0
- package/dist/esm/tests.d.ts +40 -0
- package/dist/esm/tests.js +25 -0
- package/dist/esm/tiers.d.ts +56 -1
- package/dist/esm/tiers.js +22 -9
- package/dist/esm/types.d.ts +27 -0
- package/dist/esm/verbosity.d.ts +37 -0
- package/dist/esm/verdict.d.ts +45 -0
- package/dist/esm/vitest.d.ts +72 -2
- package/dist/esm/vitest.js +17 -9
- package/dist/esm/workspace.d.ts +18 -0
- package/package.json +18 -10
package/.readyup/kits/default.js
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
|
-
/** @noformat
|
|
1
|
+
/** @noformat -- @generated. Do not edit. Compiled by rdy. */
|
|
2
2
|
/* eslint-disable */
|
|
3
|
-
export const __readyupVersion = "0.
|
|
3
|
+
export const __readyupVersion = "0.35.0";
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
// .readyup/kits/default.ts
|
|
7
7
|
import { existsSync, globSync, readdirSync as readdirSync2 } from "node:fs";
|
|
8
|
-
import { basename, join, sep } from "node:path";
|
|
8
|
+
import { basename, dirname, join, sep } from "node:path";
|
|
9
|
+
|
|
10
|
+
// ../../node_modules/.pnpm/@williamthorsen+toolbelt.errors@0.6.3/node_modules/@williamthorsen/toolbelt.errors/dist/esm/4-release/isError.js
|
|
11
|
+
function isError(error) {
|
|
12
|
+
return error instanceof Error || Object.prototype.toString.call(error) === "[object Error]";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ../../node_modules/.pnpm/@williamthorsen+toolbelt.errors@0.6.3/node_modules/@williamthorsen/toolbelt.errors/dist/esm/4-release/describeError.js
|
|
16
|
+
function describeError(error) {
|
|
17
|
+
try {
|
|
18
|
+
if (isError(error) && typeof error.message === "string" && error.message !== "") {
|
|
19
|
+
return error.message;
|
|
20
|
+
}
|
|
21
|
+
return String(error);
|
|
22
|
+
} catch {
|
|
23
|
+
return "[unstringifiable value]";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// .readyup/kits/default.ts
|
|
9
28
|
import { defineRdyKit } from "readyup";
|
|
10
29
|
import {
|
|
30
|
+
discoverWorkspaces,
|
|
11
31
|
fileContains,
|
|
12
32
|
fileExists,
|
|
13
33
|
hasDevDependency,
|
|
@@ -108,10 +128,8 @@ var TEST_EXTENSIONS = "{ts,tsx}";
|
|
|
108
128
|
var TEST_GLOB_PREFIX = `**/${TEST_DIR}/**`;
|
|
109
129
|
var TEST_FILE_PATTERN = /\.test\.tsx?$/;
|
|
110
130
|
var ALL_TEST_PATTERNS = [`${TEST_GLOB_PREFIX}/*.test.${TEST_EXTENSIONS}`];
|
|
111
|
-
function findTestFiles(rootDir) {
|
|
112
|
-
|
|
113
|
-
collectTestFiles(rootDir, "", false, found);
|
|
114
|
-
return found.toSorted();
|
|
131
|
+
function findTestFiles(rootDir, options = {}) {
|
|
132
|
+
return walkTestFiles(rootDir, options, false);
|
|
115
133
|
}
|
|
116
134
|
function hasTierInfix(filePath) {
|
|
117
135
|
const tiers = TIER_NAMES;
|
|
@@ -119,18 +137,27 @@ function hasTierInfix(filePath) {
|
|
|
119
137
|
}
|
|
120
138
|
var TEST_COLLECTION_EXCLUDE = [".git", "coverage", "dist", "node_modules"];
|
|
121
139
|
var TIER_NAMES = ["unit", "tool", "localhost", "remote"];
|
|
122
|
-
function collectTestFiles(dir, relativeDir, inTestDir,
|
|
140
|
+
function collectTestFiles(dir, relativeDir, inTestDir, context) {
|
|
123
141
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
124
142
|
for (const entry of entries) {
|
|
125
143
|
const relativePath = relativeDir === "" ? entry.name : `${relativeDir}/${entry.name}`;
|
|
126
144
|
if (entry.isDirectory()) {
|
|
127
|
-
if (
|
|
128
|
-
collectTestFiles(path.join(dir, entry.name), relativePath, inTestDir || entry.name === TEST_DIR,
|
|
129
|
-
} else if (inTestDir && TEST_FILE_PATTERN.test(entry.name)) {
|
|
130
|
-
found.push(relativePath);
|
|
145
|
+
if (context.pruned.has(entry.name)) continue;
|
|
146
|
+
collectTestFiles(path.join(dir, entry.name), relativePath, inTestDir || entry.name === TEST_DIR, context);
|
|
147
|
+
} else if (inTestDir !== context.misplaced && TEST_FILE_PATTERN.test(entry.name)) {
|
|
148
|
+
context.found.push(relativePath);
|
|
131
149
|
}
|
|
132
150
|
}
|
|
133
151
|
}
|
|
152
|
+
function walkTestFiles(rootDir, { exclude = [] }, misplaced) {
|
|
153
|
+
const context = {
|
|
154
|
+
found: [],
|
|
155
|
+
misplaced,
|
|
156
|
+
pruned: /* @__PURE__ */ new Set([...TEST_COLLECTION_EXCLUDE, ...exclude])
|
|
157
|
+
};
|
|
158
|
+
collectTestFiles(rootDir, "", false, context);
|
|
159
|
+
return context.found.toSorted();
|
|
160
|
+
}
|
|
134
161
|
|
|
135
162
|
// .readyup/kits/default.ts
|
|
136
163
|
var default_default = defineRdyKit({
|
|
@@ -240,10 +267,10 @@ var default_default = defineRdyKit({
|
|
|
240
267
|
fix: "Delete every vitest.standalone.config.* and vitest.integration.config.*. nmr's test scripts select Vitest projects instead of naming config files"
|
|
241
268
|
},
|
|
242
269
|
{
|
|
243
|
-
name: "vitest.config
|
|
270
|
+
name: "every vitest.config builds on @williamthorsen/nmr/vitest",
|
|
244
271
|
severity: "error",
|
|
245
272
|
check: () => vitestConfigBuildsOnSharedConfig(),
|
|
246
|
-
fix: "Replace
|
|
273
|
+
fix: "Replace each listed config with: import { defineVitestConfig } from '@williamthorsen/nmr/vitest'; export default defineVitestConfig(); -- a config that does not call the factory declares no projects, so every tier-selecting test command fails against it. Pass your own settings to the factory as layers to keep them"
|
|
247
274
|
},
|
|
248
275
|
{
|
|
249
276
|
name: "vitest.root.config.ts builds on @williamthorsen/nmr/vitest",
|
|
@@ -251,6 +278,12 @@ var default_default = defineRdyKit({
|
|
|
251
278
|
check: () => vitestRootConfigBuildsOnSharedConfig(),
|
|
252
279
|
fix: "Replace vitest.root.config.ts with: import { defineRootVitestConfig } from '@williamthorsen/nmr/vitest'; export default defineRootVitestConfig({ monorepoRoot: import.meta.dirname });"
|
|
253
280
|
},
|
|
281
|
+
{
|
|
282
|
+
name: "every workspace with a Vite config has a Vitest config",
|
|
283
|
+
severity: "error",
|
|
284
|
+
check: () => everyViteConfigHasVitestConfig(),
|
|
285
|
+
fix: "Add a vitest.config.ts calling defineVitestConfig() from @williamthorsen/nmr/vitest beside each listed vite.config -- Vitest stops its config search at the first directory holding either name, so the Vite config otherwise wins and the projects model is never reached"
|
|
286
|
+
},
|
|
254
287
|
{
|
|
255
288
|
name: "every test file names its isolation tier",
|
|
256
289
|
severity: "error",
|
|
@@ -313,6 +346,8 @@ var default_default = defineRdyKit({
|
|
|
313
346
|
});
|
|
314
347
|
var SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([".git", "coverage", "dist", "node_modules"]);
|
|
315
348
|
var CONFIG_EXTENSIONS = "{ts,mts,cts,js,mjs,cjs}";
|
|
349
|
+
var VITE_CONFIG_PATTERN = `vite.config.${CONFIG_EXTENSIONS}`;
|
|
350
|
+
var VITEST_CONFIG_PATTERN = `vitest.config.${CONFIG_EXTENSIONS}`;
|
|
316
351
|
var SHARED_VITEST_MODULE = "@williamthorsen/nmr/vitest";
|
|
317
352
|
var SHARED_PRETTIER_MODULE = "@williamthorsen/nmr/prettier";
|
|
318
353
|
var INERT_PRETTIER_CONFIGS = [".prettierrc", ".prettierrc.{json,json5,yaml,yml,toml}"];
|
|
@@ -403,20 +438,41 @@ function hasPrettierConfigKey(cwd) {
|
|
|
403
438
|
return false;
|
|
404
439
|
}
|
|
405
440
|
}
|
|
441
|
+
function discoverMemberWorkspaces() {
|
|
442
|
+
try {
|
|
443
|
+
return { ok: true, workspaces: discoverWorkspaces({ filter: (workspace) => !workspace.isRoot }) };
|
|
444
|
+
} catch (error) {
|
|
445
|
+
return { ok: false, detail: `cannot enumerate workspaces: ${describeError(error)}` };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
406
448
|
function everyTestFileNamesItsTier(cwd = process.cwd()) {
|
|
407
449
|
const untiered = findTestFiles(cwd).filter((path2) => !hasTierInfix(path2));
|
|
408
450
|
if (untiered.length === 0) return true;
|
|
409
451
|
return { ok: false, detail: formatPaths(untiered) };
|
|
410
452
|
}
|
|
453
|
+
function everyViteConfigHasVitestConfig() {
|
|
454
|
+
const discovery = discoverMemberWorkspaces();
|
|
455
|
+
if (!discovery.ok) return discovery;
|
|
456
|
+
const unpaired = discovery.workspaces.flatMap((workspace) => {
|
|
457
|
+
const viteConfigs = findWorkspaceConfigs(workspace, VITE_CONFIG_PATTERN);
|
|
458
|
+
if (viteConfigs.length === 0) return [];
|
|
459
|
+
return findWorkspaceConfigs(workspace, VITEST_CONFIG_PATTERN).length > 0 ? [] : viteConfigs;
|
|
460
|
+
});
|
|
461
|
+
if (unpaired.length === 0) return true;
|
|
462
|
+
return { ok: false, detail: formatPaths(unpaired) };
|
|
463
|
+
}
|
|
411
464
|
function findFiles(patterns, cwd) {
|
|
412
465
|
return globSync(patterns, { cwd, exclude: (path2) => SCAN_EXCLUDE_DIRS.has(basename(path2)) }).map((path2) => path2.split(sep).join("/")).toSorted();
|
|
413
466
|
}
|
|
467
|
+
function findWorkspaceConfigs(workspace, pattern) {
|
|
468
|
+
return findFiles([pattern], workspace.absolutePath).map((name) => `${workspace.dir}/${name}`);
|
|
469
|
+
}
|
|
414
470
|
function formatPaths(paths) {
|
|
415
471
|
return `${paths.length} found:
|
|
416
472
|
${paths.map((path2) => ` ${path2}`).join("\n")}`;
|
|
417
473
|
}
|
|
418
474
|
function getMinVersion() {
|
|
419
|
-
const picked = { "version": "0.
|
|
475
|
+
const picked = { "version": "0.35.0" };
|
|
420
476
|
if (typeof picked["version"] !== "string") {
|
|
421
477
|
throw new TypeError("nmr/package.json: 'version' must be a string");
|
|
422
478
|
}
|
|
@@ -432,11 +488,17 @@ function hasSupportedStrictLintVersion() {
|
|
|
432
488
|
exempt: resolvesVersionViaWorkspace
|
|
433
489
|
});
|
|
434
490
|
}
|
|
491
|
+
function hasViteConfigBeside(cwd, relativePath) {
|
|
492
|
+
return findFiles([VITE_CONFIG_PATTERN], join(cwd, dirname(relativePath))).length > 0;
|
|
493
|
+
}
|
|
435
494
|
function importsSharedExport(content, exportName, moduleSpecifier) {
|
|
436
495
|
if (content === void 0) return false;
|
|
437
496
|
const pattern = new RegExp(String.raw`import\s*\{[^}]*\b${exportName}\b[^}]*\}\s*from\s*['"]${moduleSpecifier}['"]`);
|
|
438
497
|
return pattern.test(content);
|
|
439
498
|
}
|
|
499
|
+
function isOwnedByReExportCheck(cwd, relativePath) {
|
|
500
|
+
return isReExportOnly(readFileIn(cwd, relativePath)) && !hasViteConfigBeside(cwd, relativePath);
|
|
501
|
+
}
|
|
440
502
|
function isReExportOnly(content) {
|
|
441
503
|
if (content === void 0) return false;
|
|
442
504
|
const statements = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "").split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
@@ -452,8 +514,8 @@ function noPnpmFieldInPackageJson(cwd = process.cwd()) {
|
|
|
452
514
|
return { ok: false, detail: formatPaths(declaring) };
|
|
453
515
|
}
|
|
454
516
|
function noReExportOnlyVitestConfigs(cwd = process.cwd()) {
|
|
455
|
-
const nonRootConfigs = findFiles([
|
|
456
|
-
const reExports = nonRootConfigs.filter((path2) =>
|
|
517
|
+
const nonRootConfigs = findFiles([`**/${VITEST_CONFIG_PATTERN}`], cwd).filter((path2) => path2.includes("/"));
|
|
518
|
+
const reExports = nonRootConfigs.filter((path2) => isOwnedByReExportCheck(cwd, path2));
|
|
457
519
|
if (reExports.length === 0) return true;
|
|
458
520
|
return { ok: false, detail: formatPaths(reExports) };
|
|
459
521
|
}
|
|
@@ -546,8 +608,18 @@ function toolVersionsHasNoPnpm() {
|
|
|
546
608
|
if (content === void 0) return true;
|
|
547
609
|
return !/^pnpm\s/m.test(content);
|
|
548
610
|
}
|
|
549
|
-
function vitestConfigBuildsOnSharedConfig(
|
|
550
|
-
|
|
611
|
+
function vitestConfigBuildsOnSharedConfig() {
|
|
612
|
+
const cwd = process.cwd();
|
|
613
|
+
const rootConfigs = findFiles([VITEST_CONFIG_PATTERN], cwd);
|
|
614
|
+
if (rootConfigs.length === 0) return { ok: false, detail: "vitest.config.ts is missing" };
|
|
615
|
+
const discovery = discoverMemberWorkspaces();
|
|
616
|
+
if (!discovery.ok) return discovery;
|
|
617
|
+
const workspaceConfigs = discovery.workspaces.flatMap((workspace) => findWorkspaceConfigs(workspace, VITEST_CONFIG_PATTERN)).filter((relativePath) => !isOwnedByReExportCheck(cwd, relativePath));
|
|
618
|
+
const stale = [...rootConfigs, ...workspaceConfigs].filter(
|
|
619
|
+
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), "defineVitestConfig", SHARED_VITEST_MODULE)
|
|
620
|
+
);
|
|
621
|
+
if (stale.length === 0) return true;
|
|
622
|
+
return { ok: false, detail: formatPaths(stale) };
|
|
551
623
|
}
|
|
552
624
|
function vitestRootConfigBuildsOnSharedConfig(cwd = process.cwd()) {
|
|
553
625
|
return checkRootVitestConfig("vitest.root.config", "defineRootVitestConfig", cwd);
|
|
@@ -555,6 +627,7 @@ function vitestRootConfigBuildsOnSharedConfig(cwd = process.cwd()) {
|
|
|
555
627
|
export {
|
|
556
628
|
default_default as default,
|
|
557
629
|
everyTestFileNamesItsTier,
|
|
630
|
+
everyViteConfigHasVitestConfig,
|
|
558
631
|
hasSupportedEslintVersion,
|
|
559
632
|
hasSupportedStrictLintVersion,
|
|
560
633
|
noPnpmFieldInPackageJson,
|
package/.readyup/manifest.json
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
"version": 1,
|
|
3
3
|
"kits": [
|
|
4
4
|
{
|
|
5
|
+
"bundledDependencies": {
|
|
6
|
+
"@williamthorsen/toolbelt.errors": "0.6.3"
|
|
7
|
+
},
|
|
5
8
|
"checklists": [
|
|
6
9
|
"nmr"
|
|
7
10
|
],
|
|
8
|
-
"esbuildVersion": "0.28.
|
|
11
|
+
"esbuildVersion": "0.28.2",
|
|
9
12
|
"inputs": [
|
|
10
13
|
{
|
|
11
|
-
"hash": "
|
|
14
|
+
"hash": "b67d9796",
|
|
12
15
|
"kind": "module",
|
|
13
16
|
"path": "kits/default.ts"
|
|
14
17
|
},
|
|
15
18
|
{
|
|
16
|
-
"hash": "
|
|
19
|
+
"hash": "7a897007",
|
|
17
20
|
"kind": "inline",
|
|
18
21
|
"path": "../package.json",
|
|
19
22
|
"paths": [
|
|
@@ -26,22 +29,22 @@
|
|
|
26
29
|
"path": "../src/default-scripts.ts"
|
|
27
30
|
},
|
|
28
31
|
{
|
|
29
|
-
"hash": "
|
|
32
|
+
"hash": "c53d0c51",
|
|
30
33
|
"kind": "module",
|
|
31
34
|
"path": "../src/resolve-scripts.ts"
|
|
32
35
|
},
|
|
33
36
|
{
|
|
34
|
-
"hash": "
|
|
37
|
+
"hash": "2dfde614",
|
|
35
38
|
"kind": "module",
|
|
36
39
|
"path": "../src/tiers.ts"
|
|
37
40
|
}
|
|
38
41
|
],
|
|
39
42
|
"name": "default",
|
|
40
43
|
"path": "kits/default.js",
|
|
41
|
-
"readyupVersion": "0.
|
|
44
|
+
"readyupVersion": "0.35.0",
|
|
42
45
|
"source": "kits/default.ts",
|
|
43
|
-
"sourceHash": "
|
|
44
|
-
"targetHash": "
|
|
46
|
+
"sourceHash": "b67d9796",
|
|
47
|
+
"targetHash": "3cd6b2d0"
|
|
45
48
|
}
|
|
46
49
|
]
|
|
47
50
|
}
|
package/README.md
CHANGED
|
@@ -3,25 +3,33 @@
|
|
|
3
3
|
Context-aware script runner for pnpm monorepos. Ships an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
|
|
4
4
|
|
|
5
5
|
<!-- section:release-notes -->
|
|
6
|
-
## Release notes — v0.
|
|
6
|
+
## Release notes — v0.35.0 (2026-09-01)
|
|
7
7
|
|
|
8
8
|
### 🎉 Features
|
|
9
9
|
|
|
10
|
-
-
|
|
10
|
+
- Expose tsconfigPaths as a first-class Vitest config flag (#751)
|
|
11
11
|
|
|
12
|
-
Adds
|
|
12
|
+
Adds a `tsconfigPaths` option to `defineVitestConfig` and `defineRootVitestConfig` in `@williamthorsen/nmr/vitest`. It emits Vite's `resolve.tsconfigPaths`, so a test resolves an aliased specifier through the `paths` its own `tsconfig.json` declares, the way `tsc` does.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
The option defaults to `false` and requires Vite 8, which `@williamthorsen/nmr` now declares as an optional peer dependency at `>=8.0.0 <9`.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
- Publish a test-file conventions check consuming repos can gate in CI (#753)
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Adds `checkTestFileConventions` at `@williamthorsen/nmr/tests`, which a consuming repo calls from a test of its own. It fails the run on any test file that names no isolation tier or sits outside `__tests__`. Both passed silently before.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Adds `testCollectionExclude` to `defineVitestConfig` and `defineRootVitestConfig`. The same array of directory names goes to the check's `exclude`, so one declaration scopes both the sweep and what Vitest collects.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
- Keep doc comments in published declaration files (#762)
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Changes the behavior of `nmr-compile` so that it preserves doc comments in `.d.ts` files.
|
|
25
|
+
|
|
26
|
+
TypeScript's `removeComments` governs a whole emit, so a TS configuration that kept comments out of JavaScript also kept them out of declaration files. `nmr-compile` now emits from two programs: The first produces the `.js` under the package's own `removeComments`, and the second produces the declarations with comments.
|
|
27
|
+
|
|
28
|
+
- Report workspaces whose Vitest config bypasses the projects model (#763)
|
|
29
|
+
|
|
30
|
+
Adds a check to nmr's ReadyUp kit to report every workspace that holds a Vite config without a Vitest config beside it, and widens the existing content check from the repo root to every workspace, so a Vitest config that does not build on `defineVitestConfig` is reported. The purpose of these checks is to ensure that tier-selecting test commands are supported.
|
|
31
|
+
|
|
32
|
+
Also fixes an issue where the `no package re-exports the ancestor Vitest config` check incorrectly advised a consumer to delete a config.
|
|
25
33
|
<!-- /section:release-notes -->
|
|
26
34
|
|
|
27
35
|
## Installation
|
|
@@ -49,7 +57,7 @@ From a package directory:
|
|
|
49
57
|
|
|
50
58
|
```bash
|
|
51
59
|
nmr test # Run tests for the current package
|
|
52
|
-
nmr build # Compile to .js and .d.ts
|
|
60
|
+
nmr build # Compile to .js and .d.ts
|
|
53
61
|
nmr check # Typecheck, format check, lint check, and tests
|
|
54
62
|
```
|
|
55
63
|
|
|
@@ -801,7 +809,7 @@ Position determines ownership: flags before the command name are nmr's own, and
|
|
|
801
809
|
```bash
|
|
802
810
|
# From a package directory
|
|
803
811
|
nmr test # Run workspace test script
|
|
804
|
-
nmr build # Compile to .js and .d.ts
|
|
812
|
+
nmr build # Compile to .js and .d.ts
|
|
805
813
|
|
|
806
814
|
# From the monorepo root
|
|
807
815
|
nmr test # Root tests + recursive workspace tests
|
|
@@ -878,7 +886,7 @@ nmr-clean
|
|
|
878
886
|
|
|
879
887
|
### `nmr-compile`
|
|
880
888
|
|
|
881
|
-
Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` and `.d.ts`
|
|
889
|
+
Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` from one program and `.d.ts` from a second that reuses it. The package's own `removeComments` governs the `.js` emit; the declarations emit forces it off, so `.d.ts` files carry their doc comments either way. Because the compiler parses each source file, every relative import form — static, re-export, dynamic `import()`, and bare side-effect — is rewritten from `.ts` to `.js` in both outputs, and `.ts` occurrences inside strings and comments are left intact. tsconfig `paths` aliases are resolved to runnable relative `.js` specifiers in both outputs, sourced from the package's tsconfig. An aliased import whose target resolves outside the package's `src/` and is not resolvable without the alias mapping fails the build with a diagnostic, rather than being emitted verbatim to produce output that fails at runtime. The build is skipped when no input has changed and the previous output is still on disk (the key is cached under `node_modules/.cache/nmr-compile/`, outside the published output). Deleting the output by any means — `nmr clean`, `rm -rf dist`, `git clean` — therefore forces a rebuild rather than a skip. This is the default `compile` script — run it from a package directory.
|
|
882
890
|
|
|
883
891
|
**What it compiles.** Entry points are `src/**/*.ts` less the directories that hold test scaffolding rather than shipped code: `__fixtures__/`, `__mocks__/`, `__tests__/`, and `test-utils/`. Add to that list per package with [`build.extraIgnorePatterns`](#package-level-configuration). Ignoring a directory drops it as an _entry point_, not from the emit: the compiler still emits whatever the surviving entry points import, so a helper that production code uses is still compiled and its importer never emits a dangling specifier. This list is deliberately not the Vitest [coverage exclusions](#what-the-config-excludes) — helpers live in `test-utils/` precisely so they stay covered.
|
|
884
892
|
|
|
@@ -937,7 +945,7 @@ ensure-prepublish-hooks
|
|
|
937
945
|
|
|
938
946
|
## Conformance checks
|
|
939
947
|
|
|
940
|
-
nmr publishes a `readyup` kit that checks a consuming repo against the current release: the shared Vitest and Prettier configs, the workspace layout, the root script registry, the [test-tier convention](#test-tiers), and that no `package.json` in the tree declares a `pnpm` field, which pnpm 11 reads no key from. Those settings belong in `pnpm-workspace.yaml`, and `pnpx codemod run pnpm-v10-to-v11` moves them. The kit ships inside the package, so it checks against the nmr version installed rather than whatever a repository ref happens to point at, and a tier added or renamed in nmr reaches the repo on upgrade.
|
|
948
|
+
nmr publishes a `readyup` kit that checks a consuming repo against the current release: the shared Vitest and Prettier configs, the workspace layout, the root script registry, the [test-tier convention](#test-tiers), and that no `package.json` in the tree declares a `pnpm` field, which pnpm 11 reads no key from. Those settings belong in `pnpm-workspace.yaml`, and `pnpx codemod run pnpm-v10-to-v11` moves them. The kit ships inside the package, so it checks against the nmr version installed rather than whatever a repository ref happens to point at, and a tier added or renamed in nmr reaches the repo on upgrade. A repo that wants the tier convention in its own gate rather than in a command run by hand declares the [conventions check](#gating-the-test-file-conventions) instead, which also covers the half the kit does not.
|
|
941
949
|
|
|
942
950
|
Add `readyup` as a devDependency, then name nmr in its config:
|
|
943
951
|
|
|
@@ -1059,7 +1067,7 @@ import { defineVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
|
1059
1067
|
export default defineVitestConfig();
|
|
1060
1068
|
```
|
|
1061
1069
|
|
|
1062
|
-
`vitest` is a peer dependency (`>=4.0.0 <5`), declared optional — the consuming repo provides it, and repos that never import this subpath are unaffected.
|
|
1070
|
+
`vitest` is a peer dependency (`>=4.0.0 <5`), declared optional — the consuming repo provides it, and repos that never import this subpath are unaffected. `vite` is an optional peer as well (`>=8.0.0 <9`), required only by the [`tsconfigPaths` setting](#resolving-through-tsconfig-paths); a repo that declares a `vite` of its own below that range sees an unmet-peer warning whether or not it sets the flag.
|
|
1063
1071
|
|
|
1064
1072
|
### Test tiers
|
|
1065
1073
|
|
|
@@ -1086,7 +1094,7 @@ vitest # every project
|
|
|
1086
1094
|
|
|
1087
1095
|
**Every test file names its tier**, in the form `<subject>[.<aspect>].<tier>.test.ts`. Only the segment immediately before `.test.` selects a project, so an earlier one is free for documentation: `resolveConfig.packaged.unit.test.ts` reads as a companion to `resolveConfig.unit.test.ts` and still names `unit`.
|
|
1088
1096
|
|
|
1089
|
-
`unit` is defined by subtracting the tiers rather than by an allow-list of infixes, so a file such as `parser.smoke.test.ts` runs under `unit` instead of being silently dropped. That is a safety net, not a licence to omit the tier: a file whose tier segment is missing or misspelt runs under `unit` and reports success, so no test run distinguishes it from a conformant one. [nmr's readyup kit](#conformance-checks) reports those files
|
|
1097
|
+
`unit` is defined by subtracting the tiers rather than by an allow-list of infixes, so a file such as `parser.smoke.test.ts` runs under `unit` instead of being silently dropped. That is a safety net, not a licence to omit the tier: a file whose tier segment is missing or misspelt runs under `unit` and reports success, so no test run distinguishes it from a conformant one until the repo declares the [conventions check](#gating-the-test-file-conventions). In a repo that has not, [nmr's readyup kit](#conformance-checks) is what reports those files.
|
|
1090
1098
|
|
|
1091
1099
|
`localhost` and `remote` have no test script of their own. They are declared anyway, so that a `*.remote.test.ts` file cannot fall into `unit` and run in the default gate unnoticed.
|
|
1092
1100
|
|
|
@@ -1094,6 +1102,40 @@ Every tier above `unit` carries a 30-second `testTimeout` and `hookTimeout`, whe
|
|
|
1094
1102
|
|
|
1095
1103
|
To raise a budget for one tier and no other, use the `tiers` seam below. The `project` seam merges over both budgets as well, but like every option passed through it the value reaches all four projects at once -- raising a tier's budget raises `unit`'s with it. To lift the ceiling for a single file rather than a whole tier, pass a timeout to the individual test or hook, which stays the narrower tool.
|
|
1096
1104
|
|
|
1105
|
+
### Gating the test-file conventions
|
|
1106
|
+
|
|
1107
|
+
Both halves of the convention are silent on their own: an untiered file runs under the residual `unit` and passes, and a file outside `__tests__` is collected by nothing at all. `@williamthorsen/nmr/tests` exports a check that reports both, which a repo declares from a one-line test of its own:
|
|
1108
|
+
|
|
1109
|
+
```ts
|
|
1110
|
+
// __tests__/test-file-conventions.unit.test.ts
|
|
1111
|
+
import { checkTestFileConventions } from '@williamthorsen/nmr/tests';
|
|
1112
|
+
|
|
1113
|
+
checkTestFileConventions();
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
It declares one suite with an assertion per half, each carrying the remedy that fixes it: the four tier names and the rename form, or the `__tests__` directory the file belongs in. The guard names its own tier like any other test file, so a misnamed guard reports itself.
|
|
1117
|
+
|
|
1118
|
+
A repo linting with `vitest/require-hook` needs a disable directive on that call, which the rule reads as setup work where it declares the suite:
|
|
1119
|
+
|
|
1120
|
+
```ts
|
|
1121
|
+
// eslint-disable-next-line vitest/require-hook -- the call declares the suite, where the rule reads it as setup work.
|
|
1122
|
+
checkTestFileConventions();
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
The sweep starts at the monorepo root rather than at the directory Vitest supplies, so one call covers the whole repo from whichever package the run began in. `rootDir` overrides that, for a check pointed at a tree other than the repo's own.
|
|
1126
|
+
|
|
1127
|
+
An exported check rather than a step nmr runs for you, because nothing nmr runs reaches the whole tree: `nmr test` fans out per package, and a direct `vitest` invocation runs no nmr script at all. A test the repo owns also lets the repo scope what the sweep covers.
|
|
1128
|
+
|
|
1129
|
+
`exclude` names directory basenames the sweep prunes at any depth, additive to the ones nmr always prunes:
|
|
1130
|
+
|
|
1131
|
+
```ts
|
|
1132
|
+
checkTestFileConventions({ exclude: ['cypress', 'generated'] });
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
**Pass the same array to [`testCollectionExclude`](#what-the-config-excludes).** The two describe one scope, and naming a directory in only one is a defect in either direction. Pruned from the sweep alone, the directory's test files still run and still report nothing, which is the silence this check exists to end. Excluded from collection alone, the sweep reports files a consumer has no reason to act on.
|
|
1136
|
+
|
|
1137
|
+
Expect the first run to fail in a repo that has never gated this. Vitest 4 excludes only `node_modules` and `.git` by default, so a `__tests__` tree under a generated or vendored directory is collected and runs today; naming that directory in both lists is the fix, rather than widening what nmr prunes for everyone.
|
|
1138
|
+
|
|
1097
1139
|
### What the config supplies
|
|
1098
1140
|
|
|
1099
1141
|
Two settings a workspace test runner needs almost universally are part of what the factory produces, so a config file declares neither:
|
|
@@ -1113,6 +1155,20 @@ export default defineVitestConfig({ isolateGit: false, resolveFromSource: false
|
|
|
1113
1155
|
|
|
1114
1156
|
**A package declaring a `source` condition must ship its sources.** Every nmr consumer now resolves that condition, so a published package whose `files` carries `dist` alone resolves to a path that is not in the tarball. A package that publishes only its build output takes a bespoke condition name instead, the way `@williamthorsen/nmr-core` uses `nmr-source` for the build's own bootstrap.
|
|
1115
1157
|
|
|
1158
|
+
### Resolving through tsconfig paths
|
|
1159
|
+
|
|
1160
|
+
A third resolution setting sits alongside those two, and this one is off. `tsconfigPaths: true` emits Vite's `resolve.tsconfigPaths`, so a test reaches an aliased specifier through the `paths` its `tsconfig.json` declares, the way `tsc` does:
|
|
1161
|
+
|
|
1162
|
+
```ts
|
|
1163
|
+
export default defineVitestConfig({ tsconfigPaths: true });
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
It folds across layers like the other two, the last layer to declare it winning. It needs no `ssr` counterpart the way a condition does: Vite holds this setting outside its per-environment resolve options, so the single key reaches the environment a test's own imports resolve through.
|
|
1167
|
+
|
|
1168
|
+
**It is an opt-in because its silent direction is the reverse of the two defaults'.** Leaving it off fails loudly, with an unresolved import naming the specifier. Turning it on is the silent direction, for a repo that declares `paths` for `tsc` alone: a specifier that resolved through a package's `exports` starts resolving through the alias instead, and nothing in the run reports the switch. A repo declaring no `paths` is unaffected either way.
|
|
1169
|
+
|
|
1170
|
+
**It requires Vite 8**, where `resolve.tsconfigPaths` arrived. nmr declares `vite` as an optional peer at `>=8.0.0 <9`, so a repo holding a stale `vite` of its own hears about it at install time. That check does not reach a repo whose only Vite comes in through Vitest's own dependency, and Vitest 4 accepts Vite 6 and 7: on those, the emitted key does nothing and the aliased import fails exactly as it would with the flag off.
|
|
1171
|
+
|
|
1116
1172
|
### Customizing by scope
|
|
1117
1173
|
|
|
1118
1174
|
Vitest applies some options at the root of a `projects` config and others per project, and placing one at the wrong level is silent rather than loud. The factory therefore takes separate override surfaces instead of one merged config:
|
|
@@ -1167,6 +1223,8 @@ Both factories fold any number of layers left to right: a later layer wins on a
|
|
|
1167
1223
|
|
|
1168
1224
|
Order is the point where `setupFiles` is concerned, since a shared setup file establishes the environment the package's own then runs in. A later layer's `project` block likewise wins over an earlier layer's `tiers` target: the nearer config is the more deliberate.
|
|
1169
1225
|
|
|
1226
|
+
**A package's own `vite.config.ts` fills that one slot too.** Vitest searches for a config by ascending from the run root and stopping at the first directory holding any of `vitest.config.*` or `vite.config.*`, so a package carrying only a Vite config ends the search there and never reaches the root config. Within a directory `vitest.config.*` is tried first, which makes the fix a Vitest config beside the Vite one, calling `defineVitestConfig()` as any other package config does. Without it the package declares no projects and every tier-selecting command fails with `No projects matched the filter`, `nmr ci` among them. [nmr's readyup kit](#conformance-checks) reports a package in that state.
|
|
1227
|
+
|
|
1170
1228
|
**A path in a shared layer must be absolute.** Vitest resolves `setupFiles` against each project's own root, not against the module that declared the path, so a bare `'./vitest.setup.ts'` in a shared module names a different file in every package that consumes it -- and a package that happens to own a file by that name loads the wrong one instead of failing. The co-located form stays correct in a config file that declares the path directly.
|
|
1171
1229
|
|
|
1172
1230
|
**Do not merge two returned configs.** `mergeConfig(defineVitestConfig(), defineVitestConfig(mine))` looks like the idiomatic recovery and fails at startup: both sides declare the same four project names, which Vitest rejects with `Project name "unit" ... is not unique`. Layers merge the factory's inputs instead, which is why they yield four projects however many fold.
|
|
@@ -1175,7 +1233,15 @@ A config file that omits the shared layer still loses those settings, silently -
|
|
|
1175
1233
|
|
|
1176
1234
|
### What the config excludes
|
|
1177
1235
|
|
|
1178
|
-
Collection skips `**/node_modules/**`, `**/.git/**`, and `**/dist/**`. Coverage skips `**/__{fixtures,mocks,tests}__/**`, `**/index.ts`, and `**/*.d.ts`.
|
|
1236
|
+
Collection skips `**/node_modules/**`, `**/.git/**`, `**/coverage/**`, and `**/dist/**`. Coverage skips `**/__{fixtures,mocks,tests}__/**`, `**/index.ts`, and `**/*.d.ts`.
|
|
1237
|
+
|
|
1238
|
+
`testCollectionExclude` adds to the collection list. It takes directory basenames rather than globs, matched at any depth, so the same array serves the [conventions check](#gating-the-test-file-conventions):
|
|
1239
|
+
|
|
1240
|
+
```ts
|
|
1241
|
+
export default defineVitestConfig({ testCollectionExclude: ['cypress', 'generated'] });
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
Every layer's entries are concatenated, and a name the shared config already prunes is emitted once. A repo needing a glob rather than a directory name has the `project` seam's own `exclude`.
|
|
1179
1245
|
|
|
1180
1246
|
Because both seams concatenate, a consumer can add an exclusion but never remove one. A pattern therefore earns its place in these lists only by preventing a _silent_ failure, one a consumer cannot self-diagnose. A visible failure, such as a stray file sitting at 0% in the coverage report, is left to the consumer's own `project` seam.
|
|
1181
1247
|
|
|
@@ -1212,7 +1278,7 @@ The projects were once named `unit`, `integration`, and `app`, for what a test _
|
|
|
1212
1278
|
2. Rename every `*.app.test.ts` onto the convention. `app` was never a tier, so keep it as the aspect segment and add the tier after it: `scaffold.app.test.ts` becomes `scaffold.app.unit.test.ts`.
|
|
1213
1279
|
3. Replace `nmr test:integration` with `nmr test:tool`, and `nmr root:test:integration` with `nmr root:test:tool`, wherever a script or workflow names them.
|
|
1214
1280
|
|
|
1215
|
-
**A file left without a tier is a silent failure.** The residual `unit` claims it and it runs in the default gate -- green, with nothing reporting that the separation was lost. [nmr's readyup kit](#conformance-checks) reports every such file
|
|
1281
|
+
**A file left without a tier is a silent failure.** The residual `unit` claims it and it runs in the default gate -- green, with nothing reporting that the separation was lost. Declare the [conventions check](#gating-the-test-file-conventions), which turns that state into a failing test run, or run [nmr's readyup kit](#conformance-checks), which reports every such file. Do one of the two before treating the migration as done.
|
|
1216
1282
|
|
|
1217
1283
|
`nmr test` also runs the `tool` tier, which the old `test` excluded. Those tests previously ran in no default selection at all: `check`, `check:strict`, and `ci` all inherited the exclusion, so a green pipeline said nothing about them. Expect `nmr test` to take longer and to surface failures that were never gated.
|
|
1218
1284
|
|
|
@@ -13,5 +13,5 @@ delivery: ambient
|
|
|
13
13
|
- A check that already passed on the current working tree skips, printing `⏭️ <scope>: <command>: passed …` and, where the recalled run left one, `— replayed: <excerpt>` of what that run concluded. Any edit, addition, or deletion re-runs it, so read the replayed excerpt rather than paying for the answer again: never pass `--no-cache` to get a "real" run, which costs the command's full runtime to reach the same answer. Its one use is a file rather than an exit status -- a skipped command writes none, so a fresh `coverage/` needs `nmr --no-cache test:coverage`. A composite replays its constituents' excerpts, each attributed as `<scope>: <command>: <excerpt>` and cut at the line's ceiling where there are too many to fit. A skip carrying no excerpt is one whose run wrote to a terminal, or one whose constituents recorded none.
|
|
14
14
|
- `nmr --log <command>` prints the whole run behind that excerpt -- up to 256 KiB, led by when it was recorded, how long it ran, and the command string that produced it -- so output you need in full is a recording to read rather than a run to pay for again. It runs nothing, and it is held to the tree rather than to the terminal: a recording a piped run made prints for a reader at one. A composite prints its constituents' excerpts, so the transcript of one of them is `nmr -F <pkg> --log <command>` or that constituent's own name. Nothing to show is said on one line and exits non-zero, naming which it is.
|
|
15
15
|
- Every command nmr runs reports one line -- `✅ <scope>: <command>: passed in 12.4s`, `❌ … failed in 1.2s (exit 1)`, `⏭️ … passed 4m ago on this tree`, or `⛔ … skipped, the override is empty` -- and a composite reports alongside every command it expands into. The lines print in every verbosity, so `-q` is the low-noise way to run rather than a silent one: it withholds the output of the commands nmr runs and keeps nmr's own verdicts, which is how a pass, a cached skip, and a `""`/`":"` override stay distinguishable. Expect a run to be quiet already without the flag: nmr recognizes a known agent harness in the environment and takes quiet from that, as it does from `NMR_COMMAND_VERBOSITY` or the repo's `output.commandVerbosity`. A command whose output you need in full is one to run with `NMR_COMMAND_VERBOSITY=full`, which outranks both.
|
|
16
|
-
- Every test file lives under a `__tests__` directory and names its isolation tier last: `<subject>[.<aspect>].<tier>.test.ts`, where any earlier segment is free-form documentation. A tier is the furthest thing the test reaches -- `tool` (a program the environment supplies), `localhost` (a service on this machine), `remote` (a machine that isn't this one), or `unit`, the residual claiming everything else.
|
|
16
|
+
- Every test file lives under a `__tests__` directory and names its isolation tier last: `<subject>[.<aspect>].<tier>.test.ts`, where any earlier segment is free-form documentation. A tier is the furthest thing the test reaches -- `tool` (a program the environment supplies), `localhost` (a service on this machine), `remote` (a machine that isn't this one), or `unit`, the residual claiming everything else. A file outside `__tests__`, or one whose tier segment is missing or misspelt, fails the test run in a repo that declares nmr's exported conventions check, which reports both. In a repo that has not, neither fails anything -- the first is collected by nothing and the second runs under `unit` -- and `rdy run --packages` reports the misspelt tier alone.
|
|
17
17
|
- The rest is in `node_modules/@williamthorsen/nmr/README.md`: pre/post hooks (every `nmr X` auto-wraps as `nmr X:pre && nmr X && nmr X:post`), script overrides and their `""`/`":"` skip values, `nmr upgrade` ceilings (report-only until `nmr upgrade --write`), the `nmr-compile` build (there is no repo-local build script), the test tiers' timeout budgets and the `tiers` seam that raises one, which script reaches which tier, and the check-result cache's key, the retention key a replayed excerpt is additionally held to, the retention ceiling `--log` prints within, its `checkCache` config, and every condition that disables it.
|
package/dist/esm/UserError.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A failure in what the user declared -- a config file, a `package.json` -- rather than a fault in nmr.
|
|
3
|
+
*
|
|
4
|
+
* The CLI boundary prints one of these as its message alone. A stack trace records where nmr noticed the
|
|
5
|
+
* problem, which says nothing about the file the reader has to edit.
|
|
6
|
+
*/
|
|
1
7
|
export declare class UserError extends Error {
|
|
2
8
|
constructor(message: string);
|
|
3
9
|
}
|