@williamthorsen/nmr 0.23.0 → 0.24.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/AGENTS.md +3 -1
- package/README.md +344 -113
- package/dist/esm/check-cache.d.ts +76 -0
- package/dist/esm/check-cache.js +236 -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/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/types.d.ts +15 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/vitest.d.ts +7 -2
- package/dist/esm/vitest.js +53 -29
- package/package.json +5 -6
- package/dist/esm/index.d.ts +0 -2
- package/dist/esm/index.js +0 -1
package/README.md
CHANGED
|
@@ -3,19 +3,47 @@
|
|
|
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.24.0 (2026-08-03)
|
|
7
7
|
|
|
8
8
|
### 🎉 Features
|
|
9
9
|
|
|
10
|
-
- 🚨 **Breaking:**
|
|
10
|
+
- 🚨 **Breaking:** Retire integration label in favor of an isolation-tier test ladder (#552)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Test groups are now named for what a test reaches rather than for how much of the codebase it covers: `integration` and `app` are replaced by `unit`, `tool`, `localhost`, and `remote`.
|
|
13
|
+
|
|
14
|
+
`nmr test` now runs the `tool` group alongside `unit`, so the first run after upgrading may fail on tests that nothing was running before. Upgrading means renaming `*.int.test.ts` files onto the new groups and replacing `nmr test:integration` with `nmr test:tool`.
|
|
15
|
+
|
|
16
|
+
- Let Vitest config files compose shared options and target one tier (#556)
|
|
17
|
+
|
|
18
|
+
Vitest settings can now be shared across config files: A repo declares its common options once, and a workspace can use its own config file to supplement or override them. Shared setup files run first. An option can now target a single test tier instead of all tiers, allowing per-tier timeouts to be set.
|
|
19
|
+
|
|
20
|
+
- 🚨 **Breaking:** Redefine ci as the code-quality gate and add prepush (#559)
|
|
21
|
+
|
|
22
|
+
Repurposes the `nmr ci` command to align with its name; it now runs the code-quality checks that run in CI: `nmr build && nmr check:strict`. A new `nmr prepush` command, combining code-quality checks and the security audit, takes the place of `nmr ci`. nmr's readyup kit is adjusted to account for the changes.
|
|
23
|
+
|
|
24
|
+
- Exclude test scaffolding from builds and make the ignore set configurable (#564)
|
|
25
|
+
|
|
26
|
+
The `build` command now ignores the directories typically used for test scaffolding, such as fixtures and mocks. The exclusion includes `test-utils/`, which is treated as the conventional name for a directory containing test utilities. A package can declare additional ignore patterns in its config file.
|
|
27
|
+
|
|
28
|
+
- Skip checks that already passed on the current working tree (#568)
|
|
29
|
+
|
|
30
|
+
`nmr` commands now skip any pure check (types, lint, tests) that has already passed on an unchanged working tree; instead, success is reported immediately. `nmr --no-cache` causes the cache check to be skipped, and `NMR_NO_CACHE=1` does the same for a whole shell. Cached results are cleared by `nmr clean`. Which commands can skip is configurable at the monorepo root, and `NMR_DEBUG=1` reports why a run didn't skip.
|
|
31
|
+
|
|
32
|
+
### 🪦 Removed
|
|
33
|
+
|
|
34
|
+
- 🚨 **Breaking:** Remove the package root entry in favor of /config (#569)
|
|
35
|
+
|
|
36
|
+
`defineConfig` is now imported from `@williamthorsen/nmr/config` instead of the bare package.
|
|
13
37
|
|
|
14
38
|
### 🐛 Bug fixes
|
|
15
39
|
|
|
16
|
-
- Stop
|
|
40
|
+
- Stop excluding *.types.ts and mock* files from coverage
|
|
41
|
+
|
|
42
|
+
Coverage now measures modules matching `*.types.ts` and `mock*`, which the shared Vitest config previously excluded. A type module that grows a type guard, or a mock helper placed in `test-utils/` where helpers are meant to stay measured, was left out of the report with nothing saying so, and because exclusions concatenate, no consuming repo could add it back.
|
|
17
43
|
|
|
18
|
-
|
|
44
|
+
What remains excluded names something that cannot hold runtime code by construction: a directory of test scaffolding, a barrel, and a declaration file. A pattern resting on a filename alone suppresses nothing it could claim to, since a file with no executable statements is absent from the report either way.
|
|
45
|
+
|
|
46
|
+
A repo enforcing its own coverage thresholds may see a lower figure once these files enter the denominator.
|
|
19
47
|
<!-- /section:release-notes -->
|
|
20
48
|
|
|
21
49
|
## Installation
|
|
@@ -50,9 +78,9 @@ nmr check # Typecheck, format check, lint check, and tests
|
|
|
50
78
|
From the monorepo root:
|
|
51
79
|
|
|
52
80
|
```bash
|
|
53
|
-
nmr test
|
|
54
|
-
nmr build
|
|
55
|
-
nmr
|
|
81
|
+
nmr test # Run root tests + recursive workspace tests
|
|
82
|
+
nmr build # Build all packages
|
|
83
|
+
nmr prepush # Run everything the remote runs, before you push
|
|
56
84
|
```
|
|
57
85
|
|
|
58
86
|
nmr detects where you are and selects the right scripts automatically — see [context-aware resolution](#context-aware-resolution) below.
|
|
@@ -61,11 +89,11 @@ nmr detects where you are and selects the right scripts automatically — see [c
|
|
|
61
89
|
|
|
62
90
|
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
91
|
|
|
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`
|
|
92
|
+
| Where you run `nmr` | Registry used | Working directory | `nmr test` runs |
|
|
93
|
+
| --------------------------------- | ----------------- | ----------------- | -------------------------------------------------------------------- |
|
|
94
|
+
| Monorepo root | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test` |
|
|
95
|
+
| Inside a workspace package | Workspace scripts | The package root | `pnpm exec vitest --project unit --project tool` (that package only) |
|
|
96
|
+
| Anywhere, with `--workspace-root` | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test` |
|
|
69
97
|
|
|
70
98
|
Relative paths in a script resolve against that working directory, not the invocation directory.
|
|
71
99
|
|
|
@@ -116,10 +144,10 @@ Passthrough arguments attach to the final step alone, because the expansion is a
|
|
|
116
144
|
|
|
117
145
|
## Configuration
|
|
118
146
|
|
|
119
|
-
Create `.config/nmr.config.ts` in the monorepo root to add or override scripts:
|
|
147
|
+
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
148
|
|
|
121
149
|
```ts
|
|
122
|
-
import { defineConfig } from '@williamthorsen/nmr';
|
|
150
|
+
import { defineConfig } from '@williamthorsen/nmr/config';
|
|
123
151
|
|
|
124
152
|
export default defineConfig({
|
|
125
153
|
workspaceScripts: {
|
|
@@ -133,14 +161,35 @@ export default defineConfig({
|
|
|
133
161
|
|
|
134
162
|
### `defineConfig` fields
|
|
135
163
|
|
|
136
|
-
| Field | Type
|
|
137
|
-
| ------------------ |
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
164
|
+
| Field | Type | Description |
|
|
165
|
+
| ------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
166
|
+
| `build` | `{ extraIgnorePatterns?: string[] }` | Patterns added to the build's ignore set (package config only) |
|
|
167
|
+
| `checkCache` | `{ enabled?: boolean; extraCommands?: string[]; excludeCommands?: string[] }` | Which commands the [check-result cache](#check-result-cache) may skip |
|
|
168
|
+
| `workspaceScripts` | `Record<string, string \| string[]>` | Scripts added or overridden in the workspace registry (tier 2) |
|
|
169
|
+
| `rootScripts` | `Record<string, string \| string[]>` | Scripts added or overridden in the root registry (tier 2) |
|
|
170
|
+
| `devBin` | `Record<string, string>` | Map binary names to source-repo replacement commands |
|
|
141
171
|
|
|
142
172
|
All fields are optional. `workspaceScripts` and `rootScripts` values follow the same `string | string[]` convention described in [script values](#script-values).
|
|
143
173
|
|
|
174
|
+
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.
|
|
175
|
+
|
|
176
|
+
### Package-level configuration
|
|
177
|
+
|
|
178
|
+
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.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// packages/my-package/.config/nmr.config.ts
|
|
182
|
+
import { defineConfig } from '@williamthorsen/nmr/config';
|
|
183
|
+
|
|
184
|
+
export default defineConfig({
|
|
185
|
+
build: { extraIgnorePatterns: ['**/fixtures/**'] },
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`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.
|
|
190
|
+
|
|
191
|
+
> **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.
|
|
192
|
+
|
|
144
193
|
### `devBin` — source-repo binary substitution
|
|
145
194
|
|
|
146
195
|
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 +224,7 @@ Extend `nmr build` with a pre-build step for every workspace package:
|
|
|
175
224
|
|
|
176
225
|
```ts
|
|
177
226
|
// .config/nmr.config.ts
|
|
178
|
-
import { defineConfig } from '@williamthorsen/nmr';
|
|
227
|
+
import { defineConfig } from '@williamthorsen/nmr/config';
|
|
179
228
|
|
|
180
229
|
export default defineConfig({
|
|
181
230
|
workspaceScripts: {
|
|
@@ -197,6 +246,114 @@ Attach a post-build step to one package:
|
|
|
197
246
|
|
|
198
247
|
The second example calls the bin directly, which sidesteps the workspace-versus-root registry distinction.
|
|
199
248
|
|
|
249
|
+
## Check-result cache
|
|
250
|
+
|
|
251
|
+
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:
|
|
252
|
+
|
|
253
|
+
```console
|
|
254
|
+
$ nmr ci
|
|
255
|
+
# ... four minutes of build, typecheck, lint, and coverage ...
|
|
256
|
+
|
|
257
|
+
$ nmr ci
|
|
258
|
+
⏭️ ci: passed 2m ago on this tree (saved ~4m). Re-run with --no-cache.
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
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`.
|
|
262
|
+
|
|
263
|
+
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.
|
|
264
|
+
|
|
265
|
+
### What is cached
|
|
266
|
+
|
|
267
|
+
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`.
|
|
268
|
+
|
|
269
|
+
Everything else runs every time:
|
|
270
|
+
|
|
271
|
+
| Command | Why it is never skipped |
|
|
272
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
273
|
+
| `audit`, `prepush` | Consult a vulnerability database that moves without the tree. `prepush`'s `ci` constituent still skips while its `audit` runs every time. |
|
|
274
|
+
| `build`, `compile` | Carry a cache of their own. |
|
|
275
|
+
| `fix`, `fmt`, `lint`, `upgrade` | Rewrite the tree they were asked about, so a recorded pass would describe a tree that no longer exists. |
|
|
276
|
+
| `test:all` | Reaches the `localhost` and `remote` tiers, which the environment supplies rather than the tree. |
|
|
277
|
+
|
|
278
|
+
### The exit-status-only contract
|
|
279
|
+
|
|
280
|
+
**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.
|
|
281
|
+
|
|
282
|
+
`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.
|
|
283
|
+
|
|
284
|
+
Overriding a cacheable name, in `package.json` or in `workspaceScripts`, inherits its cacheability. An override that writes something anyone depends on belongs in `excludeCommands`.
|
|
285
|
+
|
|
286
|
+
### What the key is made of
|
|
287
|
+
|
|
288
|
+
A hit requires all of these to match the run that recorded the pass:
|
|
289
|
+
|
|
290
|
+
- **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.
|
|
291
|
+
- **The command string that would run**, hooks and all, and the scope it would run in.
|
|
292
|
+
- **nmr's version**, the **Node version**, and the **platform and architecture**.
|
|
293
|
+
- **What pnpm has installed**, so an install, a prune, or a lockfile change forces a re-run.
|
|
294
|
+
- **`TZ`, `LANG`, `LC_ALL`, and `NODE_OPTIONS`.** This set is fixed, so two machines that differ only in shell decoration still agree.
|
|
295
|
+
|
|
296
|
+
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.
|
|
297
|
+
|
|
298
|
+
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).
|
|
299
|
+
|
|
300
|
+
### Out of contract
|
|
301
|
+
|
|
302
|
+
These change what a check concludes without moving the hash. Where one applies, exclude the affected command:
|
|
303
|
+
|
|
304
|
+
- **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.
|
|
305
|
+
- **Symlinked inputs.** A symlink is hashed by the path it names, not by the content at the far end.
|
|
306
|
+
- **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.
|
|
307
|
+
- **`passWithNoTests`.** A suite that collects no files exits 0, and that green is recorded like any other.
|
|
308
|
+
|
|
309
|
+
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.
|
|
310
|
+
|
|
311
|
+
### When the gate stands aside
|
|
312
|
+
|
|
313
|
+
The gate never wrongly skips. Where it cannot be sure, it does nothing and the command runs:
|
|
314
|
+
|
|
315
|
+
- Outside a git repository, when git fails, or when there is no commit at `HEAD`.
|
|
316
|
+
- When the repository declares or contains submodules, whose content the hash does not cover.
|
|
317
|
+
- When the monorepo root is not the git toplevel.
|
|
318
|
+
- When a changed path cannot be read, or is neither a file nor a symlink (an untracked nested repository, for instance).
|
|
319
|
+
- When there is no pnpm install to fingerprint.
|
|
320
|
+
- Under a `devBin` substitution, which runs a binary built from somewhere the hash does not describe.
|
|
321
|
+
- For any invocation carrying arguments after the command name.
|
|
322
|
+
|
|
323
|
+
`NMR_DEBUG=1` reports why a run did not skip and why the gate stood aside.
|
|
324
|
+
|
|
325
|
+
### Bypassing and clearing
|
|
326
|
+
|
|
327
|
+
Reach for these in order; the first is almost always the right one.
|
|
328
|
+
|
|
329
|
+
| Step | Effect |
|
|
330
|
+
| -------------------------------------- | ------------------------------------------------------------------------- |
|
|
331
|
+
| `nmr --no-cache <command>` | Runs the command, then records the fresh result. Reaches the whole chain. |
|
|
332
|
+
| `NMR_NO_CACHE=1` | The same, for every nmr invocation in the shell. |
|
|
333
|
+
| `rm -rf node_modules/.cache/nmr-check` | Forgets every recorded pass, leaving build output alone. |
|
|
334
|
+
| `nmr clean` | Forgets every recorded pass and removes build output, at any scope. |
|
|
335
|
+
|
|
336
|
+
`--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.
|
|
337
|
+
|
|
338
|
+
### Reserved environment variables
|
|
339
|
+
|
|
340
|
+
`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.
|
|
341
|
+
|
|
342
|
+
### Configuring
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
export default defineConfig({
|
|
346
|
+
checkCache: {
|
|
347
|
+
// Names promising exit-status-only semantics through their whole chain.
|
|
348
|
+
extraCommands: ['verify:contracts'],
|
|
349
|
+
// Names that turned out to do more than report an exit status.
|
|
350
|
+
excludeCommands: ['test:coverage'],
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`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.
|
|
356
|
+
|
|
200
357
|
## Dependency upgrades
|
|
201
358
|
|
|
202
359
|
`nmr upgrade` reports the dependency upgrades available to your repo. What it covers depends on where you run it:
|
|
@@ -245,58 +402,65 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
|
|
|
245
402
|
|
|
246
403
|
### Workspace scripts
|
|
247
404
|
|
|
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
|
-
| `
|
|
405
|
+
| Command | Runs |
|
|
406
|
+
| --------------- | ----------------------------------------------------------- |
|
|
407
|
+
| `build` | `compile` |
|
|
408
|
+
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
409
|
+
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage` |
|
|
410
|
+
| `clean` | `nmr-clean` |
|
|
411
|
+
| `compile` | `nmr-compile` |
|
|
412
|
+
| `fix` | `lint`, `fmt` |
|
|
413
|
+
| `fix:check` | `fmt:check`, `lint:check` |
|
|
414
|
+
| `fmt` | `nmr-fmt --write` |
|
|
415
|
+
| `fmt:check` | `nmr-fmt --check` |
|
|
416
|
+
| `lint` | `eslint --fix .` |
|
|
417
|
+
| `lint:check` | `eslint .` |
|
|
418
|
+
| `lint:strict` | `strict-lint` |
|
|
419
|
+
| `test` | `pnpm exec vitest --project unit --project tool` |
|
|
420
|
+
| `test:all` | `pnpm exec vitest` |
|
|
421
|
+
| `test:coverage` | `pnpm exec vitest --project unit --project tool --coverage` |
|
|
422
|
+
| `test:tool` | `pnpm exec vitest --project tool` |
|
|
423
|
+
| `test:unit` | `pnpm exec vitest --project unit` |
|
|
424
|
+
| `test:watch` | `pnpm exec vitest --project unit --project tool --watch` |
|
|
425
|
+
| `typecheck` | `tsgo --noEmit` |
|
|
426
|
+
| `upgrade` | `nmr-taze --include-locked` |
|
|
427
|
+
| `view-coverage` | `open coverage/index.html` |
|
|
270
428
|
|
|
271
429
|
`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
430
|
|
|
273
431
|
#### Test selections
|
|
274
432
|
|
|
275
|
-
Every package resolves the same
|
|
433
|
+
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
434
|
|
|
277
|
-
| To run
|
|
278
|
-
|
|
|
279
|
-
| Everything
|
|
280
|
-
|
|
|
281
|
-
|
|
|
435
|
+
| To run | Command |
|
|
436
|
+
| --------------------------------------------- | ----------- |
|
|
437
|
+
| Everything that runs on a bare install | `test` |
|
|
438
|
+
| The fastest tier alone | `test:unit` |
|
|
439
|
+
| Tests reaching a program alone | `test:tool` |
|
|
440
|
+
| Every tier, including those needing a service | `test:all` |
|
|
282
441
|
|
|
283
|
-
`test`
|
|
442
|
+
`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
443
|
|
|
285
|
-
`test:coverage` and `test:watch` are the same selection as `test` in a different mode. There are no `test:
|
|
444
|
+
`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
445
|
|
|
287
|
-
To narrow to a single project, go through `test:all`: `nmr test:all --project
|
|
446
|
+
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
447
|
|
|
289
|
-
A run that collects no test files passes. That is what lets `nmr test:
|
|
448
|
+
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
449
|
|
|
291
450
|
### Root scripts
|
|
292
451
|
|
|
293
452
|
#### Build and CI
|
|
294
453
|
|
|
295
|
-
| Command
|
|
296
|
-
|
|
|
297
|
-
| `build`
|
|
298
|
-
| `ci`
|
|
299
|
-
| `clean`
|
|
454
|
+
| Command | Runs |
|
|
455
|
+
| --------- | --------------------------------- |
|
|
456
|
+
| `build` | `pnpm --recursive exec nmr build` |
|
|
457
|
+
| `ci` | `build`, `check:strict` |
|
|
458
|
+
| `clean` | `nmr-clean` |
|
|
459
|
+
| `prepush` | `ci`, `audit` |
|
|
460
|
+
|
|
461
|
+
`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.
|
|
462
|
+
|
|
463
|
+
Neither is bound to a git hook. `prepush` is named for when you run it, not for a hook nmr installs.
|
|
300
464
|
|
|
301
465
|
#### Check and quality
|
|
302
466
|
|
|
@@ -314,15 +478,16 @@ A run that collects no test files passes. That is what lets `nmr test:integratio
|
|
|
314
478
|
|
|
315
479
|
#### Test
|
|
316
480
|
|
|
317
|
-
The same
|
|
481
|
+
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
482
|
|
|
319
|
-
| Command
|
|
320
|
-
|
|
|
321
|
-
| `test`
|
|
322
|
-
| `test:all`
|
|
323
|
-
| `test:coverage`
|
|
324
|
-
| `test:
|
|
325
|
-
| `test:
|
|
483
|
+
| Command | Runs |
|
|
484
|
+
| --------------- | ----------------------------------------------------------- |
|
|
485
|
+
| `test` | `nmr root:test && pnpm --recursive exec nmr test` |
|
|
486
|
+
| `test:all` | `nmr root:test:all && pnpm --recursive exec nmr test:all` |
|
|
487
|
+
| `test:coverage` | `nmr root:test && pnpm --recursive exec nmr test:coverage` |
|
|
488
|
+
| `test:tool` | `nmr root:test:tool && pnpm --recursive exec nmr test:tool` |
|
|
489
|
+
| `test:unit` | `nmr root:test:unit && pnpm --recursive exec nmr test:unit` |
|
|
490
|
+
| `test:watch` | `vitest --project unit --project tool --watch` |
|
|
326
491
|
|
|
327
492
|
`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
493
|
|
|
@@ -375,17 +540,18 @@ See [dependency upgrades](#dependency-upgrades) for the workflow and its configu
|
|
|
375
540
|
|
|
376
541
|
These scripts operate on root-level code only (not workspace packages):
|
|
377
542
|
|
|
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:
|
|
543
|
+
| Command | Runs |
|
|
544
|
+
| ------------------ | ----------------------------------------------------------------------- |
|
|
545
|
+
| `root:check` | `root:typecheck`, `fmt:check`, `root:lint:check`, `root:test` |
|
|
546
|
+
| `root:lint` | `eslint --fix --ignore-pattern 'packages/**' .` |
|
|
547
|
+
| `root:lint:check` | `eslint --ignore-pattern 'packages/**' .` |
|
|
548
|
+
| `root:lint:strict` | `strict-lint --ignore-pattern 'packages/**' .` |
|
|
549
|
+
| `root:test` | `vitest --config ./vitest.root.config.ts --project unit --project tool` |
|
|
550
|
+
| `root:test:all` | `vitest --config ./vitest.root.config.ts` |
|
|
551
|
+
| `root:test:tool` | `vitest --config ./vitest.root.config.ts --project tool` |
|
|
552
|
+
| `root:test:unit` | `vitest --config ./vitest.root.config.ts --project unit` |
|
|
553
|
+
| `root:typecheck` | `tsgo --noEmit` |
|
|
554
|
+
| `root:upgrade` | `nmr-taze --include-locked` |
|
|
389
555
|
|
|
390
556
|
#### Utilities
|
|
391
557
|
|
|
@@ -404,14 +570,15 @@ nmr [flags] <command> [args...]
|
|
|
404
570
|
|
|
405
571
|
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
572
|
|
|
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
|
-
|
|
|
573
|
+
| Flag | Description | Default |
|
|
574
|
+
| ------------------------ | ------------------------------------------------------- | ------- |
|
|
575
|
+
| `-F, --filter <pattern>` | Run command in matching packages | — |
|
|
576
|
+
| `-R, --recursive` | Run command in all packages | — |
|
|
577
|
+
| `-w, --workspace-root` | Use root scripts, running at the monorepo root | — |
|
|
578
|
+
| `-q, --quiet` | Suppress info messages; show full output on failure | — |
|
|
579
|
+
| `--no-cache` | Run even if this tree already passed; record the result | — |
|
|
580
|
+
| `-?, --help` | Show available commands | — |
|
|
581
|
+
| `-V, --version` | Show version number | — |
|
|
415
582
|
|
|
416
583
|
### Examples
|
|
417
584
|
|
|
@@ -421,8 +588,9 @@ nmr test # Run workspace test script
|
|
|
421
588
|
nmr build # Compile to .js and .d.ts in one pass
|
|
422
589
|
|
|
423
590
|
# From the monorepo root
|
|
424
|
-
nmr test
|
|
425
|
-
nmr ci
|
|
591
|
+
nmr test # Root tests + recursive workspace tests
|
|
592
|
+
nmr ci # build + check:strict
|
|
593
|
+
nmr prepush # ci + audit
|
|
426
594
|
|
|
427
595
|
# Target specific packages
|
|
428
596
|
nmr --filter core test # Test only the core package
|
|
@@ -488,6 +656,10 @@ nmr-clean
|
|
|
488
656
|
|
|
489
657
|
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
658
|
|
|
659
|
+
**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.
|
|
660
|
+
|
|
661
|
+
**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.
|
|
662
|
+
|
|
491
663
|
`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
664
|
|
|
493
665
|
```bash
|
|
@@ -638,29 +810,40 @@ export default defineVitestConfig();
|
|
|
638
810
|
|
|
639
811
|
`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
812
|
|
|
641
|
-
### Test
|
|
813
|
+
### Test tiers
|
|
642
814
|
|
|
643
|
-
The config declares
|
|
815
|
+
The config declares four projects, an ordered ladder named for the furthest thing a test reaches:
|
|
644
816
|
|
|
645
|
-
| Project
|
|
646
|
-
|
|
|
647
|
-
| `unit`
|
|
648
|
-
| `
|
|
649
|
-
| `
|
|
817
|
+
| Project | Matches | Reaches |
|
|
818
|
+
| ----------- | -------------------------------------- | ---------------------------------------------------- |
|
|
819
|
+
| `unit` | every test file the others don't claim | nothing outside the package's own dependency closure |
|
|
820
|
+
| `tool` | `*.tool.test.{ts,tsx}` | a program the environment must supply |
|
|
821
|
+
| `localhost` | `*.localhost.test.{ts,tsx}` | a service running on this machine |
|
|
822
|
+
| `remote` | `*.remote.test.{ts,tsx}` | a machine that isn't this one |
|
|
650
823
|
|
|
651
|
-
All
|
|
824
|
+
All four match only under a `__tests__` directory. Select them at run time with `--project`, which unions when repeated and accepts negation:
|
|
652
825
|
|
|
653
826
|
```bash
|
|
654
|
-
vitest --project
|
|
655
|
-
vitest --project
|
|
656
|
-
vitest
|
|
827
|
+
vitest --project unit --project tool # everything that runs on a bare install
|
|
828
|
+
vitest --project tool # tests reaching a program, alone
|
|
829
|
+
vitest # every project
|
|
657
830
|
```
|
|
658
831
|
|
|
659
|
-
|
|
832
|
+
**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`.
|
|
833
|
+
|
|
834
|
+
**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.
|
|
835
|
+
|
|
836
|
+
`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 also what makes an unrecognized infix safe to use as documentation: `resolveConfig.packaged.test.ts` runs under `unit` and reads as a companion to `resolveConfig.unit.test.ts`.
|
|
837
|
+
|
|
838
|
+
`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.
|
|
839
|
+
|
|
840
|
+
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.
|
|
841
|
+
|
|
842
|
+
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
843
|
|
|
661
844
|
### Customizing by scope
|
|
662
845
|
|
|
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
|
|
846
|
+
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
847
|
|
|
665
848
|
```ts
|
|
666
849
|
export default defineVitestConfig({
|
|
@@ -668,16 +851,57 @@ export default defineVitestConfig({
|
|
|
668
851
|
root: { resolve: { conditions: ['development'] } },
|
|
669
852
|
// Applied to every project.
|
|
670
853
|
project: { setupFiles: ['./vitest.setup.ts'] },
|
|
854
|
+
// Applied to one tier, after the `project` block above.
|
|
855
|
+
tiers: { tool: { testTimeout: 120_000 } },
|
|
671
856
|
});
|
|
672
857
|
```
|
|
673
858
|
|
|
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.
|
|
859
|
+
`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.
|
|
860
|
+
|
|
861
|
+
`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.
|
|
675
862
|
|
|
676
|
-
Arrays concatenate rather than replace. `exclude` and `setupFiles` therefore add to what the config already declares, and
|
|
863
|
+
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.
|
|
864
|
+
|
|
865
|
+
`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.
|
|
866
|
+
|
|
867
|
+
### Sharing options across config files
|
|
868
|
+
|
|
869
|
+
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:
|
|
870
|
+
|
|
871
|
+
```ts
|
|
872
|
+
// vitest.shared.ts
|
|
873
|
+
import { fileURLToPath } from 'node:url';
|
|
874
|
+
|
|
875
|
+
import type { VitestConfigOptions } from '@williamthorsen/nmr/vitest';
|
|
876
|
+
|
|
877
|
+
export const shared: VitestConfigOptions = {
|
|
878
|
+
root: { resolve: { conditions: ['source'] } },
|
|
879
|
+
project: { setupFiles: [fileURLToPath(new URL('./vitest.setup.ts', import.meta.url))] },
|
|
880
|
+
};
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
```ts
|
|
884
|
+
// packages/web/vitest.config.ts
|
|
885
|
+
import { defineVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
886
|
+
|
|
887
|
+
import { shared } from '../../vitest.shared.ts';
|
|
888
|
+
|
|
889
|
+
export default defineVitestConfig(shared, { project: { environment: 'jsdom' } });
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
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.
|
|
893
|
+
|
|
894
|
+
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.
|
|
895
|
+
|
|
896
|
+
**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.
|
|
897
|
+
|
|
898
|
+
**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.
|
|
899
|
+
|
|
900
|
+
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
901
|
|
|
678
902
|
### What the config excludes
|
|
679
903
|
|
|
680
|
-
Collection skips `**/node_modules/**`, `**/.git/**`, and `**/dist/**`. Coverage skips `**/__{fixtures,mocks,tests}__/**`, `**/index.ts`,
|
|
904
|
+
Collection skips `**/node_modules/**`, `**/.git/**`, and `**/dist/**`. Coverage skips `**/__{fixtures,mocks,tests}__/**`, `**/index.ts`, and `**/*.d.ts`.
|
|
681
905
|
|
|
682
906
|
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
907
|
|
|
@@ -702,24 +926,31 @@ import { defineRootVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
|
702
926
|
export default defineRootVitestConfig({ monorepoRoot: import.meta.dirname });
|
|
703
927
|
```
|
|
704
928
|
|
|
705
|
-
This variant reads `pnpm-workspace.yaml` and excludes every workspace package from all
|
|
929
|
+
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
930
|
|
|
707
931
|
`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
932
|
|
|
933
|
+
### Migrating to the isolation tiers
|
|
934
|
+
|
|
935
|
+
The projects were once named `unit`, `integration`, and `app`, for what a test _covered_ rather than what it _reached_. `integration` and `app` are gone.
|
|
936
|
+
|
|
937
|
+
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 drops the infix. Re-read each file rather than renaming in bulk -- the axis changed, so the old bucket does not map onto one new tier.
|
|
938
|
+
2. Leave `*.app.test.ts` alone, or rename at your leisure. `app` was never a tier, and its files now land in `unit`.
|
|
939
|
+
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.
|
|
940
|
+
|
|
941
|
+
**An unrenamed `*.int.test.ts` file is a silent failure.** `.int.` no longer matches a project, so the residual `unit` claims those files and they run in the default gate -- green, with nothing reporting that the separation was lost. `rdy run nmr` reports them; a passing test run does not. Run it before treating the migration as done.
|
|
942
|
+
|
|
943
|
+
`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.
|
|
944
|
+
|
|
709
945
|
### Migrating from the config-file variants
|
|
710
946
|
|
|
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.
|
|
947
|
+
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
948
|
|
|
713
949
|
1. Replace the repo's root `vitest.config.ts` with `defineVitestConfig()`, and its root-scoped config with `defineRootVitestConfig({ monorepoRoot: import.meta.dirname })`.
|
|
714
950
|
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
|
|
951
|
+
3. Rename the tests onto the tiers, per the section above.
|
|
716
952
|
4. Drop any hand-copied `test*` entries from `package.json`, which now shadow the defaults.
|
|
717
953
|
|
|
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"`. |
|
|
954
|
+
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
955
|
|
|
725
|
-
|
|
956
|
+
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.
|