@williamthorsen/nmr 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -96
- package/README.md +303 -99
- package/bin/nmr-fmt.js +11 -0
- package/dist/esm/cli-ensure-prepublish-hooks.js +1 -1
- package/dist/esm/cli-fmt.js +3 -0
- package/dist/esm/cli-report-overrides.js +1 -1
- package/dist/esm/cli-sync-agent-files.js +1 -1
- package/dist/esm/commands/clean.js +4 -3
- package/dist/esm/commands/ensure-prepublish-hooks.js +1 -1
- package/dist/esm/commands/fmt.d.ts +23 -0
- package/dist/esm/commands/fmt.js +172 -0
- package/dist/esm/context.d.ts +0 -2
- package/dist/esm/context.js +7 -60
- package/dist/esm/default-scripts.d.ts +1 -3
- package/dist/esm/default-scripts.js +17 -21
- package/dist/esm/help.js +3 -2
- package/dist/esm/helpers/workspace-patterns.d.ts +1 -0
- package/dist/esm/helpers/workspace-patterns.js +29 -0
- package/dist/esm/index.d.ts +0 -1
- package/dist/esm/resolve-scripts.d.ts +1 -1
- package/dist/esm/resolve-scripts.js +3 -6
- package/dist/esm/resolver.d.ts +1 -2
- package/dist/esm/resolver.js +3 -6
- package/dist/esm/runCli.js +17 -19
- package/dist/esm/vitest.d.ts +15 -0
- package/dist/esm/vitest.js +62 -0
- package/dist/esm/workspace.d.ts +2 -0
- package/dist/esm/workspace.js +38 -0
- package/package.json +27 -12
- package/bin/nmr-sync-pnpm-version.js +0 -11
- package/dist/esm/cli-sync-pnpm-version.js +0 -11
- package/dist/esm/commands/sync-pnpm-version.d.ts +0 -3
- package/dist/esm/commands/sync-pnpm-version.js +0 -40
- package/dist/esm/helpers/code-quality-pnpm-action.d.ts +0 -13
- package/dist/esm/helpers/code-quality-pnpm-action.js +0 -22
- package/dist/esm/tests/consistency.d.ts +0 -7
- package/dist/esm/tests/consistency.js +0 -57
- package/dist/esm/tests/helpers/get-runtime-version-from-asdf.d.ts +0 -1
- package/dist/esm/tests/helpers/get-runtime-version-from-asdf.js +0 -12
- package/dist/esm/tests/helpers/get-string-from-yaml-file.d.ts +0 -1
- package/dist/esm/tests/helpers/get-string-from-yaml-file.js +0 -12
- package/dist/esm/tests/helpers/get-value-at-path.d.ts +0 -1
- package/dist/esm/tests/helpers/get-value-at-path.js +0 -34
- /package/dist/esm/{cli-sync-pnpm-version.d.ts → cli-fmt.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,27 +1,44 @@
|
|
|
1
1
|
# @williamthorsen/nmr
|
|
2
2
|
|
|
3
|
-
Context-aware script runner for
|
|
3
|
+
Context-aware script runner for pnpm monorepos. Ships an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
|
|
4
4
|
|
|
5
5
|
<!-- section:release-notes -->
|
|
6
|
-
## Release notes — v0.
|
|
6
|
+
## Release notes — v0.21.0 (2026-07-30)
|
|
7
7
|
|
|
8
8
|
### 🎉 Features
|
|
9
9
|
|
|
10
|
-
-
|
|
10
|
+
- Add a shared Vitest config export (#526)
|
|
11
11
|
|
|
12
|
-
Adds `nmr
|
|
12
|
+
Adds a shared Vitest configuration that can be used by repos that use `nmr`. Tests are now sorted into three named categories (unit, integration, and app), so that a run can target or skip a single category. A second variant excludes all tests in workspace packages, allowing the monorepo's own root-level tests to be run separately.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
- 🚨 **Breaking:** Retire the Vitest config-file variants from nmr's defaults (#533)
|
|
15
|
+
|
|
16
|
+
Packages no longer need extra Vitest config files to keep integration tests out of the default test run. Integration tests are now recognized by an `*.int.test.ts` suffix, and every package gets the same five commands: `test`, `test:all`, `test:coverage`, `test:integration`, and `test:watch`. The same five commands work from the monorepo root. A Vitest run that collects no test files now passes instead of failing. Consumers should migrate the Vitest config when upgrading.
|
|
17
|
+
|
|
18
|
+
### 🐛 Bug fixes
|
|
19
|
+
|
|
20
|
+
- Apply ignore rules from any working directory when formatting (#535)
|
|
21
|
+
|
|
22
|
+
Fixes an issue where `nmr fmt` and `nmr fmt:check` did not apply a pattern from a package's `.gitignore` or `.prettierignore` when the run started at the monorepo root, so files a package excluded could fail a root-level format gate while passing a package-scoped one. Untracked files that git does not ignore are now formatted alongside tracked ones, and a run started outside a git repository now fails instead of reporting a clean pass over nothing. Trailing arguments to `nmr fmt` are git pathspecs rather than Prettier flags; one that matches no formattable file now fails instead of exiting silently.
|
|
15
23
|
<!-- /section:release-notes -->
|
|
16
24
|
|
|
17
25
|
## Installation
|
|
18
26
|
|
|
19
|
-
Requires Node.js 24 or later.
|
|
27
|
+
Requires Node.js 24.16 or later.
|
|
20
28
|
|
|
21
29
|
```bash
|
|
22
30
|
pnpm add -D @williamthorsen/nmr
|
|
23
31
|
```
|
|
24
32
|
|
|
33
|
+
### Making `nmr` resolvable
|
|
34
|
+
|
|
35
|
+
nmr installs as a workspace bin, so the bare `nmr` command works only when your shell can find `<root>/node_modules/.bin/nmr`. Choose one:
|
|
36
|
+
|
|
37
|
+
- **[direnv](https://direnv.net/)** (recommended for contributors). Add `PATH_add node_modules/.bin` to the repo's `.envrc`, and bare `nmr` works from any subdirectory.
|
|
38
|
+
- **`pnpm exec nmr <command>`**. Works with no setup: pnpm resolves the bin from the workspace root.
|
|
39
|
+
|
|
40
|
+
> **Note:** Avoid `npx nmr`. Inside a git worktree, `npx` can resolve a different nmr binary from outside the working tree, so the command succeeds while running the wrong code.
|
|
41
|
+
|
|
25
42
|
## Quick start
|
|
26
43
|
|
|
27
44
|
nmr works out of the box with no configuration. It ships with built-in scripts for common monorepo tasks.
|
|
@@ -39,7 +56,7 @@ From the monorepo root:
|
|
|
39
56
|
```bash
|
|
40
57
|
nmr test # Run root tests + recursive workspace tests
|
|
41
58
|
nmr build # Build all packages
|
|
42
|
-
nmr ci # Run check:strict &&
|
|
59
|
+
nmr ci # Run build && check:strict && audit (full CI pipeline)
|
|
43
60
|
```
|
|
44
61
|
|
|
45
62
|
nmr detects where you are and selects the right scripts automatically — see [context-aware resolution](#context-aware-resolution) below.
|
|
@@ -48,19 +65,26 @@ nmr detects where you are and selects the right scripts automatically — see [c
|
|
|
48
65
|
|
|
49
66
|
nmr's key feature is that the same command runs different scripts depending on where you invoke it. It walks up from your current directory to find `pnpm-workspace.yaml`, then checks whether your CWD is inside a workspace package directory.
|
|
50
67
|
|
|
51
|
-
| Where you run `nmr`
|
|
52
|
-
|
|
|
53
|
-
| Monorepo root
|
|
54
|
-
| Inside a workspace package
|
|
55
|
-
| Anywhere, with
|
|
68
|
+
| Where you run `nmr` | Registry used | Working directory | `nmr test` runs |
|
|
69
|
+
| --------------------------------- | ----------------- | ----------------- | --------------------------------------------------------------- |
|
|
70
|
+
| Monorepo root | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test` |
|
|
71
|
+
| Inside a workspace package | Workspace scripts | The package root | `pnpm exec vitest --project '!integration'` (that package only) |
|
|
72
|
+
| Anywhere, with `--workspace-root` | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test` |
|
|
73
|
+
|
|
74
|
+
Relative paths in a script resolve against that working directory, not the invocation directory.
|
|
56
75
|
|
|
57
|
-
Use
|
|
76
|
+
Use `--workspace-root` to escape package context:
|
|
58
77
|
|
|
59
78
|
```bash
|
|
60
79
|
# From inside packages/nmr-core, run the root check suite
|
|
61
|
-
nmr -
|
|
80
|
+
nmr --workspace-root check
|
|
62
81
|
```
|
|
63
82
|
|
|
83
|
+
Two consequences:
|
|
84
|
+
|
|
85
|
+
- `nmr --workspace-root clean` sweeps every workspace package, as `nmr clean` does from the root.
|
|
86
|
+
- Passthrough paths resolve against the working directory: from `packages/nmr/src/`, `nmr --workspace-root fmt pnpm-workspace.yaml` formats the file at the monorepo root.
|
|
87
|
+
|
|
64
88
|
## Three-tier override system
|
|
65
89
|
|
|
66
90
|
Scripts resolve through three tiers. Higher tiers override lower ones:
|
|
@@ -92,6 +116,8 @@ Script values can be `string` or `string[]`. Arrays expand to chained `nmr` sub-
|
|
|
92
116
|
// expands to: nmr lint && nmr fmt
|
|
93
117
|
```
|
|
94
118
|
|
|
119
|
+
Passthrough arguments attach to the final step alone, because the expansion is a single shell chain: `nmr fix --dry-run` runs `nmr lint && nmr fmt --dry-run`.
|
|
120
|
+
|
|
95
121
|
## Configuration
|
|
96
122
|
|
|
97
123
|
Create `.config/nmr.config.ts` in the monorepo root to add or override scripts:
|
|
@@ -137,6 +163,44 @@ For example, if a workspace script resolves to `my-cli --verbose`, nmr rewrites
|
|
|
137
163
|
|
|
138
164
|
> **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.
|
|
139
165
|
|
|
166
|
+
## Pre and post hooks
|
|
167
|
+
|
|
168
|
+
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. Hook failure short-circuits the chain via shell `&&` semantics, propagating the failing exit code.
|
|
169
|
+
|
|
170
|
+
Behaviors worth knowing:
|
|
171
|
+
|
|
172
|
+
- **Silent when absent**: Missing hooks produce no error and no output.
|
|
173
|
+
- **Skip overrides apply to hooks**: A hook value of `""` or `":"` is treated the same as not defining the hook, with no console message.
|
|
174
|
+
- **Skipping the main command skips its hooks**: When `X` is overridden to `""` or `":"`, neither `X:pre` nor `X:post` fires.
|
|
175
|
+
- **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`.
|
|
176
|
+
- **Passthrough args attach only to the main command**: `nmr X --flag value` runs hooks without `--flag value`.
|
|
177
|
+
|
|
178
|
+
Extend `nmr build` with a pre-build step for every workspace package:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// .config/nmr.config.ts
|
|
182
|
+
import { defineConfig } from '@williamthorsen/nmr';
|
|
183
|
+
|
|
184
|
+
export default defineConfig({
|
|
185
|
+
workspaceScripts: {
|
|
186
|
+
'build:pre': 'npx rdy compile',
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Attach a post-build step to one package:
|
|
192
|
+
|
|
193
|
+
```jsonc
|
|
194
|
+
// packages/nmr/package.json
|
|
195
|
+
{
|
|
196
|
+
"scripts": {
|
|
197
|
+
"build:post": "nmr-sync-agent-files",
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The second example calls the bin directly, which sidesteps the workspace-versus-root registry distinction.
|
|
203
|
+
|
|
140
204
|
## Dependency upgrades
|
|
141
205
|
|
|
142
206
|
`nmr upgrade` reports the dependency upgrades available to your repo. What it covers depends on where you run it:
|
|
@@ -148,6 +212,8 @@ For example, if a workspace script resolves to `my-cli --verbose`, nmr rewrites
|
|
|
148
212
|
| `nmr -F <package> upgrade` | that package, from anywhere |
|
|
149
213
|
| `nmr root:upgrade` | the root `package.json` alone |
|
|
150
214
|
|
|
215
|
+
`nmr upgrade` from the root precedes its report with any active `pnpm.overrides` declared in the root `package.json`; `nmr root:upgrade` does not. An override pins a transitive dependency, so an upgrade masked by one never appears in the report.
|
|
216
|
+
|
|
151
217
|
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:
|
|
152
218
|
|
|
153
219
|
```bash
|
|
@@ -156,8 +222,6 @@ nmr upgrade major # major upgrades, still inside the ceilings
|
|
|
156
222
|
nmr upgrade --write # apply the proposals to package.json
|
|
157
223
|
```
|
|
158
224
|
|
|
159
|
-
> **Note:** `-w` means different things on either side of the command. `nmr upgrade -w` passes `--write` to the upgrade tool; `nmr -w upgrade` is nmr's own `--workspace-root` flag and applies no write. Prefer the long forms to keep them apart. Run the root-context commands from the monorepo root: like nmr's other `root:` scripts, they take the current directory as their starting point.
|
|
160
|
-
|
|
161
225
|
### Configuring upgrades
|
|
162
226
|
|
|
163
227
|
Declare version ceilings in a `taze.config.ts` at the monorepo root:
|
|
@@ -185,38 +249,48 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
|
|
|
185
249
|
|
|
186
250
|
### Workspace scripts
|
|
187
251
|
|
|
188
|
-
| Command
|
|
189
|
-
|
|
|
190
|
-
| `build`
|
|
191
|
-
| `check`
|
|
192
|
-
| `check:strict`
|
|
193
|
-
| `clean`
|
|
194
|
-
| `compile`
|
|
195
|
-
| `fix`
|
|
196
|
-
| `fix:check`
|
|
197
|
-
| `fmt`
|
|
198
|
-
| `fmt:check`
|
|
199
|
-
| `lint`
|
|
200
|
-
| `lint:check`
|
|
201
|
-
| `lint:strict`
|
|
202
|
-
| `test`
|
|
203
|
-
| `test:
|
|
204
|
-
| `test:
|
|
205
|
-
| `
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
|
218
|
-
|
|
|
219
|
-
|
|
|
252
|
+
| Command | Runs |
|
|
253
|
+
| ------------------ | -------------------------------------------------------- |
|
|
254
|
+
| `build` | `compile` |
|
|
255
|
+
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
256
|
+
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage` |
|
|
257
|
+
| `clean` | `nmr-clean` |
|
|
258
|
+
| `compile` | `nmr-compile` |
|
|
259
|
+
| `fix` | `lint`, `fmt` |
|
|
260
|
+
| `fix:check` | `fmt:check`, `lint:check` |
|
|
261
|
+
| `fmt` | `nmr-fmt --write` |
|
|
262
|
+
| `fmt:check` | `nmr-fmt --check` |
|
|
263
|
+
| `lint` | `eslint --fix .` |
|
|
264
|
+
| `lint:check` | `eslint .` |
|
|
265
|
+
| `lint:strict` | `strict-lint` |
|
|
266
|
+
| `test` | `pnpm exec vitest --project '!integration'` |
|
|
267
|
+
| `test:all` | `pnpm exec vitest` |
|
|
268
|
+
| `test:coverage` | `pnpm exec vitest --project '!integration' --coverage` |
|
|
269
|
+
| `test:integration` | `pnpm exec vitest --project integration` |
|
|
270
|
+
| `test:watch` | `pnpm exec vitest --project '!integration' --watch` |
|
|
271
|
+
| `typecheck` | `tsgo --noEmit` |
|
|
272
|
+
| `upgrade` | `nmr-taze --include-locked` |
|
|
273
|
+
| `view-coverage` | `open coverage/index.html` |
|
|
274
|
+
|
|
275
|
+
`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).
|
|
276
|
+
|
|
277
|
+
#### Test selections
|
|
278
|
+
|
|
279
|
+
Every package resolves the same five test commands. Nothing is detected on disk: the commands select [Vitest projects](#shared-vitest-config), so a package separates its integration tests by naming them `*.int.test.ts`, not by carrying extra config files.
|
|
280
|
+
|
|
281
|
+
| To run | Command |
|
|
282
|
+
| ----------------------------------- | ------------------ |
|
|
283
|
+
| Everything except integration tests | `test` |
|
|
284
|
+
| Integration tests alone | `test:integration` |
|
|
285
|
+
| Every project | `test:all` |
|
|
286
|
+
|
|
287
|
+
`test` negates `integration` rather than naming the code-only projects, so a category added later joins the default run instead of being dropped from it.
|
|
288
|
+
|
|
289
|
+
`test:coverage` and `test:watch` are the same selection as `test` in a different mode. There are no `test:integration --coverage` equivalents because none are needed: everything after a command name is forwarded untouched, so `nmr test:integration --coverage` already works.
|
|
290
|
+
|
|
291
|
+
To narrow to a single project, go through `test:all`: `nmr test:all --project app`. Narrowing through `test` does not work, because a second `--project` widens the filter rather than restricting it.
|
|
292
|
+
|
|
293
|
+
A run that collects no test files passes. That is what lets `nmr test:integration` fan out across a monorepo in which most packages have no integration tests, and it means a package with no tests at all needs no `"test": ""` override.
|
|
220
294
|
|
|
221
295
|
### Root scripts
|
|
222
296
|
|
|
@@ -225,7 +299,7 @@ A package gets this variant automatically when it contains a `vitest.integration
|
|
|
225
299
|
| Command | Runs |
|
|
226
300
|
| ------- | --------------------------------- |
|
|
227
301
|
| `build` | `pnpm --recursive exec nmr build` |
|
|
228
|
-
| `ci` | `build`, `check:strict`
|
|
302
|
+
| `ci` | `build`, `check:strict`, `audit` |
|
|
229
303
|
| `clean` | `nmr-clean` |
|
|
230
304
|
|
|
231
305
|
#### Check and quality
|
|
@@ -244,11 +318,19 @@ A package gets this variant automatically when it contains a `vitest.integration
|
|
|
244
318
|
|
|
245
319
|
#### Test
|
|
246
320
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
|
250
|
-
|
|
|
251
|
-
| `test
|
|
321
|
+
The same five 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.
|
|
322
|
+
|
|
323
|
+
| Command | Runs |
|
|
324
|
+
| ------------------ | ------------------------------------------------------------------------- |
|
|
325
|
+
| `test` | `nmr root:test && pnpm --recursive exec nmr test` |
|
|
326
|
+
| `test:all` | `nmr root:test:all && pnpm --recursive exec nmr test:all` |
|
|
327
|
+
| `test:coverage` | `nmr root:test && pnpm --recursive exec nmr test:coverage` |
|
|
328
|
+
| `test:integration` | `nmr root:test:integration && pnpm --recursive exec nmr test:integration` |
|
|
329
|
+
| `test:watch` | `vitest --project '!integration' --watch` |
|
|
330
|
+
|
|
331
|
+
`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.
|
|
332
|
+
|
|
333
|
+
`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`.
|
|
252
334
|
|
|
253
335
|
#### Typecheck
|
|
254
336
|
|
|
@@ -266,12 +348,18 @@ A package gets this variant automatically when it contains a `vitest.integration
|
|
|
266
348
|
|
|
267
349
|
#### Format
|
|
268
350
|
|
|
269
|
-
| Command | Runs
|
|
270
|
-
| ----------- |
|
|
271
|
-
| `fmt` | `
|
|
272
|
-
| `fmt:all` | `fmt`, `fmt:sh`
|
|
273
|
-
| `fmt:check` | `
|
|
274
|
-
| `fmt:sh` | `shfmt --write **/*.sh`
|
|
351
|
+
| Command | Runs |
|
|
352
|
+
| ----------- | ----------------------- |
|
|
353
|
+
| `fmt` | `nmr-fmt --write` |
|
|
354
|
+
| `fmt:all` | `fmt`, `fmt:sh` |
|
|
355
|
+
| `fmt:check` | `nmr-fmt --check` |
|
|
356
|
+
| `fmt:sh` | `shfmt --write **/*.sh` |
|
|
357
|
+
|
|
358
|
+
#### File selection
|
|
359
|
+
|
|
360
|
+
`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.
|
|
361
|
+
|
|
362
|
+
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.
|
|
275
363
|
|
|
276
364
|
#### Audit
|
|
277
365
|
|
|
@@ -283,9 +371,9 @@ A package gets this variant automatically when it contains a `vitest.integration
|
|
|
283
371
|
|
|
284
372
|
#### Dependencies
|
|
285
373
|
|
|
286
|
-
| Command | Runs
|
|
287
|
-
| --------- |
|
|
288
|
-
| `upgrade` | `nmr-taze --include-locked --recursive` |
|
|
374
|
+
| Command | Runs |
|
|
375
|
+
| --------- | --------------------------------------------------------------- |
|
|
376
|
+
| `upgrade` | `nmr-report-overrides && nmr-taze --include-locked --recursive` |
|
|
289
377
|
|
|
290
378
|
See [dependency upgrades](#dependency-upgrades) for the workflow and its configuration.
|
|
291
379
|
|
|
@@ -293,23 +381,24 @@ See [dependency upgrades](#dependency-upgrades) for the workflow and its configu
|
|
|
293
381
|
|
|
294
382
|
These scripts operate on root-level code only (not workspace packages):
|
|
295
383
|
|
|
296
|
-
| Command
|
|
297
|
-
|
|
|
298
|
-
| `root:check`
|
|
299
|
-
| `root:lint`
|
|
300
|
-
| `root:lint:check`
|
|
301
|
-
| `root:lint:strict`
|
|
302
|
-
| `root:test`
|
|
303
|
-
| `root:
|
|
304
|
-
| `root:
|
|
384
|
+
| Command | Runs |
|
|
385
|
+
| ----------------------- | ------------------------------------------------------------------ |
|
|
386
|
+
| `root:check` | `root:typecheck`, `fmt:check`, `root:lint:check`, `root:test` |
|
|
387
|
+
| `root:lint` | `eslint --fix --ignore-pattern 'packages/**' .` |
|
|
388
|
+
| `root:lint:check` | `eslint --ignore-pattern 'packages/**' .` |
|
|
389
|
+
| `root:lint:strict` | `strict-lint --ignore-pattern 'packages/**' .` |
|
|
390
|
+
| `root:test` | `vitest --config ./vitest.root.config.ts --project '!integration'` |
|
|
391
|
+
| `root:test:all` | `vitest --config ./vitest.root.config.ts` |
|
|
392
|
+
| `root:test:integration` | `vitest --config ./vitest.root.config.ts --project integration` |
|
|
393
|
+
| `root:typecheck` | `tsgo --noEmit` |
|
|
394
|
+
| `root:upgrade` | `nmr-taze --include-locked` |
|
|
305
395
|
|
|
306
396
|
#### Utilities
|
|
307
397
|
|
|
308
|
-
| Command
|
|
309
|
-
|
|
|
310
|
-
| `report-overrides`
|
|
311
|
-
| `sync-agent-files`
|
|
312
|
-
| `sync-pnpm-version` | `nmr-sync-pnpm-version` |
|
|
398
|
+
| Command | Runs |
|
|
399
|
+
| ------------------ | ---------------------- |
|
|
400
|
+
| `report-overrides` | `nmr-report-overrides` |
|
|
401
|
+
| `sync-agent-files` | `nmr-sync-agent-files` |
|
|
313
402
|
|
|
314
403
|
## CLI reference
|
|
315
404
|
|
|
@@ -319,11 +408,13 @@ These scripts operate on root-level code only (not workspace packages):
|
|
|
319
408
|
nmr [flags] <command> [args...]
|
|
320
409
|
```
|
|
321
410
|
|
|
411
|
+
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.
|
|
412
|
+
|
|
322
413
|
| Flag | Description | Default |
|
|
323
414
|
| ------------------------ | --------------------------------------------------- | ------- |
|
|
324
415
|
| `-F, --filter <pattern>` | Run command in matching packages | — |
|
|
325
416
|
| `-R, --recursive` | Run command in all packages | — |
|
|
326
|
-
| `-w, --workspace-root` |
|
|
417
|
+
| `-w, --workspace-root` | Use root scripts, running at the monorepo root | — |
|
|
327
418
|
| `-q, --quiet` | Suppress info messages; show full output on failure | — |
|
|
328
419
|
| `-?, --help` | Show available commands | — |
|
|
329
420
|
| `-V, --version` | Show version number | — |
|
|
@@ -337,14 +428,14 @@ nmr build # Compile to .js and .d.ts in one pass
|
|
|
337
428
|
|
|
338
429
|
# From the monorepo root
|
|
339
430
|
nmr test # Root tests + recursive workspace tests
|
|
340
|
-
nmr ci # check:strict +
|
|
431
|
+
nmr ci # build + check:strict + audit
|
|
341
432
|
|
|
342
433
|
# Target specific packages
|
|
343
|
-
nmr
|
|
344
|
-
nmr
|
|
434
|
+
nmr --filter core test # Test only the core package
|
|
435
|
+
nmr --recursive lint # Lint all workspace packages
|
|
345
436
|
|
|
346
437
|
# Force root context from anywhere
|
|
347
|
-
nmr -
|
|
438
|
+
nmr --workspace-root check # Run root check from a package dir
|
|
348
439
|
```
|
|
349
440
|
|
|
350
441
|
## Additional subcommands
|
|
@@ -353,7 +444,7 @@ These commands are available as `nmr` subcommands and as standalone `nmr-`-prefi
|
|
|
353
444
|
|
|
354
445
|
### `report-overrides`
|
|
355
446
|
|
|
356
|
-
Report any active `pnpm.overrides` in the root `package.json
|
|
447
|
+
Report any active `pnpm.overrides` in the root `package.json`, 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.
|
|
357
448
|
|
|
358
449
|
```bash
|
|
359
450
|
nmr report-overrides
|
|
@@ -378,13 +469,16 @@ To expose the synced guidance to Claude Code sessions, add this include to the c
|
|
|
378
469
|
@nmr/AGENTS.md
|
|
379
470
|
```
|
|
380
471
|
|
|
381
|
-
|
|
472
|
+
#### What belongs in the synced file
|
|
382
473
|
|
|
383
|
-
|
|
474
|
+
The synced file is injected at launch in every consuming repo, so every line is paid for on every task whether or not it is relevant. It is a cheatsheet, not a manual.
|
|
384
475
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
476
|
+
A line earns a place in `AGENTS.md` only if both hold:
|
|
477
|
+
|
|
478
|
+
1. It is absent from nmr's own output. Bare `nmr` already prints the flags, every command, and the shell command each resolves to; failing checks already name their own fix. Repeating any of that costs launch tokens to say something the agent will be told anyway, at the moment it matters.
|
|
479
|
+
2. The obvious action goes wrong without it. Guidance that prevents a silent mistake earns its place; guidance that prevents a mistake the next error message would diagnose does not.
|
|
480
|
+
|
|
481
|
+
Everything else belongs in this README, reachable on demand at `node_modules/@williamthorsen/nmr/README.md`, with a pointer from `AGENTS.md` naming the topic so an agent knows to look. When adding a feature, document it here and extend that pointer rather than the cheatsheet.
|
|
388
482
|
|
|
389
483
|
## Standalone utilities
|
|
390
484
|
|
|
@@ -406,9 +500,26 @@ Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler
|
|
|
406
500
|
nmr-compile
|
|
407
501
|
```
|
|
408
502
|
|
|
503
|
+
### `nmr-fmt`
|
|
504
|
+
|
|
505
|
+
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.
|
|
506
|
+
|
|
507
|
+
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.
|
|
508
|
+
|
|
509
|
+
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.
|
|
510
|
+
|
|
511
|
+
`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.
|
|
512
|
+
|
|
513
|
+
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.
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
nmr-fmt --check
|
|
517
|
+
nmr-fmt --write packages/nmr
|
|
518
|
+
```
|
|
519
|
+
|
|
409
520
|
### `nmr-taze`
|
|
410
521
|
|
|
411
|
-
Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched. This is what
|
|
522
|
+
Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched. This is what `root:upgrade` resolves to, and what the root `upgrade` script ends with — see [dependency upgrades](#dependency-upgrades) for the workflow.
|
|
412
523
|
|
|
413
524
|
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.
|
|
414
525
|
|
|
@@ -436,25 +547,118 @@ After installing, a consuming repo's root `package.json` scripts shrink to lifec
|
|
|
436
547
|
|
|
437
548
|
```json
|
|
438
549
|
{
|
|
439
|
-
"prepare": "lefthook install"
|
|
440
|
-
"postinstall": "nmr report-overrides"
|
|
550
|
+
"prepare": "lefthook install"
|
|
441
551
|
}
|
|
442
552
|
```
|
|
443
553
|
|
|
444
554
|
Per-package `package.json` files no longer need script entries. Run `nmr <command>` directly.
|
|
445
555
|
|
|
446
|
-
##
|
|
556
|
+
## Workspace introspection
|
|
447
557
|
|
|
448
|
-
|
|
558
|
+
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:
|
|
449
559
|
|
|
450
560
|
```ts
|
|
451
|
-
// __tests__/
|
|
452
|
-
import {
|
|
561
|
+
// __tests__/packages.test.ts
|
|
562
|
+
import { findMonorepoRoot, getWorkspacePackageDirs } from '@williamthorsen/nmr/workspace';
|
|
563
|
+
|
|
564
|
+
const monorepoRoot = findMonorepoRoot();
|
|
565
|
+
|
|
566
|
+
for (const packageDir of getWorkspacePackageDirs(monorepoRoot)) {
|
|
567
|
+
// assert something about every workspace package
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
`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.
|
|
572
|
+
|
|
573
|
+
`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. 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.
|
|
574
|
+
|
|
575
|
+
One divergence from pnpm: a directory counts as a package only if it holds a `package.json`, not a `package.yaml` or `package.json5`.
|
|
576
|
+
|
|
577
|
+
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.
|
|
453
578
|
|
|
454
|
-
|
|
579
|
+
## Shared Vitest config
|
|
580
|
+
|
|
581
|
+
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:
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
// vitest.config.ts
|
|
585
|
+
import { defineVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
586
|
+
|
|
587
|
+
export default defineVitestConfig();
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
`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.
|
|
591
|
+
|
|
592
|
+
### Test categories
|
|
593
|
+
|
|
594
|
+
The config declares three projects, named for what the tests cover:
|
|
595
|
+
|
|
596
|
+
| Project | Matches | Covers |
|
|
597
|
+
| ------------- | -------------------------------------- | ---------------------------------------- |
|
|
598
|
+
| `unit` | every test file the others don't claim | source code |
|
|
599
|
+
| `integration` | `*.int.test.{ts,tsx}` | behavior beyond source code |
|
|
600
|
+
| `app` | `*.app.test.{ts,tsx}` | the app's own tooling, e.g. drift checks |
|
|
601
|
+
|
|
602
|
+
All three match only under a `__tests__` directory. Select them at run time with `--project`, which accepts negation:
|
|
603
|
+
|
|
604
|
+
```bash
|
|
605
|
+
vitest --project '!integration' # everything except integration tests
|
|
606
|
+
vitest --project integration # integration tests alone
|
|
607
|
+
vitest # every project
|
|
455
608
|
```
|
|
456
609
|
|
|
457
|
-
|
|
610
|
+
`unit` is defined by subtracting the other categories rather than by an allow-list of suffixes, so a file such as `parser.smoke.test.ts` runs under `unit` instead of being silently dropped.
|
|
611
|
+
|
|
612
|
+
### Customizing by scope
|
|
613
|
+
|
|
614
|
+
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 two separate override surfaces instead of one merged config:
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
export default defineVitestConfig({
|
|
618
|
+
// Vite-level options, plus the test options Vitest honours only at the root.
|
|
619
|
+
root: { resolve: { conditions: ['development'] } },
|
|
620
|
+
// Applied to every project.
|
|
621
|
+
project: { setupFiles: ['./vitest.setup.ts'] },
|
|
622
|
+
});
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
`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. Both surfaces merge into the generated config rather than replacing it, so overriding one coverage field leaves the rest intact.
|
|
626
|
+
|
|
627
|
+
Arrays concatenate rather than replace. `exclude` and `setupFiles` therefore add to what the config already declares, and neither seam can narrow `include` or drop a default exclusion. Adding an `include` pattern through `project` widens all three projects at once, so a file matching it is collected by each and runs three times.
|
|
628
|
+
|
|
629
|
+
### Root-scoped tests
|
|
630
|
+
|
|
631
|
+
A monorepo's own root-level tests need a second config, because the package config is found by walking up from a package directory:
|
|
632
|
+
|
|
633
|
+
```ts
|
|
634
|
+
// vitest.root.config.ts
|
|
635
|
+
import { defineRootVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
636
|
+
|
|
637
|
+
export default defineRootVitestConfig();
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
This variant reads `pnpm-workspace.yaml` and excludes every workspace package from all three projects, so a root run covers only root-level files. It reports no coverage of its own — packages cover their own sources.
|
|
641
|
+
|
|
642
|
+
The monorepo root is located by walking up from the working directory, and every project is pinned to it, so the exclusions hold wherever the run is invoked from. Pass `startDir` to locate it from somewhere else:
|
|
643
|
+
|
|
644
|
+
```ts
|
|
645
|
+
export default defineRootVitestConfig({ startDir: import.meta.dirname });
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Migrating from the config-file variants
|
|
649
|
+
|
|
650
|
+
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. **Migrate the config and the scripts together**: one without the other leaves the repo in one of the two states below.
|
|
651
|
+
|
|
652
|
+
1. Replace the repo's root `vitest.config.ts` with `defineVitestConfig()`, and its root-scoped config with `defineRootVitestConfig()`.
|
|
653
|
+
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.
|
|
654
|
+
3. Rename integration tests to `*.int.test.ts` and tooling or drift tests to `*.app.test.ts`.
|
|
655
|
+
4. Drop any hand-copied `test*` entries from `package.json`, which now shadow the defaults.
|
|
656
|
+
|
|
657
|
+
Until step 1 is done, an upgraded nmr behaves as follows against a config that declares no projects:
|
|
658
|
+
|
|
659
|
+
| Command | Result |
|
|
660
|
+
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
661
|
+
| `nmr test` / `test:coverage` | **Succeeds, and runs integration tests too.** The separation is lost with no error, because `--project '!integration'` matches nothing to exclude. |
|
|
662
|
+
| `nmr test:integration` | **Fails** with `No projects matched the filter "integration"`. |
|
|
458
663
|
|
|
459
|
-
|
|
460
|
-
- Node.js version matches between `.tool-versions` and GitHub workflow
|
|
664
|
+
The second is the reliable signal that migration is incomplete. The first is silent, so do not treat a green `nmr test` as evidence that the move succeeded.
|
package/bin/nmr-fmt.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
try {
|
|
3
|
+
await import('../dist/esm/cli-fmt.js');
|
|
4
|
+
} catch (error) {
|
|
5
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
+
process.stderr.write('nmr-fmt: build output not found — run `pnpm run build` first\n');
|
|
7
|
+
} else {
|
|
8
|
+
process.stderr.write(`nmr-fmt: failed to load: ${error.message}\n`);
|
|
9
|
+
}
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
|
|
2
2
|
import { DEFAULT_HOOK, ensurePrepublishHooks } from "./commands/ensure-prepublish-hooks.js";
|
|
3
|
-
import { findMonorepoRoot } from "./
|
|
3
|
+
import { findMonorepoRoot } from "./workspace.js";
|
|
4
4
|
const flagSchema = {
|
|
5
5
|
fix: { long: '--fix', type: 'boolean' },
|
|
6
6
|
dryRun: { long: '--dry-run', type: 'boolean' },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
2
|
import { reportOverrides } from "./commands/report-overrides.js";
|
|
3
|
-
import { findMonorepoRoot } from "./
|
|
3
|
+
import { findMonorepoRoot } from "./workspace.js";
|
|
4
4
|
try {
|
|
5
5
|
const monorepoRoot = findMonorepoRoot();
|
|
6
6
|
reportOverrides(monorepoRoot);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
|
|
2
2
|
import { check, sync } from "./commands/sync-agent-files.js";
|
|
3
|
-
import { findMonorepoRoot } from "./
|
|
3
|
+
import { findMonorepoRoot } from "./workspace.js";
|
|
4
4
|
const flagSchema = {
|
|
5
5
|
check: { long: '--check', type: 'boolean' },
|
|
6
6
|
};
|