@williamthorsen/nmr 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.meta/changelog.json +83 -0
- package/.readyup/kits/default.js +91 -3
- package/.readyup/manifest.json +5 -5
- package/CHANGELOG.md +63 -0
- package/README.md +116 -1127
- package/agents/README.md +1 -1
- package/agents/guidance/rulebooks/nmr.md +2 -2
- package/dist/esm/context.d.ts +2 -0
- package/dist/esm/context.js +1 -0
- package/dist/esm/help.js +1 -1
- package/dist/esm/helpers/filter-selection.d.ts +42 -0
- package/dist/esm/helpers/filter-selection.js +61 -0
- package/dist/esm/runCli.js +70 -2
- package/dist/esm/runner.js +6 -5
- package/dist/esm/steps.d.ts +4 -0
- package/dist/esm/verdict.d.ts +1 -1
- package/dist/esm/vitest-source-resolution.d.ts +37 -0
- package/dist/esm/vitest-source-resolution.js +205 -0
- package/dist/esm/vitest.d.ts +3 -2
- package/dist/esm/vitest.js +7 -6
- package/dist/esm/workspace.d.ts +9 -0
- package/dist/esm/workspace.js +17 -0
- package/docs/check-cache.md +176 -0
- package/docs/prettier.md +47 -0
- package/docs/reporting.md +90 -0
- package/docs/scripts.md +168 -0
- package/docs/upgrades.md +110 -0
- package/docs/utilities.md +64 -0
- package/docs/vitest.md +233 -0
- package/docs/workspace.md +11 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,38 +1,47 @@
|
|
|
1
|
+
<!-- readme-type: cli -->
|
|
2
|
+
|
|
1
3
|
# @williamthorsen/nmr
|
|
2
4
|
|
|
3
5
|
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
6
|
|
|
5
7
|
<!-- section:release-notes -->
|
|
6
|
-
## Release notes — v0.
|
|
8
|
+
## Release notes — v0.37.0 (2026-09-15)
|
|
7
9
|
|
|
8
10
|
### 🎉 Features
|
|
9
11
|
|
|
10
|
-
-
|
|
12
|
+
- Guard lefthook install in root prepare and report unguarded installs (#798)
|
|
13
|
+
|
|
14
|
+
- Adds a `warn` check to the readyup kit of `@williamthorsen/nmr` that reports a root script run by `pnpm install`, such as `prepare` or `postinstall`, whose command calls `lefthook install` without `lefthook check-install`.
|
|
15
|
+
- Includes in the check's suggested fix the guarded form `lefthook check-install || lefthook install`, which skips rewriting `.git/hooks` while `lefthook.yml` is unchanged since the last install, so that a repo using that form can run `pnpm install` without write access to `.git/hooks`, as inside the Claude Code sandbox, and under pnpm's default `verifyDepsBeforeRun` avoids a reinstall before every `pnpm exec`.
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
- Report misplaced test files and a missing guard from the readyup kit (#804)
|
|
13
18
|
|
|
14
|
-
-
|
|
19
|
+
- Adds an `error` check to the `@williamthorsen/nmr` readyup kit that reports each `*.test.ts` or `*.test.tsx` file outside a `__tests__` directory, which no Vitest project collects and which therefore runs nowhere.
|
|
20
|
+
- Adds a `warn` check that passes once a test file under `__tests__` imports `checkTestFileConventions` from `@williamthorsen/nmr/tests`, and whose fix shows the one-line guard test to add.
|
|
21
|
+
- Makes the kit skip both test-file checks once it finds that guard, because the guard reports the same files and applies the repo's `exclude` list, which the kit cannot read.
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
### 🐛 Bug fixes
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
- Fail when a -F pattern selects no workspace (#785)
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
- Fixes a run that reported success without doing anything. `nmr -F <pattern> <command>` printed nothing and exited 0 when the pattern matched no package. It now exits 1, names the rejected pattern, and explains the likely cause.
|
|
28
|
+
- Covers three further selections that ran nothing and exited 0: an exclusion that leaves only the workspace root, such as `nmr -F '!./packages/*'`; `-R` in a workspace that declares no package; and an empty `-F ''`.
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
Migration: Replace any `-F` pattern that names a directory with the package's manifest `name`. A filter that selects nothing now fails the run instead of passing in silence.
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
- Adds `every bin wrapper's build-output target is covered by files`, which reports a wrapper loading build output that `files` omits, since that publishes a bin resolving to nothing.
|
|
32
|
+
- Fix test runs failing on a dependency with a TypeScript source export (#793)
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
- Fixes the issue that, with `resolveFromSource` at its default of `true`, every test in a file failed with `Stripping types is currently unsupported for files under node_modules` when the file's imports reached a dependency whose `exports` map declares a `source` condition pointing at TypeScript.
|
|
35
|
+
- Limits the source resolution enabled by `resolveFromSource` to packages whose real path lies outside every `node_modules`, which covers workspace packages and packages linked into the repo from elsewhere.
|
|
36
|
+
- Makes an import of a workspace package fail when that package's `source` condition names a missing file, instead of resolving to the build output without warning.
|
|
37
|
+
- Makes conditions that a config adds to `resolve.conditions` or `ssr.resolve.conditions` extend Vite's defaults when `resolveFromSource` is `false`, instead of replacing them.
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
Migration: Point each workspace package's `source` condition at a file that exists, or remove the condition. Build a workspace package before running a test that reaches it through an externalized dependency (one that Vitest loads through Node instead of transforming): Node resolves that import without the `source` condition, so it loads the package's build output.
|
|
30
40
|
|
|
31
|
-
-
|
|
41
|
+
- Prevent a recursive or multi-package run from hanging under pnpm 12 (#814)
|
|
32
42
|
|
|
33
|
-
Fixes
|
|
34
|
-
|
|
35
|
-
Migration: Where a test needs its own `core.excludesFile` or `core.attributesFile`, pass it on the invocation as `git -c core.excludesFile=<path>`. An environment-injected key now outranks repository-local config.
|
|
43
|
+
- Fixes a hang under pnpm 12: In a terminal, `nmr -R <command>`, a root command that runs in every package such as `nmr build`, and a `-F` pattern that selects several packages printed nothing and waited until Ctrl+C when a package command read keyboard input or changed the terminal's mode, as the spinner in `next build` does.
|
|
44
|
+
- Gives no terminal input to each package of a `-R` run or a multi-package `-F` run, including a dependency-chain filter such as `-F 'app...'`, and keeps it for a `-F` filter that selects one package, which an interactive command such as `nmr -F <pkg> test:watch` needs.
|
|
36
45
|
<!-- /section:release-notes -->
|
|
37
46
|
|
|
38
47
|
## Installation
|
|
@@ -69,9 +78,23 @@ From the monorepo root:
|
|
|
69
78
|
```bash
|
|
70
79
|
nmr test # Run root tests + recursive workspace tests
|
|
71
80
|
nmr build # Build all packages
|
|
81
|
+
nmr ci # Build, then run the strict checks
|
|
72
82
|
nmr prepush # Run everything the remote runs, before you push
|
|
73
83
|
```
|
|
74
84
|
|
|
85
|
+
Target packages from anywhere:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
nmr --filter core test # Test only the package whose manifest name is `core`
|
|
89
|
+
nmr --recursive lint # Lint all workspace packages
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`--recursive`, and a `--filter` that selects more than one package, give each package no terminal input. An interactive command such as `test:watch` needs a filter that selects a single package.
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
`nmr --help` lists every flag and command, with the shell command each command resolves to. Per-package `package.json` files need no script entries.
|
|
97
|
+
|
|
75
98
|
nmr detects where you are and selects the right scripts automatically — see [context-aware resolution](#context-aware-resolution) below.
|
|
76
99
|
|
|
77
100
|
## Context-aware resolution
|
|
@@ -106,163 +129,45 @@ Scripts resolve through three tiers. Higher tiers override lower ones:
|
|
|
106
129
|
2. **Repo-wide config** — additions and overrides in `.config/nmr.config.ts`
|
|
107
130
|
3. **Per-package overrides** — scripts in a package's `package.json`
|
|
108
131
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
Given the `build` command for a package that defines its own build script:
|
|
112
|
-
|
|
113
|
-
| Tier | Source | Value | Wins? |
|
|
114
|
-
| ---- | ----------------------- | ----------------------- | ----- |
|
|
115
|
-
| 1 | Built-in default | `['compile']` | — |
|
|
116
|
-
| 2 | `.config/nmr.config.ts` | _(not set)_ | — |
|
|
117
|
-
| 3 | `package.json` scripts | `"tsx custom-build.ts"` | ✓ |
|
|
118
|
-
|
|
119
|
-
If no per-package override exists, the highest-tier value that is set wins. Set a script to `""` in `package.json` to skip it for that package.
|
|
120
|
-
|
|
121
|
-
> **Tip:** If your repo uses `eslint-plugin-package-json/valid-scripts`, empty strings are flagged as invalid. Use `":"` (the POSIX null command) instead — nmr treats it as a regular override that exits successfully and does nothing.
|
|
122
|
-
|
|
123
|
-
A `package.json` entry that re-invokes the command it is declared under is never honored. Honoring `"build": "nmr build"` would spawn a shell running `nmr build` in the same directory, reaching the same entry again without bound, so nmr discards the entry and resolves the command from the tier below. An entry that carries only the self-reference, trailing arguments included, is discarded in silence: it declares no step to lose. An entry that chains anything else onto it — `"build": "nmr build && rdy compile"`, or `"build": "rdy compile && nmr build"` — would lose those steps, so nmr rejects the invocation instead, naming the file, the key, and where the steps belong. For an ordinary command that is a `<command>:pre` or `<command>:post` script: `"build:pre": "rdy verify"` and `"build:post": "rdy compile"` wrap nmr's own `build` rather than replacing it. For a hook entry it is the entry itself, since nmr wraps a `:pre` or `:post` script in no hooks of its own — `"build:post": "nmr build:post && rdy compile"` becomes `"build:post": "rdy compile"`.
|
|
124
|
-
|
|
125
|
-
The re-invocation is recognized wherever it stands in the entry, on the grammar [the shelled-nmr line](#script-values) uses, and a segment carrying `-F` or `-R` is not one — it hands the command to other scopes rather than back to this one. Nor is `-w` from inside a package, which reaches the root's registry and the root's `package.json`. A filter that selects the declaring package is the hole in that exemption: `nmr -F own-pkg build` in `own-pkg`'s own `build` re-enters without bound, and nmr does not recognize it.
|
|
126
|
-
|
|
127
|
-
### Script values
|
|
128
|
-
|
|
129
|
-
A script value is a string or an array of steps. Only the built-in defaults and `.config/nmr.config.ts` carry an array; a `package.json` script must be a string, and any other value is rejected when the scripts are read. Arrays expand to chained `nmr` sub-invocations:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
// "fix": ["lint", "fmt"]
|
|
133
|
-
// expands to: nmr lint && nmr fmt
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Each array element is a command name, optionally preceded by nmr's own flags, split on whitespace — `-R test` runs the element in every package, the way `nmr -R test` does. An element carrying a quoted argument or shell syntax is rejected when the config loads: name that command as a script of its own and reference the name.
|
|
137
|
-
|
|
138
|
-
#### Where trailing arguments go
|
|
139
|
-
|
|
140
|
-
Trailing arguments reach every step of a composite, so `nmr fix --dry-run` runs `nmr lint --dry-run`, then `nmr fmt --dry-run`. A step whose work the arguments cannot narrow declines them by taking the long form of an element, and runs unnarrowed:
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
// "ci": [{ run: "build", declinesArgs: true }, "check:strict"]
|
|
144
|
-
// nmr ci src/foo.ts -> nmr build && nmr check:strict src/foo.ts
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
`{ run }` is the bare string element with room for the declaration; `declinesArgs` defaults to `false`, and an element that accepts is written as the string. Decline where the step is a prerequisite the narrowed steps run against, or where the tool it reaches would be misled — `tsgo --noEmit foo.ts` abandons the tsconfig, which is why every default `typecheck` step declines. A leaf tool that merely ignores what it is handed is not a reason to decline: nmr forwards, and the tool decides.
|
|
148
|
-
|
|
149
|
-
Where no step of a command accepts them, nmr rejects the invocation and runs nothing, rather than running the whole command unnarrowed. That is what `nmr typecheck src/foo.ts` gets at the root, whose two steps both decline.
|
|
132
|
+
[Scripts and configuration](docs/scripts.md#three-tier-override-system) shows a worked example, how to skip a script for one package, and what nmr does with a `package.json` entry that re-invokes its own command.
|
|
150
133
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
An invocation carrying trailing arguments also serves no part of its work from the [check-result cache](#check-result-cache), the steps that decline them included.
|
|
154
|
-
|
|
155
|
-
A string value is spawned through a shell as one command, so an `nmr` invocation inside it is invisible to the process above: a quiet failure surrenders the whole nested subtree rather than the failing step, and a loud run relays the nested tree through a forwarding pipe. nmr writes one line to stderr, in every verbosity, when a command resolves to a step reaching nmr through a shell.
|
|
156
|
-
|
|
157
|
-
The line names the declaration it is an edit to — the file, the field, and the key — and its remedy follows from that site. A `.config/nmr.config.ts` entry keeps its nmr steps as a step list and moves anything else to a `<command>:pre` or `<command>:post` script: a step-list element is composed into `nmr <element>`, so it can name only an nmr command. A `package.json` entry has to go, since `package.json` holds no step list: delete it outright when it restates what nmr already runs for that command, and move the steps it adds to a `<command>:pre` or `<command>:post` script when they are the package's own. Where the registry defines no such command, both apply: name the command in `.config/nmr.config.ts`, and put the package-specific steps in a hook. The pair is named rather than `:post` alone because an entry's extra steps may run on either side of the chain: `rdy verify && nmr compile` puts them first. A hook is the exception at both tiers: nmr wraps a `:pre` or `:post` script in no hooks of its own, so a hook's other steps become a script of their own that the step list names.
|
|
158
|
-
|
|
159
|
-
nmr recognizes `nmr` in command position: at the start of a step, after an unquoted `&&`, `||`, `;`, `|`, or a newline, past any leading `NAME=value` assignments, and behind `npx`, `bunx`, or an `exec`, `dlx`, or `x` subcommand of `npm`, `pnpm`, `yarn`, or `bun`. This line reports a boundary rather than enforcing a rule — it covers a step nmr runs as written, and only the reporting of the nested run is degraded — and it stays partial in stated ways: a value-taking flag standing immediately before the program name hides it (`npx -p foo nmr`), a launcher outside that set goes unreported, and a `devBin` substitution that introduces `nmr` itself is not reported, the line being an edit to a declaration rather than to a substitution.
|
|
160
|
-
|
|
161
|
-
A [self-referential `package.json` entry](#three-tier-override-system) is the shape nmr cannot run as written, and it is rejected rather than reported. The same recognition finds it, so the same residuals apply — what goes unrecognized there re-enters nmr without bound, which hangs rather than passing quietly.
|
|
134
|
+
## Pre and post hooks
|
|
162
135
|
|
|
163
|
-
|
|
136
|
+
Every `nmr X` invocation auto-wraps as the equivalent of `nmr X:pre && nmr X && nmr X:post`. Hooks are first-class scripts that resolve through the same three tiers as any other script (built-in defaults, then `.config/nmr.config.ts`, then per-package `package.json`). Wrapping is uniform: nested invocations from composite expansion get their own hook treatment. A failing hook ends the sequence, and its exit code propagates.
|
|
164
137
|
|
|
165
|
-
|
|
138
|
+
Extend `nmr build` with a pre-build step for every workspace package:
|
|
166
139
|
|
|
167
140
|
```ts
|
|
141
|
+
// .config/nmr.config.ts
|
|
168
142
|
import { defineConfig } from '@williamthorsen/nmr/config';
|
|
169
143
|
|
|
170
144
|
export default defineConfig({
|
|
171
145
|
workspaceScripts: {
|
|
172
|
-
'
|
|
173
|
-
},
|
|
174
|
-
rootScripts: {
|
|
175
|
-
'demo:catwalk': 'pnpx http-server --port=5189 demos/catwalk/',
|
|
176
|
-
},
|
|
177
|
-
});
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### `defineConfig` fields
|
|
181
|
-
|
|
182
|
-
| Field | Type | Description |
|
|
183
|
-
| ------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
184
|
-
| `build` | `{ extraIgnorePatterns?: string[] }` | Patterns added to the build's ignore set (package config only) |
|
|
185
|
-
| `checkCache` | `{ enabled?: boolean; extraCommands?: string[]; excludeCommands?: string[] }` | Which commands the [check-result cache](#check-result-cache) may skip |
|
|
186
|
-
| `workspaceScripts` | `Record<string, ScriptValue>` | Scripts added or overridden in the workspace registry (tier 2) |
|
|
187
|
-
| `rootScripts` | `Record<string, ScriptValue>` | Scripts added or overridden in the root registry (tier 2) |
|
|
188
|
-
| `devBin` | `Record<string, string>` | Map binary names to source-repo replacement commands |
|
|
189
|
-
|
|
190
|
-
All fields are optional. A `ScriptValue` is a command string or an array of steps, each a command string or `{ run, declinesArgs? }`; both are described in [script values](#script-values), and both types are exported from `@williamthorsen/nmr/config`.
|
|
191
|
-
|
|
192
|
-
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.
|
|
193
|
-
|
|
194
|
-
### Package-level configuration
|
|
195
|
-
|
|
196
|
-
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.
|
|
197
|
-
|
|
198
|
-
```ts
|
|
199
|
-
// packages/my-package/.config/nmr.config.ts
|
|
200
|
-
import { defineConfig } from '@williamthorsen/nmr/config';
|
|
201
|
-
|
|
202
|
-
export default defineConfig({
|
|
203
|
-
build: { extraIgnorePatterns: ['**/fixtures/**'] },
|
|
204
|
-
});
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
`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.
|
|
208
|
-
|
|
209
|
-
> **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.
|
|
210
|
-
|
|
211
|
-
### `devBin` — source-repo binary substitution
|
|
212
|
-
|
|
213
|
-
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:
|
|
214
|
-
|
|
215
|
-
```ts
|
|
216
|
-
export default defineConfig({
|
|
217
|
-
devBin: {
|
|
218
|
-
'my-cli': 'tsx packages/my-cli/src/cli.ts',
|
|
146
|
+
'build:pre': 'node scripts/generate-manifest.ts',
|
|
219
147
|
},
|
|
220
148
|
});
|
|
221
149
|
```
|
|
222
150
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
For example, if a workspace script resolves to `my-cli --verbose`, nmr rewrites it to `tsx /absolute/path/to/packages/my-cli/src/cli.ts --verbose`.
|
|
226
|
-
|
|
227
|
-
> **Note:** Path resolution uses a heuristic: any non-flag token containing `/` is treated as a relative path. This works well for typical dev-tool commands but may incorrectly resolve URL-like values or glob patterns. Flags using `--flag=value` syntax are not resolved; use the spaced form `--flag value` for paths that need resolution.
|
|
151
|
+
[Scripts and configuration](docs/scripts.md#pre-and-post-hooks) covers how hooks behave when absent or skipped, and how to attach a step to one package's hook.
|
|
228
152
|
|
|
229
|
-
##
|
|
230
|
-
|
|
231
|
-
Every `nmr X` invocation auto-wraps as the equivalent of `nmr X:pre && nmr X && nmr X:post`. Hooks are first-class scripts that resolve through the same three tiers as any other script (built-in defaults, then `.config/nmr.config.ts`, then per-package `package.json`). Wrapping is uniform: nested invocations from composite expansion get their own hook treatment. A failing hook ends the sequence, and its exit code propagates.
|
|
232
|
-
|
|
233
|
-
Behaviors worth knowing:
|
|
234
|
-
|
|
235
|
-
- **Silent when absent**: Missing hooks produce no error and no output.
|
|
236
|
-
- **Skip overrides apply to hooks**: A hook value of `""` or `":"` is treated the same as not defining the hook, with no console message.
|
|
237
|
-
- **Skipping the main command skips its hooks**: When `X` is overridden to `""` or `":"`, neither `X:pre` nor `X:post` fires.
|
|
238
|
-
- **Recursion guard**: Direct invocation of a hook (e.g. `nmr build:pre`) is treated as a leaf operation. It does not itself attempt to resolve `build:pre:pre` or `build:pre:post`.
|
|
239
|
-
- **Passthrough args attach only to the main command**: `nmr X --flag value` runs hooks without `--flag value`.
|
|
153
|
+
## Configuration
|
|
240
154
|
|
|
241
|
-
|
|
155
|
+
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](docs/scripts.md#package-level-configuration):
|
|
242
156
|
|
|
243
157
|
```ts
|
|
244
|
-
// .config/nmr.config.ts
|
|
245
158
|
import { defineConfig } from '@williamthorsen/nmr/config';
|
|
246
159
|
|
|
247
160
|
export default defineConfig({
|
|
248
161
|
workspaceScripts: {
|
|
249
|
-
'
|
|
162
|
+
'copy-content': 'tsx scripts/copy-content.ts',
|
|
250
163
|
},
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
Attach a step to one package's hook:
|
|
255
|
-
|
|
256
|
-
```jsonc
|
|
257
|
-
// packages/my-package/package.json
|
|
258
|
-
{
|
|
259
|
-
"scripts": {
|
|
260
|
-
"upgrade:post": "nmr-report-overrides",
|
|
164
|
+
rootScripts: {
|
|
165
|
+
'demo:catwalk': 'pnpx http-server --port=5189 demos/catwalk/',
|
|
261
166
|
},
|
|
262
|
-
}
|
|
167
|
+
});
|
|
263
168
|
```
|
|
264
169
|
|
|
265
|
-
|
|
170
|
+
[Scripts and configuration](docs/scripts.md#configuration) lists every `defineConfig` field and covers package-level configuration and `devBin`.
|
|
266
171
|
|
|
267
172
|
## What nmr reports
|
|
268
173
|
|
|
@@ -275,52 +180,9 @@ Every command nmr runs reports one line naming the scope it ran at, the command,
|
|
|
275
180
|
⛔ nmr-core: lint: skipped, the override is empty
|
|
276
181
|
```
|
|
277
182
|
|
|
278
|
-
A recalled pass also carries an excerpt of the run it recalls, where that run left one; see [Replaying a skipped run's output](#replaying-a-skipped-runs-output).
|
|
279
|
-
|
|
280
|
-
The scope is the directory the command's registry belongs to: a package's own directory, or the monorepo root. Durations and ages truncate and carry at most two units, so a check that passed 90 seconds ago reports `1m 30s ago` and never `2m ago`.
|
|
281
|
-
|
|
282
|
-
Verdicts nest. A composite reports, and so does every command it expands into; a `:pre` or `:post` hook reports nothing of its own, since the command it wraps already reports for the chain, though a hook that delegates to a named command reports under that name. A `-R` or `-F` invocation reports nothing either, every scope it fans out to reporting instead. A cold repo-wide `nmr ci` in this package's own monorepo spends 26 lines on a run that prints roughly 250.
|
|
283
|
-
|
|
284
|
-
A verdict is one write of at most 512 bytes, the smallest `PIPE_BUF` POSIX permits, so packages running concurrently under `pnpm --recursive` cannot interleave within a line. A line longer than that is cut and marked with `…`; nothing nmr reports today comes near it.
|
|
285
|
-
|
|
286
183
|
**Verdicts print in every verbosity.** `-q` withholds the output of the commands nmr runs, never nmr's own words: a passing quiet run reports its verdicts and nothing else. A command whose override resolved to `""` or `":"` reports the skip rather than exiting 0 in silence, which is what separates it from a command that passed. An `NMR_RUN_IF_PRESENT` miss reports nothing, having no command to report on.
|
|
287
184
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
`--json` renders each verdict as a JSON object instead of a line of prose — one object per line, newline-delimited, with no enclosing array and no separators between them. A consumer parses each line as it arrives, and a run cut off partway still leaves every line it already emitted parseable, which an enclosing array would not.
|
|
291
|
-
|
|
292
|
-
```console
|
|
293
|
-
$ nmr --json check
|
|
294
|
-
{"command":"typecheck","scope":"nmr-core","outcome":"passed","durationMs":255}
|
|
295
|
-
{"command":"fmt:check","scope":"nmr-core","outcome":"recalled","ageMs":240000,"savedMs":3888,"replay":[{"command":"fmt:check","excerpt":"Checking formatting... All matched files use Prettier code style!","scope":"nmr-core"}]}
|
|
296
|
-
{"command":"check","scope":"nmr-core","outcome":"passed","durationMs":31204}
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
Every object carries `command`, `scope`, and `outcome`, and then what that outcome carries and no other does:
|
|
300
|
-
|
|
301
|
-
| `outcome` | Also carries |
|
|
302
|
-
| ---------- | ---------------------------------------------------------------------------------------------------------- |
|
|
303
|
-
| `passed` | `durationMs` |
|
|
304
|
-
| `failed` | `durationMs`, `exitCode` |
|
|
305
|
-
| `recalled` | `ageMs`, `savedMs`, and `replay` where the skip has excerpts to [replay](#replaying-a-skipped-runs-output) |
|
|
306
|
-
| `no-op` | `reason`, either `empty-override` or `noop-override` |
|
|
307
|
-
|
|
308
|
-
Both renderings are produced from one record, so neither reports what the other does not. Where the prose line makes a presentation decision the record does not — a saving too small to be worth a clause, say — the object carries the fact and leaves the decision to whoever reads it.
|
|
309
|
-
|
|
310
|
-
**An object is one write of at most 512 bytes**, the [same ceiling](#what-nmr-reports) a prose line is held to, so packages running concurrently under `pnpm --recursive` cannot interleave within one. Cuts land inside the record's text rather than across its structure, so the line still parses, and a record that overruns is fitted by rungs — each shedding what a reader can better spare than the one below it:
|
|
311
|
-
|
|
312
|
-
1. **The excerpts are shortened toward one another**, so an assembly's constituents come down together rather than the first ones being emptied to leave the last whole. No excerpt is cut below `…`, which keeps a cut one distinguishable from a run that recorded nothing.
|
|
313
|
-
2. **The excerpts go**, leaving the `scope` and `command` that name each constituent. An entry carrying no `excerpt` key is one whose text did not fit.
|
|
314
|
-
3. **The trailing constituents go**, as the prose line drops its own tail, and the `replay` array itself once none fit.
|
|
315
|
-
4. **The `scope` and `command` are cut**, each marked. This is the last resort and the reason for it is the ceiling itself: a line that overruns can be split across a pipe and corrupt the records of every scope sharing it, where a marked cut costs only its own.
|
|
316
|
-
|
|
317
|
-
A package-scoped `check` carries four constituents and fits uncut. A four-package root `check` assembles twelve — `typecheck` and `test` each fan out to every package, and a nested composite's list is spliced in flat — which is more than the ceiling can name: it reaches rung 3, keeping eight and dropping the trailing four. Rung 4 needs a scope and command running to hundreds of bytes between them.
|
|
318
|
-
|
|
319
|
-
**The mode implies quiet.** The commands' own output is withheld, so stdout carries the objects alone — no prose verdict, and no override notice. `NMR_COMMAND_VERBOSITY=full` does not opt back out. A failure still surrenders the failing command's output, on stderr, as under any quiet run. Both modes leave a command on the same channels, so a machine-readable run and a quiet one share a [retention key](#replaying-a-skipped-runs-output): each replays what the other recorded.
|
|
320
|
-
|
|
321
|
-
The format is nmr's own reporting and not a mode the commands it runs are told about, so it is out of the [pass key](#what-the-key-is-made-of) and the retention key alike. Selecting it neither invalidates a recorded pass nor costs a replay.
|
|
322
|
-
|
|
323
|
-
`--log`, `--help`, and `--version` are unaffected: none of them reports a verdict, and what `--log` prints is a tool's own transcript, which has no rendering as a record.
|
|
185
|
+
[What nmr reports](docs/reporting.md) covers how verdicts nest, the `--json` format, and where the verbosity of a run comes from.
|
|
324
186
|
|
|
325
187
|
## Check-result cache
|
|
326
188
|
|
|
@@ -334,984 +196,111 @@ $ nmr ci
|
|
|
334
196
|
⏭️ node-monorepo-tools: ci: passed 2m ago on this tree, saved ~4m
|
|
335
197
|
```
|
|
336
198
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
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.
|
|
340
|
-
|
|
341
|
-
### What is cached
|
|
342
|
-
|
|
343
|
-
Cacheable by default: `check`, `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`.
|
|
344
|
-
|
|
345
|
-
Everything else runs every time:
|
|
346
|
-
|
|
347
|
-
| Command | Why it is never skipped |
|
|
348
|
-
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
349
|
-
| `audit`, `prepush` | Consult a vulnerability database that moves without the tree. `prepush`'s `ci` constituent still skips while its `audit` runs every time. |
|
|
350
|
-
| `build`, `compile` | Carry a cache of their own. |
|
|
351
|
-
| `fix`, `fmt`, `lint`, `upgrade` | Rewrite the tree they were asked about, so a recorded pass would describe a tree that no longer exists. |
|
|
352
|
-
| `test:all` | Reaches the `localhost` and `remote` tiers, which the environment supplies rather than the tree. |
|
|
353
|
-
|
|
354
|
-
### The exit-status-only contract
|
|
355
|
-
|
|
356
|
-
**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.
|
|
357
|
-
|
|
358
|
-
`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.
|
|
359
|
-
|
|
360
|
-
Overriding a cacheable name, in `package.json` or in `workspaceScripts`, inherits its cacheability. An override that writes something anyone depends on belongs in `excludeCommands`.
|
|
361
|
-
|
|
362
|
-
### What the key is made of
|
|
363
|
-
|
|
364
|
-
A hit requires all of these to match the run that recorded the pass:
|
|
365
|
-
|
|
366
|
-
- **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.
|
|
367
|
-
- **The command string that would run**, hooks and all, and the scope it would run in.
|
|
368
|
-
- **nmr's version**, the **Node version**, and the **platform and architecture**.
|
|
369
|
-
- **What pnpm has installed**, so an install, a prune, or a lockfile change forces a re-run.
|
|
370
|
-
- **`TZ`, `LANG`, `LC_ALL`, and `NODE_OPTIONS`.** This set is fixed, so two machines that differ only in shell decoration still agree.
|
|
371
|
-
|
|
372
|
-
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.
|
|
373
|
-
|
|
374
|
-
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 unchanged since the run started, and never for a run that executed nothing (a `""`/`":"` skip override, or an `NMR_RUN_IF_PRESENT` miss). Output that moves mid-run leaves no answer to which output the pass was earned over, whether another process rebuilt it or the chain built it itself: `nmr ci` is the one default command whose chain does, and it records nothing on a run whose build did work. The `nmr check:strict` inside that chain still records, because its own window opens after the build.
|
|
375
|
-
|
|
376
|
-
### Out of contract
|
|
377
|
-
|
|
378
|
-
These change what a check concludes without moving the hash. Where one applies, exclude the affected command:
|
|
379
|
-
|
|
380
|
-
- **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.
|
|
381
|
-
- **Symlinked inputs.** A symlink is hashed by the path it names, not by the content at the far end.
|
|
382
|
-
- **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.
|
|
383
|
-
- **`passWithNoTests`.** A suite that collects no files exits 0, and that green is recorded like any other.
|
|
384
|
-
|
|
385
|
-
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.
|
|
386
|
-
|
|
387
|
-
### When the gate stands aside
|
|
199
|
+
[Check-result cache](docs/check-cache.md) covers what is cached, what a hit requires, replaying and reading a recorded run, and bypassing or configuring the cache.
|
|
388
200
|
|
|
389
|
-
|
|
201
|
+
## Shared configs and helpers
|
|
390
202
|
|
|
391
|
-
|
|
392
|
-
- When the repository declares or contains submodules, whose content the hash does not cover.
|
|
393
|
-
- When the monorepo root is not the git toplevel.
|
|
394
|
-
- When a changed path cannot be read, or is neither a file nor a symlink (an untracked nested repository, for instance).
|
|
395
|
-
- When there is no pnpm install to fingerprint.
|
|
396
|
-
- Under a `devBin` substitution, which runs a binary built from somewhere the hash does not describe.
|
|
397
|
-
- For any invocation carrying arguments after the command name, and for every step below it: the arguments narrow what runs, and a step served from a recorded pass is a step that did not.
|
|
203
|
+
### Shared Vitest config
|
|
398
204
|
|
|
399
|
-
`
|
|
400
|
-
|
|
401
|
-
### Replaying a skipped run's output
|
|
402
|
-
|
|
403
|
-
A skip reports an excerpt of the run it recalls, marked as a recording rather than as this run's output:
|
|
205
|
+
Every repo consuming nmr otherwise writes and maintains its own Vitest config. The `@williamthorsen/nmr/vitest` subpath publishes that config as a factory, so a repo declares only what it customizes:
|
|
404
206
|
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
|
|
207
|
+
```ts
|
|
208
|
+
// vitest.config.ts
|
|
209
|
+
import { defineVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
408
210
|
|
|
409
|
-
|
|
410
|
-
⏭️ nmr: test: passed 2m ago on this tree, saved ~12s — replayed: Test Files 6 passed (6) Tests 41 passed (41)
|
|
211
|
+
export default defineVitestConfig();
|
|
411
212
|
```
|
|
412
213
|
|
|
413
|
-
|
|
214
|
+
`@williamthorsen/nmr/tests` exports a check that fails a test run on a test file outside `__tests__` or one that names no tier. [Shared Vitest config](docs/vitest.md) covers the test tiers, that check, what the factory supplies, and customizing it by scope.
|
|
414
215
|
|
|
415
|
-
|
|
216
|
+
### Shared Prettier config
|
|
416
217
|
|
|
417
|
-
|
|
218
|
+
Every repo consuming nmr otherwise maintains its own copy of the house Prettier options, and wires up shell formatting itself. The `@williamthorsen/nmr/prettier` subpath publishes both as a factory, so a repo declares only what it customizes:
|
|
418
219
|
|
|
419
|
-
|
|
220
|
+
```js
|
|
221
|
+
// .prettierrc.js
|
|
222
|
+
import { definePrettierConfig } from '@williamthorsen/nmr/prettier';
|
|
420
223
|
|
|
421
|
-
|
|
422
|
-
$ nmr check
|
|
423
|
-
⏭️ nmr-core: check: passed 3m ago on this tree, saved ~48s — replayed: nmr-core: fmt:check: All matched files use Prettier code style!; nmr-core: test: Test Files 6 passed (6) Tests 41 passed (41)
|
|
224
|
+
export default definePrettierConfig();
|
|
424
225
|
```
|
|
425
226
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
It is assembled when the composite records its pass rather than when it skips, so every excerpt in it was certified during that one run, on that one tree. `NMR_RUN_ID` is what makes that provable; see [reserved environment variables](#reserved-environment-variables).
|
|
429
|
-
|
|
430
|
-
**A skip certifies what it replays.** Recalling a pass proves the excerpt describes this tree, and a matching retention key proves it describes this presentation environment, so a skip restamps the recalled entry with the run replaying it. A constituent already warm when a composite ran therefore still contributes its line: `nmr check` after `nmr test` replays the test summary. The restamp touches nothing else -- the instant and the duration belong to the run that earned the pass.
|
|
227
|
+
[Shared Prettier config](docs/prettier.md) covers what it formats and how to customize it.
|
|
431
228
|
|
|
432
|
-
|
|
229
|
+
### Dependency upgrades
|
|
433
230
|
|
|
434
|
-
|
|
231
|
+
`nmr upgrade` reports available dependency upgrades. A `taze.config.ts` at the monorepo root, built with the `defineConfig` exported by `@williamthorsen/nmr/taze`, declares the upgrade policy. [Dependency upgrades](docs/upgrades.md) covers what each invocation reaches, the policy and its ceilings, and the request timeout.
|
|
435
232
|
|
|
436
|
-
###
|
|
233
|
+
### Workspace introspection
|
|
437
234
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
```console
|
|
441
|
-
$ nmr --log test
|
|
442
|
-
📼 nmr: test — recorded 2026-08-12T15:04:05.412Z (12m ago), ran in 12.4s
|
|
443
|
-
$ pnpm exec vitest --project unit --project tool
|
|
444
|
-
|
|
445
|
-
✓ src/__tests__/resolver.unit.test.ts (18 tests) 12ms
|
|
446
|
-
|
|
447
|
-
Test Files 6 passed (6)
|
|
448
|
-
Tests 41 passed (41)
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
The header is what presents the body as a recording rather than as this invocation's output. The command string is the whole chain, hooks included, so it names what earned the pass and not merely what was typed. A flag rather than a subcommand, since `log` would collide with the script registry.
|
|
452
|
-
|
|
453
|
-
**Reading a recording runs nothing.** No hook runs, no verdict prints, and nothing is recorded or restamped. `--no-cache` governs running rather than reading and is ignored.
|
|
454
|
-
|
|
455
|
-
**A recording is held to the tree, not to the terminal.** `--log` prints exactly what a skip would have recalled: the [pass key](#what-the-key-is-made-of) has to match. The [retention key](#replaying-a-skipped-runs-output) is not consulted, so a recording made under a different terminal width, or through a pipe, still prints -- a reader at a terminal being shown what a piped run wrote is the case the flag exists for.
|
|
235
|
+
Repo-wide tests and scripts often need to know where the monorepo root is, or which directories its workspace packages live in. The `@williamthorsen/nmr/workspace` subpath publishes the two pnpm-workspace lookups nmr uses internally:
|
|
456
236
|
|
|
457
|
-
|
|
237
|
+
```ts
|
|
238
|
+
// __tests__/packages.test.ts
|
|
239
|
+
import { findMonorepoRoot, getWorkspacePackageDirs } from '@williamthorsen/nmr/workspace';
|
|
458
240
|
|
|
459
|
-
|
|
241
|
+
const monorepoRoot = findMonorepoRoot();
|
|
460
242
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
243
|
+
for (const packageDir of getWorkspacePackageDirs(monorepoRoot)) {
|
|
244
|
+
// assert something about every workspace package
|
|
245
|
+
}
|
|
464
246
|
```
|
|
465
247
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
**Retention has a ceiling of 256 KiB per recorded pass**, distinct from the 512 bytes a verdict line is held to. A transcript overrunning it keeps 128 KiB from each end, because a run's opening and its closing statement each carry what the other does not, and the drop is marked with a line naming the bytes it stands for rather than being cut silently.
|
|
469
|
-
|
|
470
|
-
### Bypassing and clearing
|
|
471
|
-
|
|
472
|
-
Reach for these in order; the first is almost always the right one.
|
|
473
|
-
|
|
474
|
-
| Step | Effect |
|
|
475
|
-
| -------------------------------------- | ------------------------------------------------------------------------- |
|
|
476
|
-
| `nmr --no-cache <command>` | Runs the command, then records the fresh result. Reaches the whole chain. |
|
|
477
|
-
| `NMR_NO_CACHE=1` | The same, for every nmr invocation in the shell. |
|
|
478
|
-
| `rm -rf node_modules/.cache/nmr-check` | Forgets every recorded pass, leaving build output alone. |
|
|
479
|
-
| `nmr clean` | Forgets every recorded pass and removes build output, at any scope. |
|
|
480
|
-
|
|
481
|
-
`--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.
|
|
482
|
-
|
|
483
|
-
### Reserved environment variables
|
|
484
|
-
|
|
485
|
-
`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.
|
|
486
|
-
|
|
487
|
-
`NMR_RUN_ID` is nmr's own too: it carries one run's identity from a top-level invocation down to the processes it spawns, so the excerpts recorded at every scope during one run are recognizable as that run's, and a composite can assemble from them. Where `NMR_TREE_SNAPSHOT` is bounded by `HEAD`, this is bounded by the tree hash each entry records, so a process that outlives its run and carries the identity into a later one contributes nothing to that run's assemblies.
|
|
488
|
-
|
|
489
|
-
`NMR_COMMAND_VERBOSITY` is nmr's own as well, and carries how loudly a run reports the output of the commands it runs. Its values are `full` and `quiet`; any other non-empty value is reported and exits 1, rather than falling back to a mode nobody chose, and an empty value reads as unset. Each process in a chain suppresses the output of the command it runs rather than of everything below it, so a failure still surrenders the failing command's output. It governs those commands' output alone: nmr's [verdicts](#what-nmr-reports) print in every verbosity, and the override notice is the one message a quiet run withholds.
|
|
490
|
-
|
|
491
|
-
`-q` sets it for the run and outranks an inherited value. Both values are spelled out so either direction is expressible: export `NMR_COMMAND_VERBOSITY=quiet` for a quiet shell, and set `full` on one invocation to opt back out.
|
|
492
|
-
|
|
493
|
-
`NMR_REPORT_FORMAT` is nmr's own as well, and carries how nmr renders its own verdicts. Its values are `text` and `json`; any other non-empty value is reported and exits 1, and an empty value reads as unset. `--json` sets it for the run and outranks an inherited value, and both values are spelled out so either direction is expressible: export `NMR_REPORT_FORMAT=json` for a shell whose consumer parses, and set `text` on one invocation to opt back out. Two sources, then, where a verbosity has four: no repo-level default and no harness detection, since detection would hand every agent JSON in place of the prose the [shipped guidance](#agent-guidance) teaches. See [reporting for a machine](#reporting-for-a-machine) for what the objects carry.
|
|
494
|
-
|
|
495
|
-
#### Where a verbosity comes from
|
|
496
|
-
|
|
497
|
-
Four sources, in precedence order. Each is read once, at the top of a chain: the resolved value travels down as `NMR_COMMAND_VERBOSITY`, so a nested process reads a decision already made rather than making its own.
|
|
498
|
-
|
|
499
|
-
| Source | Set it when |
|
|
500
|
-
| ----------------------------------------------------- | ---------------------------------------------------------- |
|
|
501
|
-
| `-q` | One invocation should be quiet. |
|
|
502
|
-
| `NMR_COMMAND_VERBOSITY` | A shell, or a harness that launches one, should be quiet. |
|
|
503
|
-
| `output.commandVerbosity` in the monorepo-root config | A repo should be quiet for everyone working in it. |
|
|
504
|
-
| An agent harness nmr recognizes in the environment | Nothing above applies and an agent is running the command. |
|
|
505
|
-
|
|
506
|
-
Falling off the bottom leaves the run `full`, which is what an unrecognized harness gets: detection adds quiet where it fires and changes nothing where it does not.
|
|
248
|
+
[Workspace introspection](docs/workspace.md) covers what each lookup returns and where it diverges from pnpm.
|
|
507
249
|
|
|
508
|
-
|
|
250
|
+
## Agent guidance
|
|
509
251
|
|
|
510
|
-
|
|
252
|
+
nmr ships the rules an agent needs in order to invoke it, as [CodeAssembly](https://github.com/williamthorsen/codeassembly) package content. Adopt it by naming the package in the consuming repo's `.agents/codeassembly.yaml`:
|
|
511
253
|
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
extraCommands: ['verify:contracts'],
|
|
517
|
-
// Names that turned out to do more than report an exit status.
|
|
518
|
-
excludeCommands: ['test:coverage'],
|
|
519
|
-
},
|
|
520
|
-
});
|
|
254
|
+
```yaml
|
|
255
|
+
packages:
|
|
256
|
+
use:
|
|
257
|
+
- '@williamthorsen/nmr'
|
|
521
258
|
```
|
|
522
259
|
|
|
523
|
-
`
|
|
524
|
-
|
|
525
|
-
**A name in either list must resolve to a command**, whether through nmr's defaults, the repo config's script records, or a package's own `package.json` scripts. Both lists are read by name alone, so a misspelt entry would otherwise be inert: the command it meant to name would go on running, indistinguishable from one that cannot be cached at all. A name that resolves nowhere fails config load, naming the closest command where there is one. A hook name is rejected too, since a hook is never gated on its own -- it runs as part of the chain of the command it wraps, and is skipped or run with it.
|
|
260
|
+
Then add `codeassembly` as a devDependency and run `codeassembly sync`. The guidance is injected into the machine-local guidance file each harness loads at launch; nothing is copied into the repo or committed there. It resolves from the installed package, so upgrading nmr updates it with no second step.
|
|
526
261
|
|
|
527
|
-
|
|
262
|
+
Wiring the sync to `postinstall` keeps it current without a hand-run command:
|
|
528
263
|
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
extraAgentEnvVars: ['MY_HARNESS'],
|
|
536
|
-
},
|
|
537
|
-
});
|
|
264
|
+
```json
|
|
265
|
+
{
|
|
266
|
+
"scripts": {
|
|
267
|
+
"postinstall": "codeassembly sync --warn-only"
|
|
268
|
+
}
|
|
269
|
+
}
|
|
538
270
|
```
|
|
539
271
|
|
|
540
|
-
`
|
|
541
|
-
|
|
542
|
-
## Dependency upgrades
|
|
543
|
-
|
|
544
|
-
`nmr upgrade` reports the dependency upgrades available to your repo. What it covers depends on where you run it:
|
|
545
|
-
|
|
546
|
-
| Invocation | Covers | Catalogs |
|
|
547
|
-
| ---------------------------- | ------------------------------------------- | -------- |
|
|
548
|
-
| `nmr upgrade` (from root) | the root `package.json` and every workspace | yes |
|
|
549
|
-
| `nmr upgrade` (in a package) | that package | no |
|
|
550
|
-
| `nmr -F <package> upgrade` | that package, from anywhere | no |
|
|
551
|
-
| `nmr root:upgrade` | the root `package.json` | yes |
|
|
552
|
-
|
|
553
|
-
A [pnpm catalog](https://pnpm.io/catalogs) is declared in `pnpm-workspace.yaml`, and the upgrade tool reads that file only when it sits at the working directory, irrespective of `--recursive`, which is why `root:upgrade` covers every catalog too. A package-scoped invocation therefore cannot see a dependency the catalog declares, and a package whose dependencies are all catalogued would otherwise report itself up to date. It runs [`report-catalog`](#report-catalog) first instead, which names each catalogued dependency and the root a covering pass runs from.
|
|
554
|
-
|
|
555
|
-
Both root-scoped invocations precede their report with any active override, which pnpm declares in `pnpm-workspace.yaml`. An override pins a transitive dependency, so an upgrade masked by one never appears in the report.
|
|
556
|
-
|
|
557
|
-
A `pnpm.overrides` block left in the root `package.json` fails the command instead. pnpm reads no setting from that field as of pnpm 11, so the block pins nothing, while the upgrade tool keeps its own list of dependency fields and goes on rewriting the versions in it under `--write` — a block that looks maintained and governs nothing. Failing ahead of the tool is what keeps that write from happening, on `root:upgrade` as much as on `upgrade`. nmr supports pnpm 11 and later.
|
|
558
|
-
|
|
559
|
-
The upgrade tool ([taze](https://github.com/antfu-collective/taze)) arrives with nmr, so your repo declares no dependency on it. Everything after the command name is passed through, including the range mode:
|
|
272
|
+
`--warn-only` reports a sync failure and exits 0; without it, a failed sync aborts `pnpm install`.
|
|
560
273
|
|
|
561
|
-
|
|
562
|
-
nmr upgrade # minor and patch upgrades, regardless of the range each dependency declares
|
|
563
|
-
nmr upgrade major # major upgrades, for dependencies carrying no per-package ceiling
|
|
564
|
-
nmr upgrade --write # apply the proposals to package.json
|
|
565
|
-
```
|
|
274
|
+
## Conformance checks
|
|
566
275
|
|
|
567
|
-
|
|
276
|
+
nmr publishes a `readyup` kit that checks a consuming repo against the current release: the shared Vitest and Prettier configs, the workspace layout, the root script registry, the test-file conventions ([tier names](docs/vitest.md#test-tiers) and `__tests__` placement), that the repo's own suite gates them, that every `bin` target is a committed wrapper rather than build output that pnpm cannot link at install time, that `files` covers the output each wrapper loads, that no root install script runs `lefthook install` without `lefthook check-install`, and that no `package.json` in the tree declares a `pnpm` field, which pnpm 11 reads no key from. Those settings belong in `pnpm-workspace.yaml`, and `pnpx codemod run pnpm-v10-to-v11` moves them. The kit ships inside the package, so it checks against the nmr version installed rather than whatever a repository ref happens to point at, and a tier added or renamed in nmr reaches the repo on upgrade. A repo that declares the [conventions check](docs/vitest.md#gating-the-test-file-conventions) gates both conventions in its own test run, and the kit then leaves the test files to that check.
|
|
568
277
|
|
|
569
|
-
|
|
278
|
+
Add `readyup` as a devDependency, then name nmr in its config:
|
|
570
279
|
|
|
571
280
|
```ts
|
|
572
|
-
|
|
281
|
+
// .config/readyup.config.ts
|
|
282
|
+
import { defineRdyConfig } from 'readyup';
|
|
573
283
|
|
|
574
|
-
export default
|
|
575
|
-
|
|
576
|
-
// Hold @types/node at the Node major your `engines` floor requires.
|
|
577
|
-
'@types/node': 'minor',
|
|
578
|
-
},
|
|
284
|
+
export default defineRdyConfig({
|
|
285
|
+
packages: ['@williamthorsen/nmr'],
|
|
579
286
|
});
|
|
580
287
|
```
|
|
581
288
|
|
|
582
|
-
`defineConfig` supplies nmr's shared upgrade policy — a seven-day quarantine on brand-new releases, and the pair of settings that report a dependency pinned to an exact version: locked dependencies are included, and the range searched is `minor` — so your config carries only what is specific to your repo. Any setting you declare wins over nmr's default. Passing `undefined` clears a default rather than falling back to it, which is how you hand the quarantine policy back to `pnpm-workspace.yaml`'s `minimumReleaseAge`. `mode` is the exception: the upgrade tool carries a default of its own for it, and clearing nmr's leaves no valid range to search, so name a mode instead.
|
|
583
|
-
|
|
584
|
-
Everything the [taze configuration](https://github.com/antfu-collective/taze#config-file) accepts is accepted here, with five exceptions that reach the upgrade tool from no config file at all: `concurrency`, `githubActions`, `ignoreOtherWorkspaces`, `nodeVersion`, and `requestTimeout`. The tool's CLI writes a default for each of them over whatever the config declares ([taze#317](https://github.com/antfu-collective/taze/issues/317)), so the command line is the only place they can be set. nmr's ReadyUp kit warns when your config declares one, and [request timeout](#request-timeout) below shows how to set it instead.
|
|
585
|
-
|
|
586
|
-
> **Note:** This file is what activates nmr's upgrade policy at all. A repo without one gets neither the release quarantine nor the settings that report a dependency pinned to an exact version, and `nmr upgrade` reports nothing in a repo using pnpm's `savePrefix: ''`. nmr's ReadyUp kit warns when the file is missing.
|
|
587
|
-
|
|
588
|
-
Two consequences of the `minor` range mode the policy declares:
|
|
589
|
-
|
|
590
|
-
- A `packageMode` entry is honored only by the pass whose mode matches it, or by a `default` pass; every other pass drops that dependency rather than narrowing it. Under the policy's `minor` mode, a `patch` ceiling hides its dependency until you run `nmr upgrade patch`, and a `minor` ceiling hides it from `nmr upgrade major`.
|
|
591
|
-
- A `~` range is searched as `^`, so minor upgrades are proposed for it and `--write` applies them under the original `~`. To hold one dependency to patches, give it a `packageMode` entry of `patch` and read it from `nmr upgrade patch`; to hold the whole repo there, declare `mode: 'patch'`.
|
|
592
|
-
|
|
593
|
-
### Request timeout
|
|
594
|
-
|
|
595
|
-
Every `upgrade` script reaches the upgrade tool with a 30-second request timeout, in place of the tool's own 5 seconds.
|
|
596
|
-
|
|
597
|
-
Five seconds is too tight for a registry that proxies npm. The tool fetches a full packument whenever the resolved registry is not `registry.npmjs.org`, and a large one runs to tens of megabytes; the budget covers the whole retry chain rather than a single attempt, so one slow response ends the run with `Timeout requesting "<package>"`. The symptom is an upgrade pass that succeeds or fails according to how warm the tool's cache happens to be.
|
|
598
|
-
|
|
599
|
-
Pass a different value for one run:
|
|
600
|
-
|
|
601
289
|
```bash
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
To change it for every run, override the `upgrade` script in `.config/nmr.config.ts`, keeping the rest of the default. This is nmr's own config, not the `taze.config.ts` above; taze ignores a `rootScripts` key, and nothing reports it:
|
|
606
|
-
|
|
607
|
-
```ts
|
|
608
|
-
import { defineConfig } from '@williamthorsen/nmr/config';
|
|
609
|
-
|
|
610
|
-
export default defineConfig({
|
|
611
|
-
rootScripts: {
|
|
612
|
-
upgrade: 'nmr-report-overrides && nmr-taze --recursive --request-timeout 90000',
|
|
613
|
-
},
|
|
614
|
-
});
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
The override is one command string rather than a list of steps, because a composite element names an nmr command and both halves here are binaries.
|
|
618
|
-
|
|
619
|
-
## Default script registries
|
|
620
|
-
|
|
621
|
-
These scripts are available out of the box. Repo-wide config (tier 2) and per-package overrides (tier 3) can add to or replace any of them.
|
|
622
|
-
|
|
623
|
-
### Workspace scripts
|
|
624
|
-
|
|
625
|
-
| Command | Runs |
|
|
626
|
-
| ---------------- | ----------------------------------------------------------- |
|
|
627
|
-
| `build` | `compile` |
|
|
628
|
-
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
629
|
-
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test` |
|
|
630
|
-
| `clean` | `nmr-clean` |
|
|
631
|
-
| `compile` | `nmr-compile` |
|
|
632
|
-
| `fix` | `lint`, `fmt` |
|
|
633
|
-
| `fix:check` | `fmt:check`, `lint:check` |
|
|
634
|
-
| `fmt` | `nmr-fmt --write` |
|
|
635
|
-
| `fmt:check` | `nmr-fmt --check` |
|
|
636
|
-
| `lint` | `eslint --fix .` |
|
|
637
|
-
| `lint:check` | `eslint .` |
|
|
638
|
-
| `lint:strict` | `strict-lint` |
|
|
639
|
-
| `report-catalog` | `nmr-report-catalog` |
|
|
640
|
-
| `test` | `pnpm exec vitest --project unit --project tool` |
|
|
641
|
-
| `test:all` | `pnpm exec vitest` |
|
|
642
|
-
| `test:coverage` | `pnpm exec vitest --project unit --project tool --coverage` |
|
|
643
|
-
| `test:tool` | `pnpm exec vitest --project tool` |
|
|
644
|
-
| `test:unit` | `pnpm exec vitest --project unit` |
|
|
645
|
-
| `test:watch` | `pnpm exec vitest --project unit --project tool --watch` |
|
|
646
|
-
| `typecheck` | `tsgo --noEmit` |
|
|
647
|
-
| `upgrade` | `nmr-report-catalog && nmr-taze` |
|
|
648
|
-
| `view-coverage` | `open coverage/index.html` |
|
|
649
|
-
|
|
650
|
-
`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).
|
|
651
|
-
|
|
652
|
-
#### Test selections
|
|
653
|
-
|
|
654
|
-
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.
|
|
655
|
-
|
|
656
|
-
| To run | Command |
|
|
657
|
-
| --------------------------------------------- | ----------- |
|
|
658
|
-
| Everything that runs on a bare install | `test` |
|
|
659
|
-
| The fastest tier alone | `test:unit` |
|
|
660
|
-
| Tests reaching a program alone | `test:tool` |
|
|
661
|
-
| Every tier, including those needing a service | `test:all` |
|
|
662
|
-
|
|
663
|
-
`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`.
|
|
664
|
-
|
|
665
|
-
`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.
|
|
666
|
-
|
|
667
|
-
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.
|
|
668
|
-
|
|
669
|
-
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.
|
|
670
|
-
|
|
671
|
-
### Root scripts
|
|
672
|
-
|
|
673
|
-
#### Build and CI
|
|
674
|
-
|
|
675
|
-
| Command | Runs |
|
|
676
|
-
| --------- | ----------------------- |
|
|
677
|
-
| `build` | `-R build` |
|
|
678
|
-
| `ci` | `build`, `check:strict` |
|
|
679
|
-
| `clean` | `nmr-clean` |
|
|
680
|
-
| `prepush` | `audit`, `ci` |
|
|
681
|
-
|
|
682
|
-
`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, audit first. The audit takes seconds and `ci` takes minutes, so a vulnerability surfaces before the long gate runs. It composes `ci` rather than restating its stages, so a stage added to `ci` joins the pre-push run too.
|
|
683
|
-
|
|
684
|
-
The audit reaches the network. With no network, `ci` is the gate that still runs.
|
|
685
|
-
|
|
686
|
-
Neither is bound to a git hook. `prepush` is named for when you run it, not for a hook nmr installs.
|
|
687
|
-
|
|
688
|
-
#### Check and quality
|
|
689
|
-
|
|
690
|
-
| Command | Runs |
|
|
691
|
-
| -------------- | ----------------------------------------------- |
|
|
692
|
-
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
693
|
-
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test` |
|
|
694
|
-
|
|
695
|
-
Neither gate computes coverage: Instrumentation multiplies the wall time of the run they exist to keep short. Coverage is [`test:coverage`](#test-selections), run as a step of its own; a repo that wants it inside the gate [overrides](#configuration) `check:strict`.
|
|
696
|
-
|
|
697
|
-
#### Fix
|
|
698
|
-
|
|
699
|
-
| Command | Runs |
|
|
700
|
-
| ----------- | ------------------------- |
|
|
701
|
-
| `fix:check` | `fmt:check`, `lint:check` |
|
|
702
|
-
|
|
703
|
-
#### Test
|
|
704
|
-
|
|
705
|
-
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.
|
|
706
|
-
|
|
707
|
-
| Command | Runs |
|
|
708
|
-
| --------------- | ---------------------------------------------- |
|
|
709
|
-
| `test` | `root:test`, `-R test` |
|
|
710
|
-
| `test:all` | `root:test:all`, `-R test:all` |
|
|
711
|
-
| `test:coverage` | `root:test`, `-R test:coverage` |
|
|
712
|
-
| `test:tool` | `root:test:tool`, `-R test:tool` |
|
|
713
|
-
| `test:unit` | `root:test:unit`, `-R test:unit` |
|
|
714
|
-
| `test:watch` | `vitest --project unit --project tool --watch` |
|
|
715
|
-
|
|
716
|
-
`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.
|
|
717
|
-
|
|
718
|
-
`test:watch` is the exception to the fan-out shape, and deliberately omits `--config`: bare `vitest` at the monorepo root resolves the root `vitest.config.ts`, whose projects then cover the whole tree in one process. A chain like the others would never advance past its first watcher. To watch root-level files alone, run `nmr root:test --watch`.
|
|
719
|
-
|
|
720
|
-
#### Typecheck
|
|
721
|
-
|
|
722
|
-
| Command | Runs |
|
|
723
|
-
| ----------- | -------------------------------- |
|
|
724
|
-
| `typecheck` | `root:typecheck`, `-R typecheck` |
|
|
725
|
-
|
|
726
|
-
#### Lint
|
|
727
|
-
|
|
728
|
-
The lint commands respect each package's own `eslint.config.*`. Use the `root:` variants below to lint root-level code alone.
|
|
729
|
-
|
|
730
|
-
Requires `eslint` >= 10 and, for `lint:strict`, `@williamthorsen/strict-lint` >= 9.3.0.
|
|
731
|
-
|
|
732
|
-
| Command | Runs |
|
|
733
|
-
| ------------- | ---------------- |
|
|
734
|
-
| `lint` | `eslint --fix .` |
|
|
735
|
-
| `lint:check` | `eslint .` |
|
|
736
|
-
| `lint:strict` | `strict-lint` |
|
|
737
|
-
|
|
738
|
-
The run never enters a package, so per-package `lint:pre` and `lint:post` hooks do not fire from the root. They still run from inside the package and under `nmr -F <package> lint`.
|
|
739
|
-
|
|
740
|
-
#### Format
|
|
741
|
-
|
|
742
|
-
| Command | Runs |
|
|
743
|
-
| ----------- | ----------------- |
|
|
744
|
-
| `fmt` | `nmr-fmt --write` |
|
|
745
|
-
| `fmt:check` | `nmr-fmt --check` |
|
|
746
|
-
|
|
747
|
-
#### File selection
|
|
748
|
-
|
|
749
|
-
`fmt` and `fmt:check` format the files git reports: tracked files, plus untracked files git does not ignore. Prettier reads `.gitignore` and `.prettierignore` from the working directory alone, so a pattern in `packages/<pkg>/.gitignore` never reaches a run started at the monorepo root. git knows the whole hierarchy, including `.git/info/exclude` and `core.excludesFile`, and every `.prettierignore` in the tree is discovered from the repository root, so package-level exclusions apply from any directory as well.
|
|
750
|
-
|
|
751
|
-
A file git ignores is never formatted, even when named directly. Trailing arguments are git pathspecs rather than Prettier flags, so `nmr fmt:check packages/nmr` narrows the run, while an unrecognized option and a pathspec matching no formattable file both fail it. Running outside a git repository fails rather than reporting a clean run.
|
|
752
|
-
|
|
753
|
-
#### Audit
|
|
754
|
-
|
|
755
|
-
| Command | Runs |
|
|
756
|
-
| ------------ | ------------------------- |
|
|
757
|
-
| `audit` | `audit:prod`, `audit:dev` |
|
|
758
|
-
| `audit:dev` | `pnpm exec v11y --dev` |
|
|
759
|
-
| `audit:prod` | `pnpm exec v11y --prod` |
|
|
760
|
-
|
|
761
|
-
#### Dependencies
|
|
762
|
-
|
|
763
|
-
| Command | Runs |
|
|
764
|
-
| --------- | ---------------------------------------------- |
|
|
765
|
-
| `upgrade` | `nmr-report-overrides && nmr-taze --recursive` |
|
|
766
|
-
|
|
767
|
-
See [dependency upgrades](#dependency-upgrades) for the workflow and its configuration.
|
|
768
|
-
|
|
769
|
-
#### Root-only
|
|
770
|
-
|
|
771
|
-
These scripts operate on root-level code only (not workspace packages):
|
|
772
|
-
|
|
773
|
-
| Command | Runs |
|
|
774
|
-
| ------------------ | ----------------------------------------------------------------------- |
|
|
775
|
-
| `root:check` | `root:typecheck`, `fmt:check`, `root:lint:check`, `root:test` |
|
|
776
|
-
| `root:lint` | `eslint --fix --ignore-pattern 'packages/**' .` |
|
|
777
|
-
| `root:lint:check` | `eslint --ignore-pattern 'packages/**' .` |
|
|
778
|
-
| `root:lint:strict` | `strict-lint --ignore-pattern 'packages/**' .` |
|
|
779
|
-
| `root:test` | `vitest --config ./vitest.root.config.ts --project unit --project tool` |
|
|
780
|
-
| `root:test:all` | `vitest --config ./vitest.root.config.ts` |
|
|
781
|
-
| `root:test:tool` | `vitest --config ./vitest.root.config.ts --project tool` |
|
|
782
|
-
| `root:test:unit` | `vitest --config ./vitest.root.config.ts --project unit` |
|
|
783
|
-
| `root:typecheck` | `tsgo --noEmit` |
|
|
784
|
-
| `root:upgrade` | `nmr-report-overrides && nmr-taze` |
|
|
785
|
-
|
|
786
|
-
#### Utilities
|
|
787
|
-
|
|
788
|
-
| Command | Runs |
|
|
789
|
-
| ------------------ | ---------------------- |
|
|
790
|
-
| `report-overrides` | `nmr-report-overrides` |
|
|
791
|
-
|
|
792
|
-
## CLI reference
|
|
793
|
-
|
|
794
|
-
### `nmr`
|
|
795
|
-
|
|
796
|
-
```
|
|
797
|
-
nmr [flags] <command> [args...]
|
|
798
|
-
```
|
|
799
|
-
|
|
800
|
-
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.
|
|
801
|
-
|
|
802
|
-
| Flag | Description | Default |
|
|
803
|
-
| ------------------------ | -------------------------------------------------------------- | ------- |
|
|
804
|
-
| `-F, --filter <pattern>` | Run command in matching packages | — |
|
|
805
|
-
| `-R, --recursive` | Run command in all packages | — |
|
|
806
|
-
| `-w, --workspace-root` | Use root scripts, running at the monorepo root | — |
|
|
807
|
-
| `-q, --quiet` | Suppress command output, keeping nmr's verdicts | — |
|
|
808
|
-
| `--json` | Report one JSON object per command, withholding command output | — |
|
|
809
|
-
| `--log` | Print the recorded run instead of running it | — |
|
|
810
|
-
| `--no-cache` | Run even if this tree already passed; record the result | — |
|
|
811
|
-
| `-?, --help` | Show available commands | — |
|
|
812
|
-
| `-V, --version` | Show version number | — |
|
|
813
|
-
|
|
814
|
-
### Examples
|
|
815
|
-
|
|
816
|
-
```bash
|
|
817
|
-
# From a package directory
|
|
818
|
-
nmr test # Run workspace test script
|
|
819
|
-
nmr build # Compile to .js and .d.ts
|
|
820
|
-
|
|
821
|
-
# From the monorepo root
|
|
822
|
-
nmr test # Root tests + recursive workspace tests
|
|
823
|
-
nmr ci # build + check:strict
|
|
824
|
-
nmr prepush # audit + ci
|
|
825
|
-
|
|
826
|
-
# Target specific packages
|
|
827
|
-
nmr --filter core test # Test only the core package
|
|
828
|
-
nmr --recursive lint # Lint all workspace packages
|
|
829
|
-
|
|
830
|
-
# Force root context from anywhere
|
|
831
|
-
nmr --workspace-root check # Run root check from a package dir
|
|
832
|
-
```
|
|
833
|
-
|
|
834
|
-
## Additional subcommands
|
|
835
|
-
|
|
836
|
-
These commands are available as `nmr` subcommands and as standalone `nmr-`-prefixed binaries (for use in lifecycle hooks).
|
|
837
|
-
|
|
838
|
-
### `report-catalog`
|
|
839
|
-
|
|
840
|
-
Report the dependencies the current package takes from a [pnpm catalog](https://pnpm.io/catalogs), which a package-scoped `upgrade` cannot reach (see [dependency upgrades](#dependency-upgrades) for why). Each line names the dependency, its `catalog:` specifier, and the monorepo root a covering pass runs from. The workspace `upgrade` script runs it automatically, so no per-repo wiring is needed. The subcommand belongs to the workspace registry, so it runs from a package directory; the `nmr-report-catalog` bin reports nothing when run from the monorepo root, because a root-scoped pass reads the catalog itself.
|
|
841
|
-
|
|
842
|
-
```bash
|
|
843
|
-
nmr report-catalog
|
|
844
|
-
```
|
|
845
|
-
|
|
846
|
-
### `report-overrides`
|
|
847
|
-
|
|
848
|
-
Report any active pnpm override, declared in the root `pnpm-workspace.yaml`, reminding developers of overrides that may need cleanup. The root `upgrade` script runs it automatically, so no per-repo wiring is needed. Invoking it directly is useful for a one-off report.
|
|
849
|
-
|
|
850
|
-
A `pnpm.overrides` block in the root `package.json` fails the command, naming every entry it holds and where to move it — see [dependency upgrades](#dependency-upgrades) for why the block is inert. `pnpx codemod run pnpm-v10-to-v11` performs the move.
|
|
851
|
-
|
|
852
|
-
```bash
|
|
853
|
-
nmr report-overrides
|
|
854
|
-
```
|
|
855
|
-
|
|
856
|
-
## Agent guidance
|
|
857
|
-
|
|
858
|
-
nmr ships the rules an agent needs in order to invoke it, as [CodeAssembly](https://github.com/williamthorsen/codeassembly) package content. Adopt it by naming the package in the consuming repo's `.agents/codeassembly.yaml`:
|
|
859
|
-
|
|
860
|
-
```yaml
|
|
861
|
-
packages:
|
|
862
|
-
use:
|
|
863
|
-
- '@williamthorsen/nmr'
|
|
864
|
-
```
|
|
865
|
-
|
|
866
|
-
Then add `codeassembly` as a devDependency and run `codeassembly sync`. The guidance is injected into the machine-local guidance file each harness loads at launch; nothing is copied into the repo or committed there. It resolves from the installed package, so upgrading nmr updates it with no second step.
|
|
867
|
-
|
|
868
|
-
Wiring the sync to `postinstall` keeps it current without a hand-run command:
|
|
869
|
-
|
|
870
|
-
```json
|
|
871
|
-
{
|
|
872
|
-
"scripts": {
|
|
873
|
-
"postinstall": "codeassembly sync --warn-only"
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
```
|
|
877
|
-
|
|
878
|
-
`--warn-only` reports a sync failure and exits 0; without it, a failed sync aborts `pnpm install`.
|
|
879
|
-
|
|
880
|
-
### Migrating from `sync-agent-files`
|
|
881
|
-
|
|
882
|
-
The `nmr sync-agent-files` command, its `nmr-sync-agent-files` bin, and the `check:agent-files` script were removed in favor of the above. After upgrading, delete the generated `.agents/nmr/` directory and the `@nmr/AGENTS.md` line it was exposed through, drop `check:agent-files` from any `check:strict` override that enumerates it, and remove any direct call to the bin.
|
|
883
|
-
|
|
884
|
-
## Standalone utilities
|
|
885
|
-
|
|
886
|
-
### `nmr-clean`
|
|
887
|
-
|
|
888
|
-
Remove a package's build output (`dist`) and its `nmr-compile` cache entry, leaving no state behind for the next build to skip on. Run from a package directory it cleans that package; run from the monorepo root it sweeps every workspace package in a single pass, running each package's resolved `clean` — so a package that overrides `clean`, in `.config/nmr.config.ts` or in its own `package.json`, gets its own command rather than the sweep. Removal is idempotent — cleaning an unbuilt package is a silent no-op. This is the default `clean` script at both levels.
|
|
889
|
-
|
|
890
|
-
```bash
|
|
891
|
-
nmr-clean
|
|
892
|
-
```
|
|
893
|
-
|
|
894
|
-
### `nmr-compile`
|
|
895
|
-
|
|
896
|
-
Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` from one program and `.d.ts` from a second that reuses it. The package's own `removeComments` governs the `.js` emit; the declarations emit forces it off, so `.d.ts` files carry their doc comments either way. Because the compiler parses each source file, every relative import form — static, re-export, dynamic `import()`, and bare side-effect — is rewritten from `.ts` to `.js` in both outputs, and `.ts` occurrences inside strings and comments are left intact. tsconfig `paths` aliases are resolved to runnable relative `.js` specifiers in both outputs, sourced from the package's tsconfig. An aliased import whose target resolves outside the package's `src/` and is not resolvable without the alias mapping fails the build with a diagnostic, rather than being emitted verbatim to produce output that fails at runtime. The build is skipped when no input has changed and the previous output is still on disk (the key is cached under `node_modules/.cache/nmr-compile/`, outside the published output). Deleting the output by any means — `nmr clean`, `rm -rf dist`, `git clean` — therefore forces a rebuild rather than a skip. This is the default `compile` script — run it from a package directory.
|
|
897
|
-
|
|
898
|
-
**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.
|
|
899
|
-
|
|
900
|
-
**The build owns its output directory.** Every rebuild replaces `dist/esm` wholesale, 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 output is published 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 replaced.
|
|
901
|
-
|
|
902
|
-
**The replacement is atomic.** The emit is buffered in memory, written to a staging directory beside the output directory, and swapped into place by rename. A build that fails for any reason -- a malformed tsconfig, an unresolvable alias, a full disk -- therefore leaves the previous output exactly as it was. `dist/esm` is never observed mid-write: it holds the previous build or the new one, and is absent only between the two renames. This matters most when the package being rebuilt supplies a binary that other packages in the same recursive build invoke.
|
|
903
|
-
|
|
904
|
-
**What busts the cache.** The key folds each input's content and path, the emit options, the resolved TypeScript version, and a fingerprint of the nmr running the build -- its own build digest in a workspace checkout, its package version otherwise. Upgrading nmr in a consuming repo therefore rebuilds every package nmr compiles, and an edit to nmr's own build sources rebuilds each sibling once nmr itself has been rebuilt. A build of nmr's own package folds the version rather than the digest, which is the cache entry that build is about to write.
|
|
905
|
-
|
|
906
|
-
`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.
|
|
907
|
-
|
|
908
|
-
```bash
|
|
909
|
-
nmr-compile
|
|
910
|
-
```
|
|
911
|
-
|
|
912
|
-
### `nmr-fmt`
|
|
913
|
-
|
|
914
|
-
Format, or check the formatting of, the files git reports for the working directory. Exactly one of `--check` and `--write` is required; a bare invocation prints usage and exits non-zero rather than defaulting to a mutation. `--write` also lists the files it rewrote. This is what `fmt` and `fmt:check` resolve to, in both registries.
|
|
915
|
-
|
|
916
|
-
The selection is tracked files plus untracked files git does not ignore, scoped to the working directory, with every `.prettierignore` in the tree discovered from the repository root and passed to Prettier explicitly — see [file selection](#file-selection) for what that buys. Paths the index names but the filesystem does not have (a file deleted but not yet staged) and submodule gitlinks are both dropped, so neither reaches Prettier.
|
|
917
|
-
|
|
918
|
-
Trailing arguments are git pathspecs, not Prettier flags: they narrow the selection, an unrecognized option is rejected rather than forwarded, and pathspecs that match no formattable file fail rather than passing quietly.
|
|
919
|
-
|
|
920
|
-
`prettier` is an optional peer dependency (`>=3.9.5 <4`); the consuming repo provides it, and it is resolved through the module graph rather than from `PATH`, so the copy that runs is the one the repo declares and not whichever `prettier` happens to come first. A repository's formatter has to be the one its editor and pre-commit hook also run, which is why nmr takes it from the consumer instead of bundling a copy. It is optional because a repo can use nmr purely as a script runner; one that formats without a resolvable Prettier gets a message naming the package and the range.
|
|
921
|
-
|
|
922
|
-
The floor is a currency policy, not a capability boundary. What the design requires is `--ignore-path` honouring every flag rather than only the last, so a repository-root ignore file passed alongside a package-level one is not silently dropped — Prettier has done that since 3.0.0, while 2.x honoured only the final flag. The floor sits at the current release because every consuming repo tracks it; lowering it to `>=3.0.0` would cost correctness nothing.
|
|
923
|
-
|
|
924
|
-
```bash
|
|
925
|
-
nmr-fmt --check
|
|
926
|
-
nmr-fmt --write packages/nmr
|
|
927
|
-
```
|
|
928
|
-
|
|
929
|
-
### `nmr-taze`
|
|
930
|
-
|
|
931
|
-
Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched and adding one of its own: a 30-second request timeout, unless the invocation already carries one. This is what every `upgrade` chain ends with, `root:upgrade` included — see [dependency upgrades](#dependency-upgrades) for the workflow, and [request timeout](#request-timeout) for why the timeout is set here.
|
|
932
|
-
|
|
933
|
-
Under pnpm's isolated `node_modules`, a transitive package's binary is absent from the consuming repo's `node_modules/.bin`, so a repo that depends on nmr cannot run `taze` directly. `nmr-taze` can, because nmr is a direct dependency, and it resolves the tool from the tree nmr controls.
|
|
934
|
-
|
|
935
|
-
```bash
|
|
936
|
-
nmr-taze --recursive
|
|
937
|
-
```
|
|
938
|
-
|
|
939
|
-
### `ensure-prepublish-hooks`
|
|
940
|
-
|
|
941
|
-
Verify that all publishable workspace packages have a `prepublishOnly` script. Exits non-zero if any are missing.
|
|
942
|
-
|
|
943
|
-
```bash
|
|
944
|
-
ensure-prepublish-hooks
|
|
945
|
-
```
|
|
946
|
-
|
|
947
|
-
| Flag | Description | Default |
|
|
948
|
-
| --------------------- | ----------------------------- | ----------------- |
|
|
949
|
-
| `--fix` | Add missing hooks | — |
|
|
950
|
-
| `--dry-run` | Preview what `--fix` would do | — |
|
|
951
|
-
| `--command <command>` | Custom hook command | `"npm run build"` |
|
|
952
|
-
|
|
953
|
-
## Conformance checks
|
|
954
|
-
|
|
955
|
-
nmr publishes a `readyup` kit that checks a consuming repo against the current release: the shared Vitest and Prettier configs, the workspace layout, the root script registry, the [test-tier convention](#test-tiers), that every `bin` target is a committed wrapper rather than build output that pnpm cannot link at install time, that `files` covers the output each wrapper loads, and that no `package.json` in the tree declares a `pnpm` field, which pnpm 11 reads no key from. Those settings belong in `pnpm-workspace.yaml`, and `pnpx codemod run pnpm-v10-to-v11` moves them. The kit ships inside the package, so it checks against the nmr version installed rather than whatever a repository ref happens to point at, and a tier added or renamed in nmr reaches the repo on upgrade. A repo that wants the tier convention in its own gate rather than in a command run by hand declares the [conventions check](#gating-the-test-file-conventions) instead, which also covers the half the kit does not.
|
|
956
|
-
|
|
957
|
-
Add `readyup` as a devDependency, then name nmr in its config:
|
|
958
|
-
|
|
959
|
-
```ts
|
|
960
|
-
// .config/readyup.config.ts
|
|
961
|
-
import { defineRdyConfig } from 'readyup';
|
|
962
|
-
|
|
963
|
-
export default defineRdyConfig({
|
|
964
|
-
packages: ['@williamthorsen/nmr'],
|
|
965
|
-
});
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
```bash
|
|
969
|
-
rdy run --packages # every kit each listed package publishes
|
|
970
|
-
rdy run --from npm:@williamthorsen/nmr # nmr's kit alone, without the config entry
|
|
971
|
-
rdy list --from npm:@williamthorsen/nmr # what nmr publishes
|
|
290
|
+
rdy run --packages # every kit each listed package publishes
|
|
291
|
+
rdy run --from npm:@williamthorsen/nmr # nmr's kit alone, without the config entry
|
|
292
|
+
rdy list --from npm:@williamthorsen/nmr # what nmr publishes
|
|
972
293
|
```
|
|
973
294
|
|
|
974
295
|
`--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.
|
|
975
296
|
|
|
976
|
-
##
|
|
977
|
-
|
|
978
|
-
After installing, a consuming repo's root `package.json` scripts shrink to lifecycle hooks:
|
|
979
|
-
|
|
980
|
-
```json
|
|
981
|
-
{
|
|
982
|
-
"prepare": "lefthook install"
|
|
983
|
-
}
|
|
984
|
-
```
|
|
985
|
-
|
|
986
|
-
Per-package `package.json` files no longer need script entries. Run `nmr <command>` directly.
|
|
987
|
-
|
|
988
|
-
## Workspace introspection
|
|
989
|
-
|
|
990
|
-
Repo-wide tests and scripts often need to know where the monorepo root is, or which directories its workspace packages live in. The `@williamthorsen/nmr/workspace` subpath publishes the two pnpm-workspace lookups nmr uses internally:
|
|
991
|
-
|
|
992
|
-
```ts
|
|
993
|
-
// __tests__/packages.test.ts
|
|
994
|
-
import { findMonorepoRoot, getWorkspacePackageDirs } from '@williamthorsen/nmr/workspace';
|
|
995
|
-
|
|
996
|
-
const monorepoRoot = findMonorepoRoot();
|
|
997
|
-
|
|
998
|
-
for (const packageDir of getWorkspacePackageDirs(monorepoRoot)) {
|
|
999
|
-
// assert something about every workspace package
|
|
1000
|
-
}
|
|
1001
|
-
```
|
|
1002
|
-
|
|
1003
|
-
`findMonorepoRoot(startDir?)` walks up from `startDir`, defaulting to `process.cwd()`, until it reaches a directory containing `pnpm-workspace.yaml`. It throws if it runs out of parent directories without finding one.
|
|
1004
|
-
|
|
1005
|
-
`getWorkspacePackageDirs(monorepoRoot)` reads the workspace patterns from that repo's `pnpm-workspace.yaml` and resolves them to absolute package directories, sorted and free of duplicates. It throws if `monorepoRoot` holds no `pnpm-workspace.yaml`. Patterns carry pnpm's own semantics: `packages/*`, deeper globs such as `packages/**`, exact paths such as `tools/cli`, and `!`-prefixed exclusions such as `!packages/legacy` or `!**/test/**`, which filter every directory the positive patterns matched regardless of where they appear in the list. Nothing under `node_modules` is ever returned.
|
|
1006
|
-
|
|
1007
|
-
One divergence from pnpm: a directory counts as a package only if it holds a `package.json`, not a `package.yaml` or `package.json5`.
|
|
1008
|
-
|
|
1009
|
-
Quote exclusion patterns in the manifest — `- '!packages/legacy'`. An unquoted `!` opens a YAML tag rather than a string, so the entry never reaches nmr (or pnpm) as a pattern.
|
|
1010
|
-
|
|
1011
|
-
## Shared Prettier config
|
|
1012
|
-
|
|
1013
|
-
Every repo consuming nmr otherwise maintains its own copy of the house Prettier options, and wires up shell formatting itself. The `@williamthorsen/nmr/prettier` subpath publishes both as a factory, so a repo declares only what it customizes:
|
|
1014
|
-
|
|
1015
|
-
```js
|
|
1016
|
-
// .prettierrc.js
|
|
1017
|
-
import { definePrettierConfig } from '@williamthorsen/nmr/prettier';
|
|
1018
|
-
|
|
1019
|
-
export default definePrettierConfig();
|
|
1020
|
-
```
|
|
1021
|
-
|
|
1022
|
-
`prettier` is a peer dependency (`>=3.9.5 <4`), declared optional — the consuming repo provides it. The shell and Dockerfile plugin is a dependency of nmr rather than a peer, so its version is pinned centrally and two repos cannot format the same script differently.
|
|
1023
|
-
|
|
1024
|
-
### What it formats
|
|
1025
|
-
|
|
1026
|
-
Beyond everything Prettier already handles, this config formats `.sh`, `.bash`, `.zsh`, and the other extensions and dotfile names of the Shell language, plus `Dockerfile` and `Containerfile`. `nmr fmt` therefore covers shell scripts with no extra script, glob, or workflow entry.
|
|
1027
|
-
|
|
1028
|
-
Shell output matches `shfmt` run with no flags: `binaryNextLine`, `spaceRedirects`, and `switchCaseIndent` are pinned to shfmt's CLI defaults, which the underlying plugin inverts. A repo whose scripts shfmt already formatted sees no diff on adoption.
|
|
1029
|
-
|
|
1030
|
-
Code fences inside Markdown are left alone, in every language rather than shell alone. Registering the shell plugin also routes a fence tagged `bash` to shfmt, where a documented command's angle-bracket placeholders are valid redirections: `cmd --type <type> --harness claude` is reprinted as `cmd --type claude <type >--harness`, which still runs but no longer does what it documents. It cannot be narrowed back to shell, because Prettier matches a fence tag against the same `extensions` its file inference reads, so the `.bash` routing a script also claims the tag `bash`.
|
|
1031
|
-
|
|
1032
|
-
### Language scoping
|
|
1033
|
-
|
|
1034
|
-
`prettier-plugin-sh` routes 20 further file types to the same shell parser, among them `.gitignore`, `.env`, `.csh`, `.nu`, `.properties`, `.ics`, `.vcf`, `CODEOWNERS`, and `hosts`. Since `nmr fmt` hands git's whole file list to Prettier, registering the plugin as shipped would make every one of them formattable — and the shell parser either fails on them or silently rewrites them. A `.gitignore` pattern such as `a(b)c` fails to parse, and an unquoted `&` in a `.env` or `.properties` value is split across two lines.
|
|
1035
|
-
|
|
1036
|
-
This config registers a narrowed plugin, so Prettier infers a parser for the Shell and Dockerfile languages alone. `.flaskenv`, `gradlew`, and `mvnw` are dropped from Shell as well: the first holds dotenv content the plugin misfiles as shell, and the other two are vendored wrappers their generators would overwrite.
|
|
1037
|
-
|
|
1038
|
-
The parsers themselves stay registered, so a repo that wants a dropped language back assigns it explicitly:
|
|
1039
|
-
|
|
1040
|
-
```js
|
|
1041
|
-
export default definePrettierConfig({
|
|
1042
|
-
additionalOverrides: [{ files: ['APKBUILD', '*.ebuild'], options: { parser: 'sh' } }],
|
|
1043
|
-
});
|
|
1044
|
-
```
|
|
1045
|
-
|
|
1046
|
-
### Customizing
|
|
1047
|
-
|
|
1048
|
-
Any Prettier option passed to the factory spreads over the defaults:
|
|
1049
|
-
|
|
1050
|
-
```js
|
|
1051
|
-
export default definePrettierConfig({
|
|
1052
|
-
printWidth: 100,
|
|
1053
|
-
additionalPlugins: [await import('prettier-plugin-tailwindcss')],
|
|
1054
|
-
additionalOverrides: [{ files: ['*.md'], options: { proseWrap: 'always' } }],
|
|
1055
|
-
});
|
|
1056
|
-
```
|
|
1057
|
-
|
|
1058
|
-
`additionalPlugins` and `additionalOverrides` append to what the config declares, so a later override wins over nmr's own and no seam can drop shell support. Passing `plugins` or `overrides` throws, and both are typed `never` — replacement is inexpressible rather than merely discouraged, because a silently ignored key would produce no diff to notice.
|
|
1059
|
-
|
|
1060
|
-
### Adoption caveats
|
|
1061
|
-
|
|
1062
|
-
- **Indentation follows `.editorconfig`**, as it did under shfmt: Prettier maps `indent_style` and `indent_size` to `useTabs` and `tabWidth`, and the plugin honours both. With no `.editorconfig`, the two disagree — shfmt indents shell with tabs, Prettier with two spaces. Add `indent_style = tab` under `[*.sh]` to keep tabs.
|
|
1063
|
-
- **Markdown fences stop being tidied.** The carve-out that protects documented shell commands costs the `ts`, `json`, and `yaml` fences their formatting too. Fences already formatted stay as they are, so nothing churns on adoption; new ones are simply left as written. A repo whose docs hold no shell can take them back for the Markdown paths it actually uses: `additionalOverrides: [{ files: ['*.md'], options: { embeddedLanguageFormatting: 'auto' } }]` restores `.md` alone, and the carve-out still covers every other path Prettier reads as Markdown.
|
|
1064
|
-
- **Adopting the house options reformats the repo.** `singleQuote`, `trailingComma`, and the rest apply on the first run; review that diff separately from the shell one.
|
|
1065
|
-
|
|
1066
|
-
## Shared Vitest config
|
|
1067
|
-
|
|
1068
|
-
Every repo consuming nmr otherwise writes and maintains its own Vitest config. The `@williamthorsen/nmr/vitest` subpath publishes that config as a factory, so a repo declares only what it customizes:
|
|
1069
|
-
|
|
1070
|
-
```ts
|
|
1071
|
-
// vitest.config.ts
|
|
1072
|
-
import { defineVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
1073
|
-
|
|
1074
|
-
export default defineVitestConfig();
|
|
1075
|
-
```
|
|
1076
|
-
|
|
1077
|
-
`vitest` is a peer dependency (`>=4.0.0 <5`), declared optional — the consuming repo provides it, and repos that never import this subpath are unaffected. `vite` is an optional peer as well (`>=8.0.0 <9`), required only by the [`tsconfigPaths` setting](#resolving-through-tsconfig-paths); a repo that declares a `vite` of its own below that range sees an unmet-peer warning whether or not it sets the flag.
|
|
1078
|
-
|
|
1079
|
-
### Test tiers
|
|
1080
|
-
|
|
1081
|
-
The config declares four projects, an ordered ladder named for the furthest thing a test reaches:
|
|
1082
|
-
|
|
1083
|
-
| Project | Matches | Reaches |
|
|
1084
|
-
| ----------- | -------------------------------------- | ---------------------------------------------------- |
|
|
1085
|
-
| `unit` | every test file the others don't claim | nothing outside the package's own dependency closure |
|
|
1086
|
-
| `tool` | `*.tool.test.{ts,tsx}` | a program the environment must supply |
|
|
1087
|
-
| `localhost` | `*.localhost.test.{ts,tsx}` | a service running on this machine |
|
|
1088
|
-
| `remote` | `*.remote.test.{ts,tsx}` | a machine that isn't this one |
|
|
1089
|
-
|
|
1090
|
-
All four match only under a `__tests__` directory. Select them at run time with `--project`, which unions when repeated and accepts negation:
|
|
1091
|
-
|
|
1092
|
-
```bash
|
|
1093
|
-
vitest --project unit --project tool # everything that runs on a bare install
|
|
1094
|
-
vitest --project tool # tests reaching a program, alone
|
|
1095
|
-
vitest # every project
|
|
1096
|
-
```
|
|
1097
|
-
|
|
1098
|
-
**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`.
|
|
1099
|
-
|
|
1100
|
-
**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.
|
|
1101
|
-
|
|
1102
|
-
**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`.
|
|
1103
|
-
|
|
1104
|
-
`unit` is defined by subtracting the tiers rather than by an allow-list of infixes, so a file such as `parser.smoke.test.ts` runs under `unit` instead of being silently dropped. That is a safety net, not a licence to omit the tier: a file whose tier segment is missing or misspelt runs under `unit` and reports success, so no test run distinguishes it from a conformant one until the repo declares the [conventions check](#gating-the-test-file-conventions). In a repo that has not, [nmr's readyup kit](#conformance-checks) is what reports those files.
|
|
1105
|
-
|
|
1106
|
-
`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.
|
|
1107
|
-
|
|
1108
|
-
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.
|
|
1109
|
-
|
|
1110
|
-
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.
|
|
1111
|
-
|
|
1112
|
-
### Gating the test-file conventions
|
|
1113
|
-
|
|
1114
|
-
Both halves of the convention are silent on their own: an untiered file runs under the residual `unit` and passes, and a file outside `__tests__` is collected by nothing at all. `@williamthorsen/nmr/tests` exports a check that reports both, which a repo declares from a one-line test of its own:
|
|
1115
|
-
|
|
1116
|
-
```ts
|
|
1117
|
-
// __tests__/test-file-conventions.unit.test.ts
|
|
1118
|
-
import { checkTestFileConventions } from '@williamthorsen/nmr/tests';
|
|
1119
|
-
|
|
1120
|
-
checkTestFileConventions();
|
|
1121
|
-
```
|
|
1122
|
-
|
|
1123
|
-
It declares one suite with an assertion per half, each carrying the remedy that fixes it: the four tier names and the rename form, or the `__tests__` directory the file belongs in. The guard names its own tier like any other test file, so a misnamed guard reports itself.
|
|
1124
|
-
|
|
1125
|
-
A repo linting with `vitest/require-hook` needs a disable directive on that call, which the rule reads as setup work where it declares the suite:
|
|
1126
|
-
|
|
1127
|
-
```ts
|
|
1128
|
-
// eslint-disable-next-line vitest/require-hook -- the call declares the suite, where the rule reads it as setup work.
|
|
1129
|
-
checkTestFileConventions();
|
|
1130
|
-
```
|
|
1131
|
-
|
|
1132
|
-
The sweep starts at the monorepo root rather than at the directory Vitest supplies, so one call covers the whole repo from whichever package the run began in. `rootDir` overrides that, for a check pointed at a tree other than the repo's own.
|
|
1133
|
-
|
|
1134
|
-
An exported check rather than a step nmr runs for you, because nothing nmr runs reaches the whole tree: `nmr test` fans out per package, and a direct `vitest` invocation runs no nmr script at all. A test the repo owns also lets the repo scope what the sweep covers.
|
|
1135
|
-
|
|
1136
|
-
`exclude` names directory basenames the sweep prunes at any depth, additive to the ones nmr always prunes:
|
|
1137
|
-
|
|
1138
|
-
```ts
|
|
1139
|
-
checkTestFileConventions({ exclude: ['cypress', 'generated'] });
|
|
1140
|
-
```
|
|
1141
|
-
|
|
1142
|
-
**Pass the same array to [`testCollectionExclude`](#what-the-config-excludes).** The two describe one scope, and naming a directory in only one is a defect in either direction. Pruned from the sweep alone, the directory's test files still run and still report nothing, which is the silence this check exists to end. Excluded from collection alone, the sweep reports files a consumer has no reason to act on.
|
|
1143
|
-
|
|
1144
|
-
Expect the first run to fail in a repo that has never gated this. Vitest 4 excludes only `node_modules` and `.git` by default, so a `__tests__` tree under a generated or vendored directory is collected and runs today; naming that directory in both lists is the fix, rather than widening what nmr prunes for everyone.
|
|
1145
|
-
|
|
1146
|
-
### What the config supplies
|
|
1147
|
-
|
|
1148
|
-
Two settings a workspace test runner needs almost universally are part of what the factory produces, so a config file declares neither:
|
|
1149
|
-
|
|
1150
|
-
- **Workspace packages resolve from source.** `source` leads the emitted `resolve.conditions` and `ssr.resolve.conditions`, so a cross-package import reaches the dependency's `.ts` rather than its `dist/` and a suite runs without a prior build. A package declaring no such condition is unaffected.
|
|
1151
|
-
- **Git subprocesses are isolated.** Every project loads a setup file that points `GIT_CONFIG_GLOBAL` and `GIT_CONFIG_SYSTEM` at the null device, sets `GIT_CONFIG_NOSYSTEM` and `GIT_ATTR_NOSYSTEM`, and injects `core.excludesFile` and `core.attributesFile` through `GIT_CONFIG_COUNT` and its key/value pairs, so a test that spawns `git` reads neither the developer's identity, nor a signing config that can block on a passphrase, nor their global ignore rules and attributes. Those last two need a step of their own: neither path is config-derived, so git resolves each from `$XDG_CONFIG_HOME/git/`, or from `~/.config/git/` where that variable is unset, whatever the config variables say. `GIT_CONFIG_GLOBAL` already replaces `$XDG_CONFIG_HOME/git/config`, so those two are the rest of what that directory supplies; the system attributes file at `$(prefix)/etc/gitattributes` is reached by no config variable at all, which is what `GIT_ATTR_NOSYSTEM` covers. Redirecting `XDG_CONFIG_HOME` would reach the per-user pair as well, and is not used, because other tools spawned by a test read that variable for configuration of their own: pnpm fails to start under `XDG_CONFIG_HOME=/dev/null`. An injected key outranks repository-local config, so a repository setting its own `core.excludesFile` is overridden under the isolation; a test needing its own value passes it on the invocation as `git -c core.excludesFile=...`, which outranks the injection. The setup file leads each project's `setupFiles`, ahead of every entry the layers add, because a shared setup establishes the environment the rest run in.
|
|
1152
|
-
|
|
1153
|
-
Both defaults exist because omitting either is silent rather than loud: the tiers still exist and the suite still runs green, testing `dist/` and the developer's git identity instead. Turn either off through its own option, which folds across layers like any other, the last layer to declare it winning:
|
|
1154
|
-
|
|
1155
|
-
```ts
|
|
1156
|
-
export default defineVitestConfig({ isolateGit: false, resolveFromSource: false });
|
|
1157
|
-
```
|
|
1158
|
-
|
|
1159
|
-
`resolveFromSource: false` is for a repo that resolves a `source` condition it does not want in tests; `isolateGit: false` is for a suite meant to read the developer's own git configuration.
|
|
1160
|
-
|
|
1161
|
-
**Vite replaces its condition defaults rather than extending them.** A config writing `conditions: ['source']` by hand therefore drops `module`, so a dependency exposing a `module` entry falls through to whatever its `exports` lists next. A replaced list still resolves `node` and `development` under Vitest, which is why nothing in a test run reports the loss. The factory emits `source` alongside Vite's defaults for each environment, so a config declaring no conditions of its own starts from a complete list.
|
|
1162
|
-
|
|
1163
|
-
**A package declaring a `source` condition must ship its sources.** Every nmr consumer now resolves that condition, so a published package whose `files` carries `dist` alone resolves to a path that is not in the tarball. A package that publishes only its build output takes a bespoke condition name instead, the way `@williamthorsen/nmr-core` uses `nmr-source` for the build's own bootstrap.
|
|
1164
|
-
|
|
1165
|
-
### Resolving through tsconfig paths
|
|
1166
|
-
|
|
1167
|
-
A third resolution setting sits alongside those two, and this one is off. `tsconfigPaths: true` emits Vite's `resolve.tsconfigPaths`, so a test reaches an aliased specifier through the `paths` its `tsconfig.json` declares, the way `tsc` does:
|
|
1168
|
-
|
|
1169
|
-
```ts
|
|
1170
|
-
export default defineVitestConfig({ tsconfigPaths: true });
|
|
1171
|
-
```
|
|
1172
|
-
|
|
1173
|
-
It folds across layers like the other two, the last layer to declare it winning. It needs no `ssr` counterpart the way a condition does: Vite holds this setting outside its per-environment resolve options, so the single key reaches the environment a test's own imports resolve through.
|
|
1174
|
-
|
|
1175
|
-
**It is an opt-in because its silent direction is the reverse of the two defaults'.** Leaving it off fails loudly, with an unresolved import naming the specifier. Turning it on is the silent direction, for a repo that declares `paths` for `tsc` alone: a specifier that resolved through a package's `exports` starts resolving through the alias instead, and nothing in the run reports the switch. A repo declaring no `paths` is unaffected either way.
|
|
1176
|
-
|
|
1177
|
-
**It requires Vite 8**, where `resolve.tsconfigPaths` arrived. nmr declares `vite` as an optional peer at `>=8.0.0 <9`, so a repo holding a stale `vite` of its own hears about it at install time. That check does not reach a repo whose only Vite comes in through Vitest's own dependency, and Vitest 4 accepts Vite 6 and 7: on those, the emitted key does nothing and the aliased import fails exactly as it would with the flag off.
|
|
1178
|
-
|
|
1179
|
-
### Customizing by scope
|
|
1180
|
-
|
|
1181
|
-
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:
|
|
1182
|
-
|
|
1183
|
-
```ts
|
|
1184
|
-
export default defineVitestConfig({
|
|
1185
|
-
// Vite-level options, plus the test options Vitest honours only at the root.
|
|
1186
|
-
root: { ssr: { resolve: { conditions: ['my-condition'] } } },
|
|
1187
|
-
// Applied to every project.
|
|
1188
|
-
project: { setupFiles: ['./vitest.setup.ts'] },
|
|
1189
|
-
// Applied to one tier, after the `project` block above.
|
|
1190
|
-
tiers: { tool: { testTimeout: 120_000 } },
|
|
1191
|
-
});
|
|
1192
|
-
```
|
|
1193
|
-
|
|
1194
|
-
`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.
|
|
1195
|
-
|
|
1196
|
-
`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.
|
|
1197
|
-
|
|
1198
|
-
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.
|
|
1199
|
-
|
|
1200
|
-
`resolve.conditions` concatenates too, onto the `source` condition and the Vite defaults the factory already emits, 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; removing `source` is what `resolveFromSource: false` is for. `resolve.alias` is the one key that merges override-first, so a later alias takes precedence over an earlier one.
|
|
1201
|
-
|
|
1202
|
-
**A condition for the tests' own resolution goes under `ssr`.** `resolve` is per-environment, and Vitest resolves a test's imports through the server environment, so `root: { ssr: { resolve: { conditions: ['my-condition'] } } }` is the seam that reaches them. A top-level `root: { resolve: { conditions: [...] } }` entry reaches the client environment, which only browser-mode tests resolve through; it composes into that array and never reports that a node test did not see it. `resolve.alias` is not per-environment, so a top-level alias reaches both.
|
|
1203
|
-
|
|
1204
|
-
### Sharing options across config files
|
|
1205
|
-
|
|
1206
|
-
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:
|
|
1207
|
-
|
|
1208
|
-
```ts
|
|
1209
|
-
// vitest.shared.ts
|
|
1210
|
-
import { fileURLToPath } from 'node:url';
|
|
1211
|
-
|
|
1212
|
-
import type { VitestConfigOptions } from '@williamthorsen/nmr/vitest';
|
|
1213
|
-
|
|
1214
|
-
export const shared: VitestConfigOptions = {
|
|
1215
|
-
root: { resolve: { alias: { '~': fileURLToPath(new URL('.', import.meta.url)) } } },
|
|
1216
|
-
project: { setupFiles: [fileURLToPath(new URL('./vitest.setup.ts', import.meta.url))] },
|
|
1217
|
-
};
|
|
1218
|
-
```
|
|
1219
|
-
|
|
1220
|
-
```ts
|
|
1221
|
-
// packages/web/vitest.config.ts
|
|
1222
|
-
import { defineVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
1223
|
-
|
|
1224
|
-
import { shared } from '../../vitest.shared.ts';
|
|
1225
|
-
|
|
1226
|
-
export default defineVitestConfig(shared, { project: { environment: 'jsdom' } });
|
|
1227
|
-
```
|
|
1228
|
-
|
|
1229
|
-
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.
|
|
1230
|
-
|
|
1231
|
-
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.
|
|
1232
|
-
|
|
1233
|
-
**A package's own `vite.config.ts` fills that one slot too.** Vitest searches for a config by ascending from the run root and stopping at the first directory holding any of `vitest.config.*` or `vite.config.*`, so a package carrying only a Vite config ends the search there and never reaches the root config. Within a directory `vitest.config.*` is tried first, which makes the fix a Vitest config beside the Vite one, calling `defineVitestConfig()` as any other package config does. Without it the package declares no projects and every tier-selecting command fails with `No projects matched the filter`, `nmr ci` among them. [nmr's readyup kit](#conformance-checks) reports a package in that state.
|
|
1234
|
-
|
|
1235
|
-
**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.
|
|
1236
|
-
|
|
1237
|
-
**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.
|
|
1238
|
-
|
|
1239
|
-
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. Source resolution and git isolation need no such guard: the factory supplies both, so a config file cannot omit them by forgetting a layer.
|
|
1240
|
-
|
|
1241
|
-
### What the config excludes
|
|
1242
|
-
|
|
1243
|
-
Collection skips `**/node_modules/**`, `**/.git/**`, `**/coverage/**`, and `**/dist/**`. Coverage skips `**/__{fixtures,mocks,tests}__/**`, `**/index.ts`, and `**/*.d.ts`.
|
|
1244
|
-
|
|
1245
|
-
`testCollectionExclude` adds to the collection list. It takes directory basenames rather than globs, matched at any depth, so the same array serves the [conventions check](#gating-the-test-file-conventions):
|
|
1246
|
-
|
|
1247
|
-
```ts
|
|
1248
|
-
export default defineVitestConfig({ testCollectionExclude: ['cypress', 'generated'] });
|
|
1249
|
-
```
|
|
1250
|
-
|
|
1251
|
-
Every layer's entries are concatenated, and a name the shared config already prunes is emitted once. A repo needing a glob rather than a directory name has the `project` seam's own `exclude`.
|
|
1252
|
-
|
|
1253
|
-
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.
|
|
1254
|
-
|
|
1255
|
-
Excluding build output from collection is the clearest case. A build that copies `.ts` sources rather than compiling them puts a second copy of the suite under `dist/`, where it is collected and passes green against stale code, and nothing in the run says so. (`nmr build` emits only `.js` and `.d.ts`, neither of which the include matches, so an nmr-built package was never exposed.) `dist/` is deliberately absent from the coverage list, because build output reaching a coverage report shows up as a diagnosable 0% entry.
|
|
1256
|
-
|
|
1257
|
-
#### Where to put fixtures
|
|
1258
|
-
|
|
1259
|
-
A `__fixtures__/` directory is excluded from coverage at any depth, so fixture data stops counting against a package's numbers whether or not a test imports it.
|
|
1260
|
-
|
|
1261
|
-
Placing it outside `__tests__/` settles a second problem: anything named `*.test.{ts,tsx}` under `__tests__/` is collected and run, whatever it holds, so a fixture carrying that name becomes a failing test. `src/__fixtures__/` resolves both halves; `src/__tests__/__fixtures__/` resolves the coverage half alone.
|
|
1262
|
-
|
|
1263
|
-
No collection pattern names fixtures, and that asymmetry is intentional. A coverage exclusion cannot hide a real test, whereas a collection exclusion can; since it could not be removed, a consumer who legitimately keeps a test under `__fixtures__/` would have no recourse.
|
|
1264
|
-
|
|
1265
|
-
### Root-scoped tests
|
|
1266
|
-
|
|
1267
|
-
A monorepo's own root-level tests need a second config, because the package config is found by walking up from a package directory:
|
|
1268
|
-
|
|
1269
|
-
```ts
|
|
1270
|
-
// vitest.root.config.ts
|
|
1271
|
-
import { defineRootVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
1272
|
-
|
|
1273
|
-
export default defineRootVitestConfig({ monorepoRoot: import.meta.dirname });
|
|
1274
|
-
```
|
|
1275
|
-
|
|
1276
|
-
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.
|
|
1277
|
-
|
|
1278
|
-
`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.
|
|
1279
|
-
|
|
1280
|
-
### Migrating to the isolation tiers
|
|
1281
|
-
|
|
1282
|
-
The projects were once named `unit`, `integration`, and `app`, for what a test _covered_ rather than what it _reached_. `integration` and `app` are gone.
|
|
1283
|
-
|
|
1284
|
-
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.
|
|
1285
|
-
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`.
|
|
1286
|
-
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.
|
|
1287
|
-
|
|
1288
|
-
**A file left without a tier is a silent failure.** The residual `unit` claims it and it runs in the default gate -- green, with nothing reporting that the separation was lost. Declare the [conventions check](#gating-the-test-file-conventions), which turns that state into a failing test run, or run [nmr's readyup kit](#conformance-checks), which reports every such file. Do one of the two before treating the migration as done.
|
|
1289
|
-
|
|
1290
|
-
`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.
|
|
1291
|
-
|
|
1292
|
-
### Migrating from the config-file variants
|
|
1293
|
-
|
|
1294
|
-
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.
|
|
1295
|
-
|
|
1296
|
-
1. Replace the repo's root `vitest.config.ts` with `defineVitestConfig()`, and its root-scoped config with `defineRootVitestConfig({ monorepoRoot: import.meta.dirname })`.
|
|
1297
|
-
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.
|
|
1298
|
-
3. Rename the tests onto the tiers, per the section above.
|
|
1299
|
-
4. Drop any hand-copied `test*` entries from `package.json`, which now shadow the defaults.
|
|
1300
|
-
|
|
1301
|
-
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.
|
|
1302
|
-
|
|
1303
|
-
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.
|
|
1304
|
-
|
|
1305
|
-
### Migrating a repo that layered source resolution or git isolation by hand
|
|
1306
|
-
|
|
1307
|
-
The factory supplies both, so the layer a repo wrote for them, and any guard test that checked every config file imported it, can go.
|
|
1308
|
-
|
|
1309
|
-
1. Delete the shared module's `resolve.conditions` entry for `source` and its `ssr.resolve` twin. Keep the module for whatever else it carries; delete it outright once it carries nothing.
|
|
1310
|
-
2. Delete the git-isolation setup file and its `setupFiles` entry. nmr's runs in its place, ahead of every entry a layer adds.
|
|
1311
|
-
3. Delete the test that asserted each config file imports the shared module, where the module is gone.
|
|
1312
|
-
|
|
1313
|
-
Two behaviors change on upgrade even in a repo that layered neither.
|
|
1314
|
-
|
|
1315
|
-
A suite whose tests run `git commit` while relying on the developer's global identity now fails with git's own `Please tell me who you are`. Set the identity locally in the fixture repo the test builds -- `git -C <repo> config user.email`, `user.name`, and `commit.gpgsign false` -- which is what makes the test report the same result on every machine.
|
|
297
|
+
## Documentation
|
|
1316
298
|
|
|
1317
|
-
|
|
299
|
+
- [Scripts and configuration](docs/scripts.md): override tiers, script values, hooks, `defineConfig` fields, `devBin`, and the default commands
|
|
300
|
+
- [What nmr reports](docs/reporting.md): verdicts, `--json`, and verbosity
|
|
301
|
+
- [Check-result cache](docs/check-cache.md): what is skipped, what a hit requires, and replaying, reading, and bypassing recorded runs
|
|
302
|
+
- [Dependency upgrades](docs/upgrades.md): `nmr upgrade`, the upgrade policy, `report-catalog`, `report-overrides`, and `nmr-taze`
|
|
303
|
+
- [Standalone utilities](docs/utilities.md): `nmr-clean`, `nmr-compile`, `nmr-fmt`, and `ensure-prepublish-hooks`
|
|
304
|
+
- [Shared Vitest config](docs/vitest.md): test tiers, test selections, the conventions check, and customization
|
|
305
|
+
- [Shared Prettier config](docs/prettier.md): what it formats and how to customize it
|
|
306
|
+
- [Workspace introspection](docs/workspace.md): the two workspace lookups
|