@williamthorsen/nmr 0.18.2 → 0.20.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 -84
- package/README.md +187 -46
- package/bin/nmr-taze.js +11 -0
- package/dist/esm/cli-ensure-prepublish-hooks.js +1 -1
- package/dist/esm/cli-report-overrides.js +1 -1
- package/dist/esm/cli-sync-agent-files.js +1 -1
- package/dist/esm/cli-taze.js +10 -0
- package/dist/esm/commands/build.d.ts +1 -1
- package/dist/esm/commands/build.js +8 -6
- package/dist/esm/commands/clean.js +2 -1
- package/dist/esm/commands/ensure-prepublish-hooks.js +1 -1
- package/dist/esm/commands/taze.d.ts +12 -0
- package/dist/esm/commands/taze.js +32 -0
- package/dist/esm/context.d.ts +0 -2
- package/dist/esm/context.js +8 -61
- package/dist/esm/default-scripts.js +3 -5
- package/dist/esm/help.js +9 -6
- 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/runCli.js +16 -16
- package/dist/esm/taze.d.ts +5 -0
- package/dist/esm/taze.js +6 -0
- package/dist/esm/workspace.d.ts +2 -0
- package/dist/esm/workspace.js +38 -0
- package/package.json +16 -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-taze.d.ts} +0 -0
package/AGENTS.md
CHANGED
|
@@ -2,88 +2,9 @@
|
|
|
2
2
|
source: '@williamthorsen/nmr@0.0.0-source'
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
# nmr: agent guidance
|
|
5
|
+
# nmr: agent guidance (generated; do not edit)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Run `nmr` with no command (from the monorepo root or any workspace package) to list every available script, including composite expansions and resolved shell commands. Check this before guessing a script name from another repo: The registry is authoritative.
|
|
12
|
-
|
|
13
|
-
## Invocation rules
|
|
14
|
-
|
|
15
|
-
- Use `nmr <command>` for anything nmr provides. Do not use `pnpm run <command>`.
|
|
16
|
-
- You can invoke nmr from the monorepo root or any workspace package.
|
|
17
|
-
|
|
18
|
-
### How to make `nmr` resolvable
|
|
19
|
-
|
|
20
|
-
`nmr` ships as a workspace bin. The bare `nmr` command works only when your shell can find `<root>/node_modules/.bin/nmr`. Choose one:
|
|
21
|
-
|
|
22
|
-
- **direnv** (recommended for contributors). With [direnv](https://direnv.net/) installed, the repo's `.envrc` adds `node_modules/.bin` to your `PATH` automatically. From any subdirectory, bare `nmr` works.
|
|
23
|
-
- **`pnpm exec nmr <command>`**: Works without setup. pnpm resolves the bin from the workspace root.
|
|
24
|
-
|
|
25
|
-
Avoid `npx nmr`. Inside git worktrees, `npx` can resolve a different nmr binary from outside the working tree.
|
|
26
|
-
|
|
27
|
-
## Root vs. workspace context
|
|
28
|
-
|
|
29
|
-
nmr walks up to find `pnpm-workspace.yaml`, then decides which registry to use based on whether your cwd is inside a workspace package. The same command name (e.g. `build`, `test`, `check:strict`) often exists in both registries with different behavior; the root version typically delegates across all workspaces. Use `-w` to force the root registry from inside a package dir, and `-F <pkg>` to run a single package's script from anywhere.
|
|
30
|
-
|
|
31
|
-
## Composite scripts
|
|
32
|
-
|
|
33
|
-
A script value shown in `nmr` output as `[a, b, c]` is a composite: it runs `nmr a && nmr b && nmr c`, stopping at the first failure. Composites can reference other composites.
|
|
34
|
-
|
|
35
|
-
## `root:*` siblings
|
|
36
|
-
|
|
37
|
-
Some root scripts (e.g. `lint`, `typecheck`, `test`) expand to `nmr root:X && pnpm --recursive exec nmr X`. The `root:X` variant runs only against root-level files; the plain name runs everywhere. Use `root:X` directly when you want to isolate a failure to the root code.
|
|
38
|
-
|
|
39
|
-
## Managed build
|
|
40
|
-
|
|
41
|
-
The default `compile` script runs `nmr-compile`, a standalone bin that uses the TypeScript compiler API to emit a package's `src` to `dist/esm` as `.js` and `.d.ts` in one pass, rewriting relative `.ts`→`.js` specifiers and tsconfig `paths` aliases in both outputs, and skipping work when inputs are unchanged. `typescript` is a peer dependency (`>=5.7.0`). There is no repo-local build script to maintain, and no separate typings step; `nmr build` runs `compile` alone. To find or debug the build, look to `nmr-compile`, not a `config/build.ts` in the consuming repo.
|
|
42
|
-
|
|
43
|
-
## Override behaviors
|
|
44
|
-
|
|
45
|
-
In `.config/nmr.config.ts` or a package's `package.json`, override values have special semantics:
|
|
46
|
-
|
|
47
|
-
- `""` (empty string): Skip the script with a "Skipping" message; exit 0.
|
|
48
|
-
- `":"`: No-op; exit 0. Prefer this over `""` if your repo enforces non-empty script values.
|
|
49
|
-
- Any other string: Runs in place of the default.
|
|
50
|
-
|
|
51
|
-
## Pre and post hooks
|
|
52
|
-
|
|
53
|
-
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 3-tier registry (built-in defaults → `.config/nmr.config.ts` → 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.
|
|
54
|
-
|
|
55
|
-
Behaviors worth knowing:
|
|
56
|
-
|
|
57
|
-
- **Silent when absent**: Missing hooks produce no error and no output.
|
|
58
|
-
- **Skip overrides apply to hooks**: A hook value of `""` or `":"` is treated the same as not defining the hook. No console message.
|
|
59
|
-
- **Skipping the main command skips its hooks**: When `X` is overridden to `""` or `":"`, neither `X:pre` nor `X:post` fires.
|
|
60
|
-
- **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`.
|
|
61
|
-
- **Passthrough args attach only to the main command**: `nmr X --flag value` runs hooks without `--flag value`.
|
|
62
|
-
|
|
63
|
-
Worked examples:
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
// .config/nmr.config.ts — extend `nmr build` with a pre-build compile step
|
|
67
|
-
import { defineConfig } from '@williamthorsen/nmr';
|
|
68
|
-
|
|
69
|
-
export default defineConfig({
|
|
70
|
-
workspaceScripts: {
|
|
71
|
-
'build:pre': 'npx rdy compile',
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
```jsonc
|
|
77
|
-
// packages/nmr/package.json — re-stamp .agents/nmr/AGENTS.md after every build
|
|
78
|
-
{
|
|
79
|
-
"scripts": {
|
|
80
|
-
"build:post": "nmr-sync-agent-files",
|
|
81
|
-
},
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
The second example calls the bin directly to sidestep the workspace-vs-root registry distinction.
|
|
86
|
-
|
|
87
|
-
## Agent-file sync
|
|
88
|
-
|
|
89
|
-
The presence and version stamp of `.agents/nmr/AGENTS.md` is verified by `check:agent-files`, which is part of the default root `check:strict` composite. If it fails, run `nmr sync-agent-files`.
|
|
7
|
+
- Use `nmr <command>`, not `pnpm run <command>`, for anything nmr provides. Run bare `nmr` to list every command and the shell command it resolves to; a wrong guess reports only `Unknown command`, with no list.
|
|
8
|
+
- Scope follows cwd, and bare `nmr` lists both registries: from the repo root a command covers root files and every workspace; from inside a package, that package alone. `nmr -F <pkg> <command>` targets one package from anywhere; `nmr root:<command>` targets root files alone, which isolates a failure to root code.
|
|
9
|
+
- Never `npx nmr`: inside a git worktree it can resolve a different nmr from outside the tree. Fall back to `pnpm exec nmr`.
|
|
10
|
+
- The rest is in `node_modules/@williamthorsen/nmr/README.md`: pre/post hooks (every `nmr X` auto-wraps as `nmr X:pre && nmr X && nmr X:post`), script overrides and their `""`/`":"` skip values, `nmr upgrade` ceilings (report-only until `nmr upgrade --write`), and the `nmr-compile` build (there is no repo-local build script).
|
package/README.md
CHANGED
|
@@ -1,23 +1,54 @@
|
|
|
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.20.0 (2026-07-28)
|
|
7
|
+
|
|
8
|
+
### 🎉 Features
|
|
9
|
+
|
|
10
|
+
- 🚨 **Breaking:** Migrate to code-quality workflow v7 and retire the version-restatement apparatus (#503)
|
|
11
|
+
|
|
12
|
+
Migrates the code-quality workflow from `@v6` to `@v7`, which no longer requires the Node version to be specified in two places, and removes the test that previously checked for drift. Also removed are the `nmr sync-pnpm-version` command and drift-related exported functions.
|
|
13
|
+
|
|
14
|
+
Taking this upgrade requires migrating the repo's code-quality workflow to `@v7` and deleting its version-alignment test in the same change.
|
|
15
|
+
|
|
16
|
+
- 🚨 **Breaking:** Resolve workspace packages with pnpm's pattern semantics (#509)
|
|
17
|
+
|
|
18
|
+
nmr now excludes packages matched by a `!`-prefixed pattern in `pnpm-workspace.yaml` from the packages it operates on. Deep globs such as `packages/**` now match nested packages, and running a command from inside a nested package targets that package rather than an enclosing ancestor. Exclusion patterns must be quoted to take effect. nmr now requires Node.js 24.16 or later.
|
|
19
|
+
|
|
20
|
+
- Report pnpm overrides when reviewing dependencies (#514)
|
|
21
|
+
|
|
22
|
+
From the monorepo root, `nmr upgrade` now reports the repo's active pnpm dependency overrides alongside the upgrade report, making clear when a pin is the reason an expected upgrade is absent. Repos that wired up this override report themselves with an install-time hook can now remove that hook.
|
|
23
|
+
|
|
24
|
+
- Trim the bundled agent guidance to a cheatsheet (#516)
|
|
25
|
+
|
|
26
|
+
Trims the agent guidance nmr installs into consuming repos from 100 lines to 10. Only the bare essentials are now injected into context. Agents are instructed to consult the package's README for details.
|
|
7
27
|
|
|
8
28
|
### 🐛 Bug fixes
|
|
9
29
|
|
|
10
|
-
-
|
|
30
|
+
- Anchor script execution to the resolved registry's directory (#517)
|
|
11
31
|
|
|
12
|
-
Fixes an issue where
|
|
32
|
+
Fixes an issue where `nmr --workspace-root`, run from inside a workspace package, either failed outright or silently acted on the wrong directory. Commands now behave the same regardless of the directory they are invoked from: root commands act on the monorepo root, and package commands on their own package. Two changes for existing users: `nmr --workspace-root clean` now sweeps every workspace package instead of cleaning only the current one, and relative paths passed to a script now resolve against the directory the script runs in.
|
|
13
33
|
<!-- /section:release-notes -->
|
|
14
34
|
|
|
15
35
|
## Installation
|
|
16
36
|
|
|
37
|
+
Requires Node.js 24.16 or later.
|
|
38
|
+
|
|
17
39
|
```bash
|
|
18
40
|
pnpm add -D @williamthorsen/nmr
|
|
19
41
|
```
|
|
20
42
|
|
|
43
|
+
### Making `nmr` resolvable
|
|
44
|
+
|
|
45
|
+
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:
|
|
46
|
+
|
|
47
|
+
- **[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.
|
|
48
|
+
- **`pnpm exec nmr <command>`**. Works with no setup: pnpm resolves the bin from the workspace root.
|
|
49
|
+
|
|
50
|
+
> **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.
|
|
51
|
+
|
|
21
52
|
## Quick start
|
|
22
53
|
|
|
23
54
|
nmr works out of the box with no configuration. It ships with built-in scripts for common monorepo tasks.
|
|
@@ -35,7 +66,7 @@ From the monorepo root:
|
|
|
35
66
|
```bash
|
|
36
67
|
nmr test # Run root tests + recursive workspace tests
|
|
37
68
|
nmr build # Build all packages
|
|
38
|
-
nmr ci # Run check:strict &&
|
|
69
|
+
nmr ci # Run build && check:strict && audit (full CI pipeline)
|
|
39
70
|
```
|
|
40
71
|
|
|
41
72
|
nmr detects where you are and selects the right scripts automatically — see [context-aware resolution](#context-aware-resolution) below.
|
|
@@ -44,19 +75,26 @@ nmr detects where you are and selects the right scripts automatically — see [c
|
|
|
44
75
|
|
|
45
76
|
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.
|
|
46
77
|
|
|
47
|
-
| Where you run `nmr`
|
|
48
|
-
|
|
|
49
|
-
| Monorepo root
|
|
50
|
-
| Inside a workspace package
|
|
51
|
-
| Anywhere, with
|
|
78
|
+
| Where you run `nmr` | Registry used | Working directory | `nmr test` runs |
|
|
79
|
+
| --------------------------------- | ----------------- | ----------------- | --------------------------------------------- |
|
|
80
|
+
| Monorepo root | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test` |
|
|
81
|
+
| Inside a workspace package | Workspace scripts | The package root | `pnpm exec vitest` (for that package only) |
|
|
82
|
+
| Anywhere, with `--workspace-root` | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test` |
|
|
52
83
|
|
|
53
|
-
|
|
84
|
+
Relative paths in a script resolve against that working directory, not the invocation directory.
|
|
85
|
+
|
|
86
|
+
Use `--workspace-root` to escape package context:
|
|
54
87
|
|
|
55
88
|
```bash
|
|
56
89
|
# From inside packages/nmr-core, run the root check suite
|
|
57
|
-
nmr -
|
|
90
|
+
nmr --workspace-root check
|
|
58
91
|
```
|
|
59
92
|
|
|
93
|
+
Two consequences:
|
|
94
|
+
|
|
95
|
+
- `nmr --workspace-root clean` sweeps every workspace package, as `nmr clean` does from the root.
|
|
96
|
+
- 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.
|
|
97
|
+
|
|
60
98
|
## Three-tier override system
|
|
61
99
|
|
|
62
100
|
Scripts resolve through three tiers. Higher tiers override lower ones:
|
|
@@ -88,6 +126,8 @@ Script values can be `string` or `string[]`. Arrays expand to chained `nmr` sub-
|
|
|
88
126
|
// expands to: nmr lint && nmr fmt
|
|
89
127
|
```
|
|
90
128
|
|
|
129
|
+
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`.
|
|
130
|
+
|
|
91
131
|
## Configuration
|
|
92
132
|
|
|
93
133
|
Create `.config/nmr.config.ts` in the monorepo root to add or override scripts:
|
|
@@ -133,6 +173,86 @@ For example, if a workspace script resolves to `my-cli --verbose`, nmr rewrites
|
|
|
133
173
|
|
|
134
174
|
> **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.
|
|
135
175
|
|
|
176
|
+
## Pre and post hooks
|
|
177
|
+
|
|
178
|
+
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.
|
|
179
|
+
|
|
180
|
+
Behaviors worth knowing:
|
|
181
|
+
|
|
182
|
+
- **Silent when absent**: Missing hooks produce no error and no output.
|
|
183
|
+
- **Skip overrides apply to hooks**: A hook value of `""` or `":"` is treated the same as not defining the hook, with no console message.
|
|
184
|
+
- **Skipping the main command skips its hooks**: When `X` is overridden to `""` or `":"`, neither `X:pre` nor `X:post` fires.
|
|
185
|
+
- **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`.
|
|
186
|
+
- **Passthrough args attach only to the main command**: `nmr X --flag value` runs hooks without `--flag value`.
|
|
187
|
+
|
|
188
|
+
Extend `nmr build` with a pre-build step for every workspace package:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// .config/nmr.config.ts
|
|
192
|
+
import { defineConfig } from '@williamthorsen/nmr';
|
|
193
|
+
|
|
194
|
+
export default defineConfig({
|
|
195
|
+
workspaceScripts: {
|
|
196
|
+
'build:pre': 'npx rdy compile',
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Attach a post-build step to one package:
|
|
202
|
+
|
|
203
|
+
```jsonc
|
|
204
|
+
// packages/nmr/package.json
|
|
205
|
+
{
|
|
206
|
+
"scripts": {
|
|
207
|
+
"build:post": "nmr-sync-agent-files",
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The second example calls the bin directly, which sidesteps the workspace-versus-root registry distinction.
|
|
213
|
+
|
|
214
|
+
## Dependency upgrades
|
|
215
|
+
|
|
216
|
+
`nmr upgrade` reports the dependency upgrades available to your repo. What it covers depends on where you run it:
|
|
217
|
+
|
|
218
|
+
| Invocation | Covers |
|
|
219
|
+
| ---------------------------- | ------------------------------------------- |
|
|
220
|
+
| `nmr upgrade` (from root) | the root `package.json` and every workspace |
|
|
221
|
+
| `nmr upgrade` (in a package) | that package |
|
|
222
|
+
| `nmr -F <package> upgrade` | that package, from anywhere |
|
|
223
|
+
| `nmr root:upgrade` | the root `package.json` alone |
|
|
224
|
+
|
|
225
|
+
`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.
|
|
226
|
+
|
|
227
|
+
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:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
nmr upgrade # upgrades available within each package's version ceilings
|
|
231
|
+
nmr upgrade major # major upgrades, still inside the ceilings
|
|
232
|
+
nmr upgrade --write # apply the proposals to package.json
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Configuring upgrades
|
|
236
|
+
|
|
237
|
+
Declare version ceilings in a `taze.config.ts` at the monorepo root:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { defineConfig } from '@williamthorsen/nmr/taze';
|
|
241
|
+
|
|
242
|
+
export default defineConfig({
|
|
243
|
+
packageMode: {
|
|
244
|
+
// Hold @types/node at the Node major your `engines` floor requires.
|
|
245
|
+
'@types/node': 'minor',
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`defineConfig` supplies nmr's shared upgrade policy — currently a seven-day quarantine on brand-new releases — 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`.
|
|
251
|
+
|
|
252
|
+
Everything the [taze configuration](https://github.com/antfu-collective/taze#config-file) accepts is accepted here.
|
|
253
|
+
|
|
254
|
+
> **Note:** `--include-locked` is part of both registry entries because a repo that pins dependencies to exact versions (pnpm's `savePrefix: ''`) has no dependency the tool would otherwise consider — without it, an upgrade pass reports nothing at all. Drop it via `.config/nmr.config.ts` if your repo declares version ranges instead.
|
|
255
|
+
|
|
136
256
|
## Default script registries
|
|
137
257
|
|
|
138
258
|
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.
|
|
@@ -157,6 +277,7 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
|
|
|
157
277
|
| `test:coverage` | `pnpm exec vitest --coverage` |
|
|
158
278
|
| `test:watch` | `pnpm exec vitest --watch` |
|
|
159
279
|
| `typecheck` | `tsgo --noEmit` |
|
|
280
|
+
| `upgrade` | `nmr-taze --include-locked` |
|
|
160
281
|
| `view-coverage` | `open coverage/index.html` |
|
|
161
282
|
|
|
162
283
|
#### Integration test variant
|
|
@@ -178,7 +299,7 @@ A package gets this variant automatically when it contains a `vitest.integration
|
|
|
178
299
|
| Command | Runs |
|
|
179
300
|
| ------- | --------------------------------- |
|
|
180
301
|
| `build` | `pnpm --recursive exec nmr build` |
|
|
181
|
-
| `ci` | `build`, `check:strict`
|
|
302
|
+
| `ci` | `build`, `check:strict`, `audit` |
|
|
182
303
|
| `clean` | `nmr-clean` |
|
|
183
304
|
|
|
184
305
|
#### Check and quality
|
|
@@ -236,12 +357,11 @@ A package gets this variant automatically when it contains a `vitest.integration
|
|
|
236
357
|
|
|
237
358
|
#### Dependencies
|
|
238
359
|
|
|
239
|
-
| Command
|
|
240
|
-
|
|
|
241
|
-
| `
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
| `update:latest` | `pnpm update --latest --recursive` |
|
|
360
|
+
| Command | Runs |
|
|
361
|
+
| --------- | --------------------------------------------------------------- |
|
|
362
|
+
| `upgrade` | `nmr-report-overrides && nmr-taze --include-locked --recursive` |
|
|
363
|
+
|
|
364
|
+
See [dependency upgrades](#dependency-upgrades) for the workflow and its configuration.
|
|
245
365
|
|
|
246
366
|
#### Root-only
|
|
247
367
|
|
|
@@ -255,14 +375,14 @@ These scripts operate on root-level code only (not workspace packages):
|
|
|
255
375
|
| `root:lint:strict` | `strict-lint --ignore-pattern 'packages/**' .` |
|
|
256
376
|
| `root:test` | `vitest --config ./vitest.root.config.ts` |
|
|
257
377
|
| `root:typecheck` | `tsgo --noEmit` |
|
|
378
|
+
| `root:upgrade` | `nmr-taze --include-locked` |
|
|
258
379
|
|
|
259
380
|
#### Utilities
|
|
260
381
|
|
|
261
|
-
| Command
|
|
262
|
-
|
|
|
263
|
-
| `report-overrides`
|
|
264
|
-
| `sync-agent-files`
|
|
265
|
-
| `sync-pnpm-version` | `nmr-sync-pnpm-version` |
|
|
382
|
+
| Command | Runs |
|
|
383
|
+
| ------------------ | ---------------------- |
|
|
384
|
+
| `report-overrides` | `nmr-report-overrides` |
|
|
385
|
+
| `sync-agent-files` | `nmr-sync-agent-files` |
|
|
266
386
|
|
|
267
387
|
## CLI reference
|
|
268
388
|
|
|
@@ -272,11 +392,13 @@ These scripts operate on root-level code only (not workspace packages):
|
|
|
272
392
|
nmr [flags] <command> [args...]
|
|
273
393
|
```
|
|
274
394
|
|
|
395
|
+
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.
|
|
396
|
+
|
|
275
397
|
| Flag | Description | Default |
|
|
276
398
|
| ------------------------ | --------------------------------------------------- | ------- |
|
|
277
399
|
| `-F, --filter <pattern>` | Run command in matching packages | — |
|
|
278
400
|
| `-R, --recursive` | Run command in all packages | — |
|
|
279
|
-
| `-w, --workspace-root` |
|
|
401
|
+
| `-w, --workspace-root` | Use root scripts, running at the monorepo root | — |
|
|
280
402
|
| `-q, --quiet` | Suppress info messages; show full output on failure | — |
|
|
281
403
|
| `-?, --help` | Show available commands | — |
|
|
282
404
|
| `-V, --version` | Show version number | — |
|
|
@@ -290,14 +412,14 @@ nmr build # Compile to .js and .d.ts in one pass
|
|
|
290
412
|
|
|
291
413
|
# From the monorepo root
|
|
292
414
|
nmr test # Root tests + recursive workspace tests
|
|
293
|
-
nmr ci # check:strict +
|
|
415
|
+
nmr ci # build + check:strict + audit
|
|
294
416
|
|
|
295
417
|
# Target specific packages
|
|
296
|
-
nmr
|
|
297
|
-
nmr
|
|
418
|
+
nmr --filter core test # Test only the core package
|
|
419
|
+
nmr --recursive lint # Lint all workspace packages
|
|
298
420
|
|
|
299
421
|
# Force root context from anywhere
|
|
300
|
-
nmr -
|
|
422
|
+
nmr --workspace-root check # Run root check from a package dir
|
|
301
423
|
```
|
|
302
424
|
|
|
303
425
|
## Additional subcommands
|
|
@@ -306,7 +428,7 @@ These commands are available as `nmr` subcommands and as standalone `nmr-`-prefi
|
|
|
306
428
|
|
|
307
429
|
### `report-overrides`
|
|
308
430
|
|
|
309
|
-
Report any active `pnpm.overrides` in the root `package.json
|
|
431
|
+
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.
|
|
310
432
|
|
|
311
433
|
```bash
|
|
312
434
|
nmr report-overrides
|
|
@@ -331,13 +453,16 @@ To expose the synced guidance to Claude Code sessions, add this include to the c
|
|
|
331
453
|
@nmr/AGENTS.md
|
|
332
454
|
```
|
|
333
455
|
|
|
334
|
-
|
|
456
|
+
#### What belongs in the synced file
|
|
335
457
|
|
|
336
|
-
|
|
458
|
+
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.
|
|
337
459
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
460
|
+
A line earns a place in `AGENTS.md` only if both hold:
|
|
461
|
+
|
|
462
|
+
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.
|
|
463
|
+
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.
|
|
464
|
+
|
|
465
|
+
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.
|
|
341
466
|
|
|
342
467
|
## Standalone utilities
|
|
343
468
|
|
|
@@ -353,12 +478,22 @@ nmr-clean
|
|
|
353
478
|
|
|
354
479
|
Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` and `.d.ts` in one pass. Because the compiler parses each source file, every relative import form — static, re-export, dynamic `import()`, and bare side-effect — is rewritten from `.ts` to `.js` in both outputs, and `.ts` occurrences inside strings and comments are left intact. tsconfig `paths` aliases are resolved to runnable relative `.js` specifiers in both outputs, sourced from the package's tsconfig. An aliased import whose target resolves outside the package's `src/` and is not resolvable without the alias mapping fails the build with a diagnostic, rather than being emitted verbatim to produce output that fails at runtime. The build is skipped when no input has changed and the previous output is still on disk (a content-and-path hash is cached under `node_modules/.cache/nmr-compile/`, outside the published output). Deleting the output by any means — `nmr clean`, `rm -rf dist`, `git clean` — therefore forces a rebuild rather than a skip. This is the default `compile` script — run it from a package directory.
|
|
355
480
|
|
|
356
|
-
`typescript` is a peer dependency (`>=5.7.0
|
|
481
|
+
`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.
|
|
357
482
|
|
|
358
483
|
```bash
|
|
359
484
|
nmr-compile
|
|
360
485
|
```
|
|
361
486
|
|
|
487
|
+
### `nmr-taze`
|
|
488
|
+
|
|
489
|
+
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.
|
|
490
|
+
|
|
491
|
+
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.
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
nmr-taze --include-locked --recursive
|
|
495
|
+
```
|
|
496
|
+
|
|
362
497
|
### `ensure-prepublish-hooks`
|
|
363
498
|
|
|
364
499
|
Verify that all publishable workspace packages have a `prepublishOnly` script. Exits non-zero if any are missing.
|
|
@@ -379,25 +514,31 @@ After installing, a consuming repo's root `package.json` scripts shrink to lifec
|
|
|
379
514
|
|
|
380
515
|
```json
|
|
381
516
|
{
|
|
382
|
-
"prepare": "lefthook install"
|
|
383
|
-
"postinstall": "nmr report-overrides"
|
|
517
|
+
"prepare": "lefthook install"
|
|
384
518
|
}
|
|
385
519
|
```
|
|
386
520
|
|
|
387
521
|
Per-package `package.json` files no longer need script entries. Run `nmr <command>` directly.
|
|
388
522
|
|
|
389
|
-
##
|
|
523
|
+
## Workspace introspection
|
|
390
524
|
|
|
391
|
-
|
|
525
|
+
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:
|
|
392
526
|
|
|
393
527
|
```ts
|
|
394
|
-
// __tests__/
|
|
395
|
-
import {
|
|
528
|
+
// __tests__/packages.test.ts
|
|
529
|
+
import { findMonorepoRoot, getWorkspacePackageDirs } from '@williamthorsen/nmr/workspace';
|
|
396
530
|
|
|
397
|
-
|
|
531
|
+
const monorepoRoot = findMonorepoRoot();
|
|
532
|
+
|
|
533
|
+
for (const packageDir of getWorkspacePackageDirs(monorepoRoot)) {
|
|
534
|
+
// assert something about every workspace package
|
|
535
|
+
}
|
|
398
536
|
```
|
|
399
537
|
|
|
400
|
-
|
|
538
|
+
`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.
|
|
539
|
+
|
|
540
|
+
`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.
|
|
541
|
+
|
|
542
|
+
One divergence from pnpm: a directory counts as a package only if it holds a `package.json`, not a `package.yaml` or `package.json5`.
|
|
401
543
|
|
|
402
|
-
|
|
403
|
-
- Node.js version matches between `.tool-versions` and GitHub workflow
|
|
544
|
+
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.
|
package/bin/nmr-taze.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
try {
|
|
3
|
+
await import('../dist/esm/cli-taze.js');
|
|
4
|
+
} catch (error) {
|
|
5
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
+
process.stderr.write('nmr-taze: build output not found — run `pnpm run build` first\n');
|
|
7
|
+
} else {
|
|
8
|
+
process.stderr.write(`nmr-taze: 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
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { reportError } from '@williamthorsen/nmr-core';
|
|
3
|
+
import { runTaze } from "./commands/taze.js";
|
|
4
|
+
try {
|
|
5
|
+
process.exitCode = runTaze(process.argv.slice(2));
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
reportError(error instanceof Error ? error.message : String(error));
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
}
|
|
@@ -4,6 +4,6 @@ export interface BuildOptions {
|
|
|
4
4
|
outdir?: string;
|
|
5
5
|
}
|
|
6
6
|
export declare function buildPackage(packageDir: string, options?: BuildOptions): Promise<void>;
|
|
7
|
-
export declare function computeBuildHash(packageDir: string, files: string[], emitConfig: object): Promise<string>;
|
|
7
|
+
export declare function computeBuildHash(packageDir: string, files: string[], emitConfig: object, compilerVersion: string): Promise<string>;
|
|
8
8
|
export declare function resolveTsconfigChain(packageDir: string, configFileName?: string): string[];
|
|
9
9
|
export declare function resolveBuildCachePath(packageDir: string): string;
|
|
@@ -24,14 +24,14 @@ export async function buildPackage(packageDir, options = {}) {
|
|
|
24
24
|
ignore: options.ignore ?? DEFAULT_IGNORE,
|
|
25
25
|
});
|
|
26
26
|
const dependencies = ['package.json', ...resolveTsconfigChain(packageDir)];
|
|
27
|
-
const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints));
|
|
27
|
+
const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, ts.version, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints));
|
|
28
28
|
if (!changed) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
emitPackage(packageDir, entryPoints, outdir);
|
|
32
32
|
await writeBuildCache(cachePath, currentHash);
|
|
33
33
|
}
|
|
34
|
-
export async function computeBuildHash(packageDir, files, emitConfig) {
|
|
34
|
+
export async function computeBuildHash(packageDir, files, emitConfig, compilerVersion) {
|
|
35
35
|
const hash = createHash('sha256');
|
|
36
36
|
for (const file of [...files].sort()) {
|
|
37
37
|
hash.update(file);
|
|
@@ -39,6 +39,8 @@ export async function computeBuildHash(packageDir, files, emitConfig) {
|
|
|
39
39
|
hash.update(await readFile(path.join(packageDir, file)));
|
|
40
40
|
}
|
|
41
41
|
hash.update(JSON.stringify(emitConfig));
|
|
42
|
+
hash.update('\0');
|
|
43
|
+
hash.update(compilerVersion);
|
|
42
44
|
return hash.digest('hex');
|
|
43
45
|
}
|
|
44
46
|
export function resolveTsconfigChain(packageDir, configFileName = 'tsconfig.json') {
|
|
@@ -140,7 +142,7 @@ function resolveSpecifierReplacement(specifier, sourceContainingFile, compilerOp
|
|
|
140
142
|
const rewritten = swapTypeScriptExtension(specifier);
|
|
141
143
|
return rewritten === specifier ? undefined : rewritten;
|
|
142
144
|
}
|
|
143
|
-
if (
|
|
145
|
+
if (aliasPrefixes.every((prefix) => !(specifier === prefix || specifier.startsWith(prefix)))) {
|
|
144
146
|
return undefined;
|
|
145
147
|
}
|
|
146
148
|
const resolved = ts.resolveModuleName(specifier, sourceContainingFile, compilerOptions, ts.sys).resolvedModule;
|
|
@@ -207,10 +209,10 @@ function findNearestNodeModulesHost(startDir) {
|
|
|
207
209
|
}
|
|
208
210
|
return current;
|
|
209
211
|
}
|
|
210
|
-
async function detectBuildChanges(packageDir, files, emitConfig, cachePath, outputPresent) {
|
|
212
|
+
async function detectBuildChanges(packageDir, files, emitConfig, compilerVersion, cachePath, outputPresent) {
|
|
211
213
|
const packageName = path.basename(packageDir);
|
|
212
214
|
const previousHash = existsSync(cachePath) ? readFileSync(cachePath, 'utf8') : undefined;
|
|
213
|
-
const currentHash = await computeBuildHash(packageDir, files, emitConfig);
|
|
215
|
+
const currentHash = await computeBuildHash(packageDir, files, emitConfig, compilerVersion);
|
|
214
216
|
if (previousHash === currentHash) {
|
|
215
217
|
if (outputPresent) {
|
|
216
218
|
console.info(`${SKIPPED_ICON} ${packageName}: No changes detected. Skipping build.`);
|
|
@@ -235,7 +237,7 @@ async function writeBuildCache(cachePath, hash) {
|
|
|
235
237
|
await writeFile(cachePath, hash);
|
|
236
238
|
}
|
|
237
239
|
function assertSupportedTypeScript() {
|
|
238
|
-
const [majorPart, minorPart] = ts.versionMajorMinor.split('.');
|
|
240
|
+
const [majorPart, minorPart] = ts.versionMajorMinor.split('.', 2);
|
|
239
241
|
const major = majorPart === undefined ? 0 : Number(majorPart);
|
|
240
242
|
const minor = minorPart === undefined ? 0 : Number(minorPart);
|
|
241
243
|
const tooOld = major < MINIMUM_TYPESCRIPT_MAJOR || (major === MINIMUM_TYPESCRIPT_MAJOR && minor < MINIMUM_TYPESCRIPT_MINOR);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { rm } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { loadConfig } from "../config.js";
|
|
4
|
-
import { findContainingPackageDir
|
|
4
|
+
import { findContainingPackageDir } from "../context.js";
|
|
5
5
|
import { applyDevBin, buildWorkspaceRegistry, hasIntegrationTestConfig, resolveScript } from "../resolver.js";
|
|
6
6
|
import { runCommand } from "../runner.js";
|
|
7
|
+
import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
|
|
7
8
|
import { resolveBuildCachePath } from "./build.js";
|
|
8
9
|
const CLEAN_ICON = '🧹';
|
|
9
10
|
const CLEAN_COMMAND = 'clean';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { getWorkspacePackageDirs } from "../context.js";
|
|
4
3
|
import { readPackageJson } from "../helpers/package-json.js";
|
|
5
4
|
import { isObject } from "../helpers/type-guards.js";
|
|
5
|
+
import { getWorkspacePackageDirs } from "../workspace.js";
|
|
6
6
|
export const DEFAULT_HOOK = 'npm run build';
|
|
7
7
|
export function ensurePrepublishHooks(monorepoRoot, options) {
|
|
8
8
|
const hookCommand = options.command ?? DEFAULT_HOOK;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
export interface SpawnOutcome {
|
|
3
|
+
status: number | null;
|
|
4
|
+
error?: Error | undefined;
|
|
5
|
+
}
|
|
6
|
+
export interface RunTazeOptions {
|
|
7
|
+
resolveCliPath?: () => string;
|
|
8
|
+
spawn?: (nodePath: string, argv: string[]) => SpawnOutcome;
|
|
9
|
+
stderr?: Writable;
|
|
10
|
+
}
|
|
11
|
+
export declare function runTaze(args: string[], options?: RunTazeOptions): number;
|
|
12
|
+
export declare function resolveTazeCliPath(): string;
|