@williamthorsen/nmr 0.23.0 → 0.25.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 +479 -0
- package/.readyup/manifest.json +16 -0
- package/AGENTS.md +3 -1
- package/README.md +343 -114
- package/dist/esm/check-cache.d.ts +76 -0
- package/dist/esm/check-cache.js +224 -0
- package/dist/esm/cli-build.js +8 -2
- package/dist/esm/commands/build-output.d.ts +13 -0
- package/dist/esm/commands/build-output.js +36 -0
- package/dist/esm/commands/build.d.ts +1 -6
- package/dist/esm/commands/build.js +27 -42
- package/dist/esm/commands/clean.js +10 -3
- package/dist/esm/config.d.ts +5 -7
- package/dist/esm/config.js +115 -15
- package/dist/esm/context.d.ts +1 -1
- package/dist/esm/context.js +2 -2
- package/dist/esm/default-scripts.js +14 -9
- package/dist/esm/defineConfig.d.ts +3 -0
- package/dist/esm/defineConfig.js +3 -0
- package/dist/esm/help.d.ts +1 -1
- package/dist/esm/help.js +1 -0
- package/dist/esm/helpers/duration.d.ts +2 -0
- package/dist/esm/helpers/duration.js +21 -0
- package/dist/esm/helpers/type-guards.d.ts +1 -0
- package/dist/esm/helpers/type-guards.js +3 -0
- package/dist/esm/resolver.d.ts +1 -1
- package/dist/esm/runCli.js +229 -27
- package/dist/esm/tiers.d.ts +7 -0
- package/dist/esm/tiers.js +35 -0
- package/dist/esm/types.d.ts +15 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/vitest.d.ts +6 -2
- package/dist/esm/vitest.js +51 -31
- package/package.json +9 -7
- package/dist/esm/index.d.ts +0 -2
- package/dist/esm/index.js +0 -1
package/README.md
CHANGED
|
@@ -3,19 +3,20 @@
|
|
|
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.25.0 (2026-08-03)
|
|
7
7
|
|
|
8
8
|
### 🎉 Features
|
|
9
9
|
|
|
10
|
-
-
|
|
10
|
+
- Mark a cache hit's time saving with a speed-boost icon (#579)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Time saved due to `nmr`'s cache feature now earns a rocket emoji in the terminal output.
|
|
13
|
+
Savings less than 1 second are no longer reported.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
- Publish nmr's readyup kit with a test-tier conformance check (#580)
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
`@williamthorsen/nmr` is now bundled with ReadyUp kits that check a repo's alignment with `nmr` requirements and recommendations.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
`rdy run --from npm:@williamthorsen/nmr` checks the repo against the kits without prior configuration. Once the package is added to the ReadyUp config, `rdy run --packages` will run the `nmr` kits along with those of any other registered package. Requires `readyup` v0.23 or greater.
|
|
19
20
|
<!-- /section:release-notes -->
|
|
20
21
|
|
|
21
22
|
## Installation
|
|
@@ -50,9 +51,9 @@ nmr check # Typecheck, format check, lint check, and tests
|
|
|
50
51
|
From the monorepo root:
|
|
51
52
|
|
|
52
53
|
```bash
|
|
53
|
-
nmr test
|
|
54
|
-
nmr build
|
|
55
|
-
nmr
|
|
54
|
+
nmr test # Run root tests + recursive workspace tests
|
|
55
|
+
nmr build # Build all packages
|
|
56
|
+
nmr prepush # Run everything the remote runs, before you push
|
|
56
57
|
```
|
|
57
58
|
|
|
58
59
|
nmr detects where you are and selects the right scripts automatically — see [context-aware resolution](#context-aware-resolution) below.
|
|
@@ -61,11 +62,11 @@ nmr detects where you are and selects the right scripts automatically — see [c
|
|
|
61
62
|
|
|
62
63
|
nmr's key feature is that the same command runs different scripts depending on where you invoke it. It walks up from your current directory to find `pnpm-workspace.yaml`, then checks whether your CWD is inside a workspace package directory.
|
|
63
64
|
|
|
64
|
-
| Where you run `nmr` | Registry used | Working directory | `nmr test` runs
|
|
65
|
-
| --------------------------------- | ----------------- | ----------------- |
|
|
66
|
-
| Monorepo root | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test`
|
|
67
|
-
| Inside a workspace package | Workspace scripts | The package root | `pnpm exec vitest --project
|
|
68
|
-
| Anywhere, with `--workspace-root` | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test`
|
|
65
|
+
| Where you run `nmr` | Registry used | Working directory | `nmr test` runs |
|
|
66
|
+
| --------------------------------- | ----------------- | ----------------- | -------------------------------------------------------------------- |
|
|
67
|
+
| Monorepo root | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test` |
|
|
68
|
+
| Inside a workspace package | Workspace scripts | The package root | `pnpm exec vitest --project unit --project tool` (that package only) |
|
|
69
|
+
| Anywhere, with `--workspace-root` | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test` |
|
|
69
70
|
|
|
70
71
|
Relative paths in a script resolve against that working directory, not the invocation directory.
|
|
71
72
|
|
|
@@ -116,10 +117,10 @@ Passthrough arguments attach to the final step alone, because the expansion is a
|
|
|
116
117
|
|
|
117
118
|
## Configuration
|
|
118
119
|
|
|
119
|
-
Create `.config/nmr.config.ts` in the monorepo root to add or override scripts:
|
|
120
|
+
Create `.config/nmr.config.ts` in the monorepo root to add or override scripts. A package may carry one too, for [build settings of its own](#package-level-configuration):
|
|
120
121
|
|
|
121
122
|
```ts
|
|
122
|
-
import { defineConfig } from '@williamthorsen/nmr';
|
|
123
|
+
import { defineConfig } from '@williamthorsen/nmr/config';
|
|
123
124
|
|
|
124
125
|
export default defineConfig({
|
|
125
126
|
workspaceScripts: {
|
|
@@ -133,14 +134,35 @@ export default defineConfig({
|
|
|
133
134
|
|
|
134
135
|
### `defineConfig` fields
|
|
135
136
|
|
|
136
|
-
| Field | Type
|
|
137
|
-
| ------------------ |
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
137
|
+
| Field | Type | Description |
|
|
138
|
+
| ------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
139
|
+
| `build` | `{ extraIgnorePatterns?: string[] }` | Patterns added to the build's ignore set (package config only) |
|
|
140
|
+
| `checkCache` | `{ enabled?: boolean; extraCommands?: string[]; excludeCommands?: string[] }` | Which commands the [check-result cache](#check-result-cache) may skip |
|
|
141
|
+
| `workspaceScripts` | `Record<string, string \| string[]>` | Scripts added or overridden in the workspace registry (tier 2) |
|
|
142
|
+
| `rootScripts` | `Record<string, string \| string[]>` | Scripts added or overridden in the root registry (tier 2) |
|
|
143
|
+
| `devBin` | `Record<string, string>` | Map binary names to source-repo replacement commands |
|
|
141
144
|
|
|
142
145
|
All fields are optional. `workspaceScripts` and `rootScripts` values follow the same `string | string[]` convention described in [script values](#script-values).
|
|
143
146
|
|
|
147
|
+
Each field belongs to exactly one tier, and a config loads only the fields its own tier honors: `build` in a package config, the other four in the monorepo-root config. Declaring a field at the wrong tier fails with a message naming it and where it goes, as does a key nmr does not recognize at all — a typo cannot degrade into a setting that silently applies nowhere.
|
|
148
|
+
|
|
149
|
+
### Package-level configuration
|
|
150
|
+
|
|
151
|
+
A package may carry its own `.config/nmr.config.ts`, which `nmr-compile` reads from the package it is compiling. This tier honors `build` alone; declaring `workspaceScripts`, `rootScripts`, or `devBin` there fails with a message naming them, rather than being quietly ignored — those belong in the monorepo-root config, and a package that appeared to set them would otherwise build on settings nothing applied. The monorepo-root config carries the mirror restriction: `build` there fails the same way, since only a package's own config reaches the compile.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// packages/my-package/.config/nmr.config.ts
|
|
155
|
+
import { defineConfig } from '@williamthorsen/nmr/config';
|
|
156
|
+
|
|
157
|
+
export default defineConfig({
|
|
158
|
+
build: { extraIgnorePatterns: ['**/fixtures/**'] },
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`extraIgnorePatterns` adds to the build's [default ignore set](#nmr-compile) rather than replacing it, so declaring a pattern cannot start a package shipping its own tests. The programmatic `buildPackage` option of the same name behaves identically; its bare `ignorePatterns` is the one that replaces.
|
|
163
|
+
|
|
164
|
+
> **Building nmr from source:** a config file that has to load while nmr itself is being built — before its `dist` exists — must use the type-only form, `import type { NmrConfig } from '@williamthorsen/nmr/config'` with `satisfies NmrConfig`, which carries no runtime import. `defineConfig` resolves through nmr's build output and so is unavailable at that moment. Every other config, including every consumer's, installs nmr from a tarball that ships `dist` and can use `defineConfig` freely.
|
|
165
|
+
|
|
144
166
|
### `devBin` — source-repo binary substitution
|
|
145
167
|
|
|
146
168
|
When developing a CLI tool inside the monorepo, the published binary may not reflect your latest source changes. `devBin` lets you map a binary name to a replacement command that runs from source:
|
|
@@ -175,7 +197,7 @@ Extend `nmr build` with a pre-build step for every workspace package:
|
|
|
175
197
|
|
|
176
198
|
```ts
|
|
177
199
|
// .config/nmr.config.ts
|
|
178
|
-
import { defineConfig } from '@williamthorsen/nmr';
|
|
200
|
+
import { defineConfig } from '@williamthorsen/nmr/config';
|
|
179
201
|
|
|
180
202
|
export default defineConfig({
|
|
181
203
|
workspaceScripts: {
|
|
@@ -197,6 +219,114 @@ Attach a post-build step to one package:
|
|
|
197
219
|
|
|
198
220
|
The second example calls the bin directly, which sidesteps the workspace-versus-root registry distinction.
|
|
199
221
|
|
|
222
|
+
## Check-result cache
|
|
223
|
+
|
|
224
|
+
A check that passed on a working tree passes again on the same working tree. nmr records that, and the next run of the same command on an unchanged tree exits 0 without doing the work:
|
|
225
|
+
|
|
226
|
+
```console
|
|
227
|
+
$ nmr ci
|
|
228
|
+
# ... four minutes of build, typecheck, lint, and coverage ...
|
|
229
|
+
|
|
230
|
+
$ nmr ci
|
|
231
|
+
⏭️ ci: passed 2m ago on this tree (🚀 saved ~4m). Re-run with --no-cache.
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Composite commands expand into child `nmr` processes, so one green `nmr ci` records a pass for every cacheable command in the chain, at every scope it ran at. A later `nmr check`, `nmr typecheck`, or `nmr -F core test` on the same tree skips too. The skip line is suppressed by `-q`.
|
|
235
|
+
|
|
236
|
+
The cache is a working-tree cache, not a build cache: it answers "has this exact tree already passed this check?" and nothing else. It is also local to one checkout (the install fingerprint in its key is machine-specific), so it saves repeated work in a working copy rather than sharing results across machines or with CI.
|
|
237
|
+
|
|
238
|
+
### What is cached
|
|
239
|
+
|
|
240
|
+
Cacheable by default: `check`, `check:agent-files`, `check:strict`, `ci`, `fix:check`, `fmt:check`, `lint:check`, `lint:strict`, `test`, `test:coverage`, `test:tool`, `test:unit`, `typecheck`, and the `root:` variants `root:check`, `root:lint:check`, `root:lint:strict`, `root:test`, `root:test:tool`, `root:test:unit`, and `root:typecheck`.
|
|
241
|
+
|
|
242
|
+
Everything else runs every time:
|
|
243
|
+
|
|
244
|
+
| Command | Why it is never skipped |
|
|
245
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
246
|
+
| `audit`, `prepush` | Consult a vulnerability database that moves without the tree. `prepush`'s `ci` constituent still skips while its `audit` runs every time. |
|
|
247
|
+
| `build`, `compile` | Carry a cache of their own. |
|
|
248
|
+
| `fix`, `fmt`, `lint`, `upgrade` | Rewrite the tree they were asked about, so a recorded pass would describe a tree that no longer exists. |
|
|
249
|
+
| `test:all` | Reaches the `localhost` and `remote` tiers, which the environment supplies rather than the tree. |
|
|
250
|
+
|
|
251
|
+
### The exit-status-only contract
|
|
252
|
+
|
|
253
|
+
**A cacheable name promises that its whole contribution is an exit status**, through its entire chain, `:pre` and `:post` hooks included. A hit skips that chain wholesale, so any file a cacheable command was relied on to produce is a file that will not appear.
|
|
254
|
+
|
|
255
|
+
`test:coverage` is cacheable on exactly this reading: what it contributes is the pass, not the `coverage/` directory, which a skipped run leaves as whatever the last real run wrote. Read `coverage/` after a `test:coverage` that may have skipped, and it may be stale; `--no-cache` is how you insist on a fresh one.
|
|
256
|
+
|
|
257
|
+
Overriding a cacheable name, in `package.json` or in `workspaceScripts`, inherits its cacheability. An override that writes something anyone depends on belongs in `excludeCommands`.
|
|
258
|
+
|
|
259
|
+
### What the key is made of
|
|
260
|
+
|
|
261
|
+
A hit requires all of these to match the run that recorded the pass:
|
|
262
|
+
|
|
263
|
+
- **The working tree's content**, hashed from the commit's tree object folded with the current content of every path git reports as changed or untracked. Timestamps are not content, so a `touch` that rewrites no bytes leaves the hash alone.
|
|
264
|
+
- **The command string that would run**, hooks and all, and the scope it would run in.
|
|
265
|
+
- **nmr's version**, the **Node version**, and the **platform and architecture**.
|
|
266
|
+
- **What pnpm has installed**, so an install, a prune, or a lockfile change forces a re-run.
|
|
267
|
+
- **`TZ`, `LANG`, `LC_ALL`, and `NODE_OPTIONS`.** This set is fixed, so two machines that differ only in shell decoration still agree.
|
|
268
|
+
|
|
269
|
+
A hit additionally requires the build output of every package nmr's own build covers to be present, and to have been built from this tree. Output is git-ignored, so neither its removal nor its replacement moves the hash: restoring a tree with `git stash` or `git checkout` restores none of the output that tree was built with. nmr compares each covered package's recorded build digest against the one on disk, so a `dist` compiled from another tree is a miss the following run repairs. A package that overrides `build` or `compile` emits on terms nmr does not know and is exempt.
|
|
270
|
+
|
|
271
|
+
A pass is recorded only when the command exits 0, only when the tree still matches the one the run started against, only when every covered package's output is present, and never for a run that executed nothing (a `""`/`":"` skip override, or an `NMR_RUN_IF_PRESENT` miss).
|
|
272
|
+
|
|
273
|
+
### Out of contract
|
|
274
|
+
|
|
275
|
+
These change what a check concludes without moving the hash. Where one applies, exclude the affected command:
|
|
276
|
+
|
|
277
|
+
- **Content filters.** `core.autocrlf` and `.gitattributes` filters mean the bytes on disk differ from the bytes git records; the hash follows what is on disk, but a checkout on another platform may not reproduce it.
|
|
278
|
+
- **Symlinked inputs.** A symlink is hashed by the path it names, not by the content at the far end.
|
|
279
|
+
- **Gitignored inputs.** Anything git does not report is invisible to the hash. The build-output probe is the one exception, and covers only nmr's own build.
|
|
280
|
+
- **`passWithNoTests`.** A suite that collects no files exits 0, and that green is recorded like any other.
|
|
281
|
+
|
|
282
|
+
Committing an already-checked tree also moves the hash, because the commit's tree object is the base of the fold; the next run does the work again. The reverse holds and is useful: a rebase or an amended message that preserves content leaves the hash alone, as does checking out a branch whose tree is identical.
|
|
283
|
+
|
|
284
|
+
### When the gate stands aside
|
|
285
|
+
|
|
286
|
+
The gate never wrongly skips. Where it cannot be sure, it does nothing and the command runs:
|
|
287
|
+
|
|
288
|
+
- Outside a git repository, when git fails, or when there is no commit at `HEAD`.
|
|
289
|
+
- When the repository declares or contains submodules, whose content the hash does not cover.
|
|
290
|
+
- When the monorepo root is not the git toplevel.
|
|
291
|
+
- When a changed path cannot be read, or is neither a file nor a symlink (an untracked nested repository, for instance).
|
|
292
|
+
- When there is no pnpm install to fingerprint.
|
|
293
|
+
- Under a `devBin` substitution, which runs a binary built from somewhere the hash does not describe.
|
|
294
|
+
- For any invocation carrying arguments after the command name.
|
|
295
|
+
|
|
296
|
+
`NMR_DEBUG=1` reports why a run did not skip and why the gate stood aside.
|
|
297
|
+
|
|
298
|
+
### Bypassing and clearing
|
|
299
|
+
|
|
300
|
+
Reach for these in order; the first is almost always the right one.
|
|
301
|
+
|
|
302
|
+
| Step | Effect |
|
|
303
|
+
| -------------------------------------- | ------------------------------------------------------------------------- |
|
|
304
|
+
| `nmr --no-cache <command>` | Runs the command, then records the fresh result. Reaches the whole chain. |
|
|
305
|
+
| `NMR_NO_CACHE=1` | The same, for every nmr invocation in the shell. |
|
|
306
|
+
| `rm -rf node_modules/.cache/nmr-check` | Forgets every recorded pass, leaving build output alone. |
|
|
307
|
+
| `nmr clean` | Forgets every recorded pass and removes build output, at any scope. |
|
|
308
|
+
|
|
309
|
+
`--no-cache` belongs before the command name. After it, it is an argument to the command rather than a flag to nmr; nmr says so rather than letting the bypass silently not happen.
|
|
310
|
+
|
|
311
|
+
### Reserved environment variables
|
|
312
|
+
|
|
313
|
+
`NMR_TREE_SNAPSHOT` is nmr's own: it carries one observation of the tree from a top-level invocation down to the processes it spawns, so a chain hashes the tree once rather than at every link. nmr trusts an inherited value only while `HEAD` still stands where it did when the observation was taken, which bounds a process that outlives the run that spawned it. That bound does not extend to a tree edited without committing, so **a process that survives its run and later invokes nmr should clear `NMR_TREE_SNAPSHOT`** — a test suite that shells out to `nmr` is the case worth checking.
|
|
314
|
+
|
|
315
|
+
### Configuring
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
export default defineConfig({
|
|
319
|
+
checkCache: {
|
|
320
|
+
// Names promising exit-status-only semantics through their whole chain.
|
|
321
|
+
extraCommands: ['verify:contracts'],
|
|
322
|
+
// Names that turned out to do more than report an exit status.
|
|
323
|
+
excludeCommands: ['test:coverage'],
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
`extraCommands` extends the default set rather than replacing it, so naming one command cannot silently drop the rest; `excludeCommands` is applied afterwards, so a name in both is excluded. `enabled: false` turns the gate off entirely. The key is `checkCache`, in the monorepo-root config.
|
|
329
|
+
|
|
200
330
|
## Dependency upgrades
|
|
201
331
|
|
|
202
332
|
`nmr upgrade` reports the dependency upgrades available to your repo. What it covers depends on where you run it:
|
|
@@ -245,58 +375,65 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
|
|
|
245
375
|
|
|
246
376
|
### Workspace scripts
|
|
247
377
|
|
|
248
|
-
| Command
|
|
249
|
-
|
|
|
250
|
-
| `build`
|
|
251
|
-
| `check`
|
|
252
|
-
| `check:strict`
|
|
253
|
-
| `clean`
|
|
254
|
-
| `compile`
|
|
255
|
-
| `fix`
|
|
256
|
-
| `fix:check`
|
|
257
|
-
| `fmt`
|
|
258
|
-
| `fmt:check`
|
|
259
|
-
| `lint`
|
|
260
|
-
| `lint:check`
|
|
261
|
-
| `lint:strict`
|
|
262
|
-
| `test`
|
|
263
|
-
| `test:all`
|
|
264
|
-
| `test:coverage`
|
|
265
|
-
| `test:
|
|
266
|
-
| `test:
|
|
267
|
-
| `
|
|
268
|
-
| `
|
|
269
|
-
| `
|
|
378
|
+
| Command | Runs |
|
|
379
|
+
| --------------- | ----------------------------------------------------------- |
|
|
380
|
+
| `build` | `compile` |
|
|
381
|
+
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
382
|
+
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage` |
|
|
383
|
+
| `clean` | `nmr-clean` |
|
|
384
|
+
| `compile` | `nmr-compile` |
|
|
385
|
+
| `fix` | `lint`, `fmt` |
|
|
386
|
+
| `fix:check` | `fmt:check`, `lint:check` |
|
|
387
|
+
| `fmt` | `nmr-fmt --write` |
|
|
388
|
+
| `fmt:check` | `nmr-fmt --check` |
|
|
389
|
+
| `lint` | `eslint --fix .` |
|
|
390
|
+
| `lint:check` | `eslint .` |
|
|
391
|
+
| `lint:strict` | `strict-lint` |
|
|
392
|
+
| `test` | `pnpm exec vitest --project unit --project tool` |
|
|
393
|
+
| `test:all` | `pnpm exec vitest` |
|
|
394
|
+
| `test:coverage` | `pnpm exec vitest --project unit --project tool --coverage` |
|
|
395
|
+
| `test:tool` | `pnpm exec vitest --project tool` |
|
|
396
|
+
| `test:unit` | `pnpm exec vitest --project unit` |
|
|
397
|
+
| `test:watch` | `pnpm exec vitest --project unit --project tool --watch` |
|
|
398
|
+
| `typecheck` | `tsgo --noEmit` |
|
|
399
|
+
| `upgrade` | `nmr-taze --include-locked` |
|
|
400
|
+
| `view-coverage` | `open coverage/index.html` |
|
|
270
401
|
|
|
271
402
|
`fmt` and `fmt:check` select their files through git rather than by walking the directory, which is what makes a package-level run and a root-level run apply the same ignore rules — see [file selection](#file-selection).
|
|
272
403
|
|
|
273
404
|
#### Test selections
|
|
274
405
|
|
|
275
|
-
Every package resolves the same
|
|
406
|
+
Every package resolves the same six test commands. Nothing is detected on disk: the commands select [Vitest projects](#shared-vitest-config), so a package separates its tool-tier tests by naming them `*.tool.test.ts`, not by carrying extra config files.
|
|
276
407
|
|
|
277
|
-
| To run
|
|
278
|
-
|
|
|
279
|
-
| Everything
|
|
280
|
-
|
|
|
281
|
-
|
|
|
408
|
+
| To run | Command |
|
|
409
|
+
| --------------------------------------------- | ----------- |
|
|
410
|
+
| Everything that runs on a bare install | `test` |
|
|
411
|
+
| The fastest tier alone | `test:unit` |
|
|
412
|
+
| Tests reaching a program alone | `test:tool` |
|
|
413
|
+
| Every tier, including those needing a service | `test:all` |
|
|
282
414
|
|
|
283
|
-
`test`
|
|
415
|
+
`test` names the tiers it runs rather than negating the ones it skips, so a tier added in a later release is opt-in rather than joining the default run on arrival. It covers `unit` and `tool`, the two that need nothing beyond a checkout and an install; `localhost` and `remote` are reached only through `test:all` or an explicit `--project`.
|
|
284
416
|
|
|
285
|
-
`test:coverage` and `test:watch` are the same selection as `test` in a different mode. There are no `test:
|
|
417
|
+
`test:coverage` and `test:watch` are the same selection as `test` in a different mode. There are no `test:tool --coverage` equivalents because none are needed: everything after a command name is forwarded untouched, so `nmr test:tool --coverage` already works.
|
|
286
418
|
|
|
287
|
-
To narrow to a single project, go through `test:all`: `nmr test:all --project
|
|
419
|
+
To narrow to a single project, go through `test:all`: `nmr test:all --project remote`. Narrowing through `test` does not work, because a second `--project` widens the selection rather than restricting it.
|
|
288
420
|
|
|
289
|
-
A run that collects no test files passes. That is what lets `nmr test:
|
|
421
|
+
A run that collects no test files passes. That is what lets `nmr test:tool` fan out across a monorepo in which most packages hold no tool-tier tests, and it means a package with no tests at all needs no `"test": ""` override.
|
|
290
422
|
|
|
291
423
|
### Root scripts
|
|
292
424
|
|
|
293
425
|
#### Build and CI
|
|
294
426
|
|
|
295
|
-
| Command
|
|
296
|
-
|
|
|
297
|
-
| `build`
|
|
298
|
-
| `ci`
|
|
299
|
-
| `clean`
|
|
427
|
+
| Command | Runs |
|
|
428
|
+
| --------- | --------------------------------- |
|
|
429
|
+
| `build` | `pnpm --recursive exec nmr build` |
|
|
430
|
+
| `ci` | `build`, `check:strict` |
|
|
431
|
+
| `clean` | `nmr-clean` |
|
|
432
|
+
| `prepush` | `ci`, `audit` |
|
|
433
|
+
|
|
434
|
+
`ci` is what a code-quality workflow runs; it leaves out the audit, which belongs in a workflow of its own. `prepush` is what a developer runs before pushing: both gates. It composes `ci` rather than restating its stages, so a stage added to `ci` joins the pre-push run too.
|
|
435
|
+
|
|
436
|
+
Neither is bound to a git hook. `prepush` is named for when you run it, not for a hook nmr installs.
|
|
300
437
|
|
|
301
438
|
#### Check and quality
|
|
302
439
|
|
|
@@ -314,15 +451,16 @@ A run that collects no test files passes. That is what lets `nmr test:integratio
|
|
|
314
451
|
|
|
315
452
|
#### Test
|
|
316
453
|
|
|
317
|
-
The same
|
|
454
|
+
The same six names the workspace registry carries, so a command means the same thing from the monorepo root as from inside a package. Each fans out to root-level files and every workspace package.
|
|
318
455
|
|
|
319
|
-
| Command
|
|
320
|
-
|
|
|
321
|
-
| `test`
|
|
322
|
-
| `test:all`
|
|
323
|
-
| `test:coverage`
|
|
324
|
-
| `test:
|
|
325
|
-
| `test:
|
|
456
|
+
| Command | Runs |
|
|
457
|
+
| --------------- | ----------------------------------------------------------- |
|
|
458
|
+
| `test` | `nmr root:test && pnpm --recursive exec nmr test` |
|
|
459
|
+
| `test:all` | `nmr root:test:all && pnpm --recursive exec nmr test:all` |
|
|
460
|
+
| `test:coverage` | `nmr root:test && pnpm --recursive exec nmr test:coverage` |
|
|
461
|
+
| `test:tool` | `nmr root:test:tool && pnpm --recursive exec nmr test:tool` |
|
|
462
|
+
| `test:unit` | `nmr root:test:unit && pnpm --recursive exec nmr test:unit` |
|
|
463
|
+
| `test:watch` | `vitest --project unit --project tool --watch` |
|
|
326
464
|
|
|
327
465
|
`test:coverage` chains `root:test` rather than a `root:test:coverage`, because the root config reports no coverage of its own; packages cover their own sources.
|
|
328
466
|
|
|
@@ -375,17 +513,18 @@ See [dependency upgrades](#dependency-upgrades) for the workflow and its configu
|
|
|
375
513
|
|
|
376
514
|
These scripts operate on root-level code only (not workspace packages):
|
|
377
515
|
|
|
378
|
-
| Command
|
|
379
|
-
|
|
|
380
|
-
| `root:check`
|
|
381
|
-
| `root:lint`
|
|
382
|
-
| `root:lint:check`
|
|
383
|
-
| `root:lint:strict`
|
|
384
|
-
| `root:test`
|
|
385
|
-
| `root:test:all`
|
|
386
|
-
| `root:test:
|
|
387
|
-
| `root:
|
|
388
|
-
| `root:
|
|
516
|
+
| Command | Runs |
|
|
517
|
+
| ------------------ | ----------------------------------------------------------------------- |
|
|
518
|
+
| `root:check` | `root:typecheck`, `fmt:check`, `root:lint:check`, `root:test` |
|
|
519
|
+
| `root:lint` | `eslint --fix --ignore-pattern 'packages/**' .` |
|
|
520
|
+
| `root:lint:check` | `eslint --ignore-pattern 'packages/**' .` |
|
|
521
|
+
| `root:lint:strict` | `strict-lint --ignore-pattern 'packages/**' .` |
|
|
522
|
+
| `root:test` | `vitest --config ./vitest.root.config.ts --project unit --project tool` |
|
|
523
|
+
| `root:test:all` | `vitest --config ./vitest.root.config.ts` |
|
|
524
|
+
| `root:test:tool` | `vitest --config ./vitest.root.config.ts --project tool` |
|
|
525
|
+
| `root:test:unit` | `vitest --config ./vitest.root.config.ts --project unit` |
|
|
526
|
+
| `root:typecheck` | `tsgo --noEmit` |
|
|
527
|
+
| `root:upgrade` | `nmr-taze --include-locked` |
|
|
389
528
|
|
|
390
529
|
#### Utilities
|
|
391
530
|
|
|
@@ -404,14 +543,15 @@ nmr [flags] <command> [args...]
|
|
|
404
543
|
|
|
405
544
|
Position determines ownership: flags before the command name are nmr's own, and everything after the command name is forwarded untouched to the resolved command.
|
|
406
545
|
|
|
407
|
-
| Flag | Description
|
|
408
|
-
| ------------------------ |
|
|
409
|
-
| `-F, --filter <pattern>` | Run command in matching packages
|
|
410
|
-
| `-R, --recursive` | Run command in all packages
|
|
411
|
-
| `-w, --workspace-root` | Use root scripts, running at the monorepo root
|
|
412
|
-
| `-q, --quiet` | Suppress info messages; show full output on failure
|
|
413
|
-
|
|
|
414
|
-
|
|
|
546
|
+
| Flag | Description | Default |
|
|
547
|
+
| ------------------------ | ------------------------------------------------------- | ------- |
|
|
548
|
+
| `-F, --filter <pattern>` | Run command in matching packages | — |
|
|
549
|
+
| `-R, --recursive` | Run command in all packages | — |
|
|
550
|
+
| `-w, --workspace-root` | Use root scripts, running at the monorepo root | — |
|
|
551
|
+
| `-q, --quiet` | Suppress info messages; show full output on failure | — |
|
|
552
|
+
| `--no-cache` | Run even if this tree already passed; record the result | — |
|
|
553
|
+
| `-?, --help` | Show available commands | — |
|
|
554
|
+
| `-V, --version` | Show version number | — |
|
|
415
555
|
|
|
416
556
|
### Examples
|
|
417
557
|
|
|
@@ -421,8 +561,9 @@ nmr test # Run workspace test script
|
|
|
421
561
|
nmr build # Compile to .js and .d.ts in one pass
|
|
422
562
|
|
|
423
563
|
# From the monorepo root
|
|
424
|
-
nmr test
|
|
425
|
-
nmr ci
|
|
564
|
+
nmr test # Root tests + recursive workspace tests
|
|
565
|
+
nmr ci # build + check:strict
|
|
566
|
+
nmr prepush # ci + audit
|
|
426
567
|
|
|
427
568
|
# Target specific packages
|
|
428
569
|
nmr --filter core test # Test only the core package
|
|
@@ -488,6 +629,10 @@ nmr-clean
|
|
|
488
629
|
|
|
489
630
|
Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` and `.d.ts` in one pass. 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 (a content-and-path hash 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.
|
|
490
631
|
|
|
632
|
+
**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.
|
|
633
|
+
|
|
634
|
+
**The build owns its output directory.** Every rebuild wipes `dist/esm` before emitting, so the directory holds exactly the current emit: a source that was deleted, or that a widened ignore set now covers, leaves nothing behind. Assets therefore belong outside the output directory, or in a `build:post` hook, which runs after the wipe and so composes correctly. Note that a bare `nmr compile` runs no hook, so assets a `build:post` step copies in are absent until a full `nmr build` restores them. An `outdir` that does not resolve inside the package is rejected rather than wiped.
|
|
635
|
+
|
|
491
636
|
`typescript` is a peer dependency (`>=5.7.0 <7`); the consuming repo provides it. The floor is what `rewriteRelativeImportExtensions` requires; the ceiling is because TypeScript 7 ships no compiler API — its root export is a version constant, so `nmr-compile` cannot run on it. Relative imports in source must carry explicit `.ts` extensions for them to be rewritten.
|
|
492
637
|
|
|
493
638
|
```bash
|
|
@@ -535,6 +680,29 @@ ensure-prepublish-hooks
|
|
|
535
680
|
| `--dry-run` | Preview what `--fix` would do | — |
|
|
536
681
|
| `--command <command>` | Custom hook command | `"npm run build"` |
|
|
537
682
|
|
|
683
|
+
## Conformance checks
|
|
684
|
+
|
|
685
|
+
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, and the [test-tier convention](#test-tiers). 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.
|
|
686
|
+
|
|
687
|
+
Add `readyup` as a devDependency, then name nmr in its config:
|
|
688
|
+
|
|
689
|
+
```ts
|
|
690
|
+
// .config/readyup.config.ts
|
|
691
|
+
import { defineRdyConfig } from 'readyup';
|
|
692
|
+
|
|
693
|
+
export default defineRdyConfig({
|
|
694
|
+
packages: ['@williamthorsen/nmr'],
|
|
695
|
+
});
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
```bash
|
|
699
|
+
rdy run --packages # every kit each listed package publishes
|
|
700
|
+
rdy run --from npm:@williamthorsen/nmr # nmr's kit alone, without the config entry
|
|
701
|
+
rdy list --from npm:@williamthorsen/nmr # what nmr publishes
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
`--packages` is the form that survives nmr publishing further kits. Both need `readyup` 0.23 or later, and `@williamthorsen/nmr` as a _direct_ devDependency: a strict pnpm layout links nothing else into the project, so a transitive copy is unreachable.
|
|
705
|
+
|
|
538
706
|
## Consumer migration
|
|
539
707
|
|
|
540
708
|
After installing, a consuming repo's root `package.json` scripts shrink to lifecycle hooks:
|
|
@@ -638,29 +806,42 @@ export default defineVitestConfig();
|
|
|
638
806
|
|
|
639
807
|
`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.
|
|
640
808
|
|
|
641
|
-
### Test
|
|
809
|
+
### Test tiers
|
|
642
810
|
|
|
643
|
-
The config declares
|
|
811
|
+
The config declares four projects, an ordered ladder named for the furthest thing a test reaches:
|
|
644
812
|
|
|
645
|
-
| Project
|
|
646
|
-
|
|
|
647
|
-
| `unit`
|
|
648
|
-
| `
|
|
649
|
-
| `
|
|
813
|
+
| Project | Matches | Reaches |
|
|
814
|
+
| ----------- | -------------------------------------- | ---------------------------------------------------- |
|
|
815
|
+
| `unit` | every test file the others don't claim | nothing outside the package's own dependency closure |
|
|
816
|
+
| `tool` | `*.tool.test.{ts,tsx}` | a program the environment must supply |
|
|
817
|
+
| `localhost` | `*.localhost.test.{ts,tsx}` | a service running on this machine |
|
|
818
|
+
| `remote` | `*.remote.test.{ts,tsx}` | a machine that isn't this one |
|
|
650
819
|
|
|
651
|
-
All
|
|
820
|
+
All four match only under a `__tests__` directory. Select them at run time with `--project`, which unions when repeated and accepts negation:
|
|
652
821
|
|
|
653
822
|
```bash
|
|
654
|
-
vitest --project
|
|
655
|
-
vitest --project
|
|
656
|
-
vitest
|
|
823
|
+
vitest --project unit --project tool # everything that runs on a bare install
|
|
824
|
+
vitest --project tool # tests reaching a program, alone
|
|
825
|
+
vitest # every project
|
|
657
826
|
```
|
|
658
827
|
|
|
659
|
-
|
|
828
|
+
**A tier names what a test reaches, never how it invokes it.** A test driving a compiler through its JavaScript API is `tool`, exactly as one spawning the compiler binary is; whether a program ships as a library or an executable is an accident of packaging, not a property of the test. A package's `peerDependencies`, plus whatever binaries it spawns, are a good statement of where its `tool` boundary sits: a bundled dependency ships inside the package's own closure and is always present, so reaching one is `unit`.
|
|
829
|
+
|
|
830
|
+
**A tier is not a statement about preconditions.** A `unit` test may still need something set up before it runs: a build, a generated fixture, a seeded file. What makes it `unit` is that it reaches nothing beyond the test process _while running_. The filesystem sits in `unit` for the same reason the tiers exist: they gate on what must be available, and the filesystem always is.
|
|
831
|
+
|
|
832
|
+
**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`.
|
|
833
|
+
|
|
834
|
+
`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, which is the only thing that does.
|
|
835
|
+
|
|
836
|
+
`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.
|
|
837
|
+
|
|
838
|
+
Every tier above `unit` carries a 30-second `testTimeout` and `hookTimeout`, where `unit` keeps Vitest's defaults of 5 and 10 seconds. A tier test waits on something it doesn't control, and coverage instrumentation multiplies that wait, so the defaults turn a green suite flaky the moment `nmr test:coverage` collects it. Both budgets move together because a tier that scaffolds in `beforeAll` moves that wait out from under `testTimeout` entirely, where raising the test budget alone never reaches it. `unit` keeps the tight budgets, which is what makes a hung unit test fail fast.
|
|
839
|
+
|
|
840
|
+
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.
|
|
660
841
|
|
|
661
842
|
### Customizing by scope
|
|
662
843
|
|
|
663
|
-
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
|
|
844
|
+
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:
|
|
664
845
|
|
|
665
846
|
```ts
|
|
666
847
|
export default defineVitestConfig({
|
|
@@ -668,16 +849,57 @@ export default defineVitestConfig({
|
|
|
668
849
|
root: { resolve: { conditions: ['development'] } },
|
|
669
850
|
// Applied to every project.
|
|
670
851
|
project: { setupFiles: ['./vitest.setup.ts'] },
|
|
852
|
+
// Applied to one tier, after the `project` block above.
|
|
853
|
+
tiers: { tool: { testTimeout: 120_000 } },
|
|
671
854
|
});
|
|
672
855
|
```
|
|
673
856
|
|
|
674
|
-
`root` is typed to accept only the options that work at the root, so writing a per-project option there is a compile error rather than a setting that never runs.
|
|
857
|
+
`root` is typed to accept only the options that work at the root, so writing a per-project option there is a compile error rather than a setting that never runs. Every surface merges into the generated config rather than replacing it, so overriding one coverage field leaves the rest intact.
|
|
858
|
+
|
|
859
|
+
`tiers` is keyed by tier name and reaches all four, `unit` included. A key naming no tier throws and names the valid ones: ignoring it would leave the suite green on the budget the key failed to change, which nothing in the run reports. A tier target sets whichever keys it names and no others, so raising `testTimeout` alone leaves that tier's `hookTimeout` at 30 seconds.
|
|
860
|
+
|
|
861
|
+
Arrays concatenate rather than replace. `exclude` and `setupFiles` therefore add to what the config already declares, and no surface can narrow `include` or drop a default exclusion. Adding an `include` pattern through `project` widens all four projects at once, so a file matching it is collected by each and runs four times.
|
|
862
|
+
|
|
863
|
+
`resolve.conditions` concatenates too, but layer order carries no meaning there: Vite consumes conditions as a set, and which one wins is decided by the key order of the consumed package's own `exports`. A later layer can add a condition and can never remove or outrank one an earlier layer contributed. `resolve.alias` is the one key that merges override-first, so a later alias takes precedence over an earlier one.
|
|
864
|
+
|
|
865
|
+
### Sharing options across config files
|
|
866
|
+
|
|
867
|
+
Vitest resolves one config per run, so a package that adds its own `vitest.config.ts` stops seeing the repo's root config entirely -- not the one setting it meant to change, all of them. Pass the shared settings as a layer instead:
|
|
868
|
+
|
|
869
|
+
```ts
|
|
870
|
+
// vitest.shared.ts
|
|
871
|
+
import { fileURLToPath } from 'node:url';
|
|
872
|
+
|
|
873
|
+
import type { VitestConfigOptions } from '@williamthorsen/nmr/vitest';
|
|
874
|
+
|
|
875
|
+
export const shared: VitestConfigOptions = {
|
|
876
|
+
root: { resolve: { conditions: ['source'] } },
|
|
877
|
+
project: { setupFiles: [fileURLToPath(new URL('./vitest.setup.ts', import.meta.url))] },
|
|
878
|
+
};
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
```ts
|
|
882
|
+
// packages/web/vitest.config.ts
|
|
883
|
+
import { defineVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
675
884
|
|
|
676
|
-
|
|
885
|
+
import { shared } from '../../vitest.shared.ts';
|
|
886
|
+
|
|
887
|
+
export default defineVitestConfig(shared, { project: { environment: 'jsdom' } });
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
Both factories fold any number of layers left to right: a later layer wins on a scalar, arrays concatenate in layer order, and an `undefined` layer is skipped, so `defineVitestConfig(shared, isCI ? ciLayer : undefined)` composes without a spread. `defineRootVitestConfig` takes the same layers, with `monorepoRoot` on the last one -- the config file's own, the only place `import.meta.dirname` names this repo.
|
|
891
|
+
|
|
892
|
+
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.
|
|
893
|
+
|
|
894
|
+
**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.
|
|
895
|
+
|
|
896
|
+
**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.
|
|
897
|
+
|
|
898
|
+
A config file that omits the shared layer still loses those settings, silently -- Vitest's own resolution contract, not something the factory can intercept. Guard it with a test that fails when a package's suite goes missing, which also catches a shared `exclude` pattern swallowing one package's tests.
|
|
677
899
|
|
|
678
900
|
### What the config excludes
|
|
679
901
|
|
|
680
|
-
Collection skips `**/node_modules/**`, `**/.git/**`, and `**/dist/**`. Coverage skips `**/__{fixtures,mocks,tests}__/**`, `**/index.ts`,
|
|
902
|
+
Collection skips `**/node_modules/**`, `**/.git/**`, and `**/dist/**`. Coverage skips `**/__{fixtures,mocks,tests}__/**`, `**/index.ts`, and `**/*.d.ts`.
|
|
681
903
|
|
|
682
904
|
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.
|
|
683
905
|
|
|
@@ -702,24 +924,31 @@ import { defineRootVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
|
702
924
|
export default defineRootVitestConfig({ monorepoRoot: import.meta.dirname });
|
|
703
925
|
```
|
|
704
926
|
|
|
705
|
-
This variant reads `pnpm-workspace.yaml` and excludes every workspace package from all
|
|
927
|
+
This variant reads `pnpm-workspace.yaml` and excludes every workspace package from all four projects, so a root run covers only root-level files. It reports no coverage of its own — packages cover their own sources.
|
|
706
928
|
|
|
707
929
|
`monorepoRoot` is required, and because the config sits at the monorepo root, it is always `import.meta.dirname`. Stating the root rather than searching for it is what makes the exclusions describe this repo: a search from the working directory would resolve whichever monorepo the run started in, and every project is then pinned to that one. A directory holding no `pnpm-workspace.yaml` throws, naming the directory.
|
|
708
930
|
|
|
931
|
+
### Migrating to the isolation tiers
|
|
932
|
+
|
|
933
|
+
The projects were once named `unit`, `integration`, and `app`, for what a test _covered_ rather than what it _reached_. `integration` and `app` are gone.
|
|
934
|
+
|
|
935
|
+
1. Rename every `*.int.test.ts`. A test that reaches a program the environment supplies becomes `*.tool.test.ts`; one that reaches nothing beyond the test process becomes `*.unit.test.ts`. Re-read each file rather than renaming in bulk -- the axis changed, so the old bucket does not map onto one new tier.
|
|
936
|
+
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`.
|
|
937
|
+
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.
|
|
938
|
+
|
|
939
|
+
**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; a passing test run does not. Run it before treating the migration as done.
|
|
940
|
+
|
|
941
|
+
`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.
|
|
942
|
+
|
|
709
943
|
### Migrating from the config-file variants
|
|
710
944
|
|
|
711
|
-
nmr once selected a package's test scripts by looking for a `vitest.integration.config.ts` on disk, which meant three config files per package that separated its integration tests. Those files are no longer consulted.
|
|
945
|
+
nmr once selected a package's test scripts by looking for a `vitest.integration.config.ts` on disk, which meant three config files per package that separated its integration tests. Those files are no longer consulted.
|
|
712
946
|
|
|
713
947
|
1. Replace the repo's root `vitest.config.ts` with `defineVitestConfig()`, and its root-scoped config with `defineRootVitestConfig({ monorepoRoot: import.meta.dirname })`.
|
|
714
948
|
2. Delete every `vitest.integration.config.ts` and `vitest.standalone.config.ts`, plus any per-package `vitest.config.ts` that only re-exports an ancestor. Vitest resolves config by walking up from the run root, so those are redundant.
|
|
715
|
-
3. Rename
|
|
949
|
+
3. Rename the tests onto the tiers, per the section above.
|
|
716
950
|
4. Drop any hand-copied `test*` entries from `package.json`, which now shadow the defaults.
|
|
717
951
|
|
|
718
|
-
Until step 1 is done,
|
|
719
|
-
|
|
720
|
-
| Command | Result |
|
|
721
|
-
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
722
|
-
| `nmr test` / `test:coverage` | **Succeeds, and runs integration tests too.** The separation is lost with no error, because `--project '!integration'` matches nothing to exclude. |
|
|
723
|
-
| `nmr test:integration` | **Fails** with `No projects matched the filter "integration"`. |
|
|
952
|
+
Until step 1 is done, every test command that names a tier fails against a config declaring no projects, with `Error: No projects matched the filter "unit", "tool".` at startup. `test:all` is the exception: it names no project, so it runs everything the config collects.
|
|
724
953
|
|
|
725
|
-
|
|
954
|
+
That is a change for the better. While `test` selected by negation, an unmigrated config produced a _green_ run, because `--project '!integration'` had nothing to exclude, so the separation was lost with no error anywhere. Positive selection turns the same state into a startup failure that names the missing projects.
|