@williamthorsen/nmr 0.19.0 → 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 -96
- package/README.md +131 -47
- 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/commands/clean.js +2 -1
- package/dist/esm/commands/ensure-prepublish-hooks.js +1 -1
- package/dist/esm/context.d.ts +0 -2
- package/dist/esm/context.js +7 -60
- package/dist/esm/default-scripts.js +1 -2
- package/dist/esm/help.js +5 -3
- 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/workspace.d.ts +2 -0
- package/dist/esm/workspace.js +38 -0
- package/package.json +7 -12
- package/bin/nmr-sync-pnpm-version.js +0 -11
- package/dist/esm/cli-sync-pnpm-version.d.ts +0 -1
- 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/AGENTS.md
CHANGED
|
@@ -2,100 +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 <7`; TypeScript 7 ships no compiler API). 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
|
-
## Dependency upgrades
|
|
44
|
-
|
|
45
|
-
`nmr upgrade` reports available dependency upgrades, honoring the version ceilings the repo declares in its root `taze.config.ts`. Scope follows context: from the root it covers the root `package.json` and every workspace in one pass; from inside a package, or under `nmr -F <package> upgrade`, it covers that package. `nmr root:upgrade` covers the root `package.json` alone. Run the root-context commands from the monorepo root.
|
|
46
|
-
|
|
47
|
-
There are no `outdated` or `update` commands; `upgrade` replaced them. Reach for `pnpm outdated` or `pnpm update` directly if you need a report that ignores the ceilings.
|
|
48
|
-
|
|
49
|
-
Arguments pass through to the underlying tool, including the range mode: `nmr upgrade major` proposes major upgrades, still inside the ceilings. Every form above is report-only until you add `--write`.
|
|
50
|
-
|
|
51
|
-
**`-w` is not the same on both sides of the command.** `nmr upgrade -w` passes `--write` through and rewrites `package.json`; `nmr -w upgrade` is nmr's own `--workspace-root` and writes nothing. Use the long forms.
|
|
52
|
-
|
|
53
|
-
To change what a repo holds back, edit its `taze.config.ts`. It imports `defineConfig` from `@williamthorsen/nmr/taze`, which supplies the shared upgrade policy — so that file should carry only the ceilings specific to that repo, and the repo needs no dependency on the upgrade tool itself.
|
|
54
|
-
|
|
55
|
-
## Override behaviors
|
|
56
|
-
|
|
57
|
-
In `.config/nmr.config.ts` or a package's `package.json`, override values have special semantics:
|
|
58
|
-
|
|
59
|
-
- `""` (empty string): Skip the script with a "Skipping" message; exit 0.
|
|
60
|
-
- `":"`: No-op; exit 0. Prefer this over `""` if your repo enforces non-empty script values.
|
|
61
|
-
- Any other string: Runs in place of the default.
|
|
62
|
-
|
|
63
|
-
## Pre and post hooks
|
|
64
|
-
|
|
65
|
-
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.
|
|
66
|
-
|
|
67
|
-
Behaviors worth knowing:
|
|
68
|
-
|
|
69
|
-
- **Silent when absent**: Missing hooks produce no error and no output.
|
|
70
|
-
- **Skip overrides apply to hooks**: A hook value of `""` or `":"` is treated the same as not defining the hook. No console message.
|
|
71
|
-
- **Skipping the main command skips its hooks**: When `X` is overridden to `""` or `":"`, neither `X:pre` nor `X:post` fires.
|
|
72
|
-
- **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`.
|
|
73
|
-
- **Passthrough args attach only to the main command**: `nmr X --flag value` runs hooks without `--flag value`.
|
|
74
|
-
|
|
75
|
-
Worked examples:
|
|
76
|
-
|
|
77
|
-
```ts
|
|
78
|
-
// .config/nmr.config.ts — extend `nmr build` with a pre-build compile step
|
|
79
|
-
import { defineConfig } from '@williamthorsen/nmr';
|
|
80
|
-
|
|
81
|
-
export default defineConfig({
|
|
82
|
-
workspaceScripts: {
|
|
83
|
-
'build:pre': 'npx rdy compile',
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
```jsonc
|
|
89
|
-
// packages/nmr/package.json — re-stamp .agents/nmr/AGENTS.md after every build
|
|
90
|
-
{
|
|
91
|
-
"scripts": {
|
|
92
|
-
"build:post": "nmr-sync-agent-files",
|
|
93
|
-
},
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
The second example calls the bin directly to sidestep the workspace-vs-root registry distinction.
|
|
98
|
-
|
|
99
|
-
## Agent-file sync
|
|
100
|
-
|
|
101
|
-
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,27 +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
7
|
|
|
8
8
|
### 🎉 Features
|
|
9
9
|
|
|
10
|
-
- 🚨 **Breaking:**
|
|
10
|
+
- 🚨 **Breaking:** Migrate to code-quality workflow v7 and retire the version-restatement apparatus (#503)
|
|
11
11
|
|
|
12
|
-
|
|
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
13
|
|
|
14
|
-
|
|
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.
|
|
27
|
+
|
|
28
|
+
### 🐛 Bug fixes
|
|
29
|
+
|
|
30
|
+
- Anchor script execution to the resolved registry's directory (#517)
|
|
31
|
+
|
|
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.
|
|
15
33
|
<!-- /section:release-notes -->
|
|
16
34
|
|
|
17
35
|
## Installation
|
|
18
36
|
|
|
19
|
-
Requires Node.js 24 or later.
|
|
37
|
+
Requires Node.js 24.16 or later.
|
|
20
38
|
|
|
21
39
|
```bash
|
|
22
40
|
pnpm add -D @williamthorsen/nmr
|
|
23
41
|
```
|
|
24
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
|
+
|
|
25
52
|
## Quick start
|
|
26
53
|
|
|
27
54
|
nmr works out of the box with no configuration. It ships with built-in scripts for common monorepo tasks.
|
|
@@ -39,7 +66,7 @@ From the monorepo root:
|
|
|
39
66
|
```bash
|
|
40
67
|
nmr test # Run root tests + recursive workspace tests
|
|
41
68
|
nmr build # Build all packages
|
|
42
|
-
nmr ci # Run check:strict &&
|
|
69
|
+
nmr ci # Run build && check:strict && audit (full CI pipeline)
|
|
43
70
|
```
|
|
44
71
|
|
|
45
72
|
nmr detects where you are and selects the right scripts automatically — see [context-aware resolution](#context-aware-resolution) below.
|
|
@@ -48,19 +75,26 @@ nmr detects where you are and selects the right scripts automatically — see [c
|
|
|
48
75
|
|
|
49
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.
|
|
50
77
|
|
|
51
|
-
| Where you run `nmr`
|
|
52
|
-
|
|
|
53
|
-
| Monorepo root
|
|
54
|
-
| Inside a workspace package
|
|
55
|
-
| 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` |
|
|
56
83
|
|
|
57
|
-
|
|
84
|
+
Relative paths in a script resolve against that working directory, not the invocation directory.
|
|
85
|
+
|
|
86
|
+
Use `--workspace-root` to escape package context:
|
|
58
87
|
|
|
59
88
|
```bash
|
|
60
89
|
# From inside packages/nmr-core, run the root check suite
|
|
61
|
-
nmr -
|
|
90
|
+
nmr --workspace-root check
|
|
62
91
|
```
|
|
63
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
|
+
|
|
64
98
|
## Three-tier override system
|
|
65
99
|
|
|
66
100
|
Scripts resolve through three tiers. Higher tiers override lower ones:
|
|
@@ -92,6 +126,8 @@ Script values can be `string` or `string[]`. Arrays expand to chained `nmr` sub-
|
|
|
92
126
|
// expands to: nmr lint && nmr fmt
|
|
93
127
|
```
|
|
94
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
|
+
|
|
95
131
|
## Configuration
|
|
96
132
|
|
|
97
133
|
Create `.config/nmr.config.ts` in the monorepo root to add or override scripts:
|
|
@@ -137,6 +173,44 @@ For example, if a workspace script resolves to `my-cli --verbose`, nmr rewrites
|
|
|
137
173
|
|
|
138
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.
|
|
139
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
|
+
|
|
140
214
|
## Dependency upgrades
|
|
141
215
|
|
|
142
216
|
`nmr upgrade` reports the dependency upgrades available to your repo. What it covers depends on where you run it:
|
|
@@ -148,6 +222,8 @@ For example, if a workspace script resolves to `my-cli --verbose`, nmr rewrites
|
|
|
148
222
|
| `nmr -F <package> upgrade` | that package, from anywhere |
|
|
149
223
|
| `nmr root:upgrade` | the root `package.json` alone |
|
|
150
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
|
+
|
|
151
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:
|
|
152
228
|
|
|
153
229
|
```bash
|
|
@@ -156,8 +232,6 @@ nmr upgrade major # major upgrades, still inside the ceilings
|
|
|
156
232
|
nmr upgrade --write # apply the proposals to package.json
|
|
157
233
|
```
|
|
158
234
|
|
|
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
235
|
### Configuring upgrades
|
|
162
236
|
|
|
163
237
|
Declare version ceilings in a `taze.config.ts` at the monorepo root:
|
|
@@ -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
|
|
@@ -283,9 +357,9 @@ A package gets this variant automatically when it contains a `vitest.integration
|
|
|
283
357
|
|
|
284
358
|
#### Dependencies
|
|
285
359
|
|
|
286
|
-
| Command | Runs
|
|
287
|
-
| --------- |
|
|
288
|
-
| `upgrade` | `nmr-taze --include-locked --recursive` |
|
|
360
|
+
| Command | Runs |
|
|
361
|
+
| --------- | --------------------------------------------------------------- |
|
|
362
|
+
| `upgrade` | `nmr-report-overrides && nmr-taze --include-locked --recursive` |
|
|
289
363
|
|
|
290
364
|
See [dependency upgrades](#dependency-upgrades) for the workflow and its configuration.
|
|
291
365
|
|
|
@@ -305,11 +379,10 @@ These scripts operate on root-level code only (not workspace packages):
|
|
|
305
379
|
|
|
306
380
|
#### Utilities
|
|
307
381
|
|
|
308
|
-
| Command
|
|
309
|
-
|
|
|
310
|
-
| `report-overrides`
|
|
311
|
-
| `sync-agent-files`
|
|
312
|
-
| `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` |
|
|
313
386
|
|
|
314
387
|
## CLI reference
|
|
315
388
|
|
|
@@ -319,11 +392,13 @@ These scripts operate on root-level code only (not workspace packages):
|
|
|
319
392
|
nmr [flags] <command> [args...]
|
|
320
393
|
```
|
|
321
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
|
+
|
|
322
397
|
| Flag | Description | Default |
|
|
323
398
|
| ------------------------ | --------------------------------------------------- | ------- |
|
|
324
399
|
| `-F, --filter <pattern>` | Run command in matching packages | — |
|
|
325
400
|
| `-R, --recursive` | Run command in all packages | — |
|
|
326
|
-
| `-w, --workspace-root` |
|
|
401
|
+
| `-w, --workspace-root` | Use root scripts, running at the monorepo root | — |
|
|
327
402
|
| `-q, --quiet` | Suppress info messages; show full output on failure | — |
|
|
328
403
|
| `-?, --help` | Show available commands | — |
|
|
329
404
|
| `-V, --version` | Show version number | — |
|
|
@@ -337,14 +412,14 @@ nmr build # Compile to .js and .d.ts in one pass
|
|
|
337
412
|
|
|
338
413
|
# From the monorepo root
|
|
339
414
|
nmr test # Root tests + recursive workspace tests
|
|
340
|
-
nmr ci # check:strict +
|
|
415
|
+
nmr ci # build + check:strict + audit
|
|
341
416
|
|
|
342
417
|
# Target specific packages
|
|
343
|
-
nmr
|
|
344
|
-
nmr
|
|
418
|
+
nmr --filter core test # Test only the core package
|
|
419
|
+
nmr --recursive lint # Lint all workspace packages
|
|
345
420
|
|
|
346
421
|
# Force root context from anywhere
|
|
347
|
-
nmr -
|
|
422
|
+
nmr --workspace-root check # Run root check from a package dir
|
|
348
423
|
```
|
|
349
424
|
|
|
350
425
|
## Additional subcommands
|
|
@@ -353,7 +428,7 @@ These commands are available as `nmr` subcommands and as standalone `nmr-`-prefi
|
|
|
353
428
|
|
|
354
429
|
### `report-overrides`
|
|
355
430
|
|
|
356
|
-
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.
|
|
357
432
|
|
|
358
433
|
```bash
|
|
359
434
|
nmr report-overrides
|
|
@@ -378,13 +453,16 @@ To expose the synced guidance to Claude Code sessions, add this include to the c
|
|
|
378
453
|
@nmr/AGENTS.md
|
|
379
454
|
```
|
|
380
455
|
|
|
381
|
-
|
|
456
|
+
#### What belongs in the synced file
|
|
382
457
|
|
|
383
|
-
|
|
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.
|
|
384
459
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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.
|
|
388
466
|
|
|
389
467
|
## Standalone utilities
|
|
390
468
|
|
|
@@ -408,7 +486,7 @@ nmr-compile
|
|
|
408
486
|
|
|
409
487
|
### `nmr-taze`
|
|
410
488
|
|
|
411
|
-
Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched. This is what
|
|
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.
|
|
412
490
|
|
|
413
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.
|
|
414
492
|
|
|
@@ -436,25 +514,31 @@ After installing, a consuming repo's root `package.json` scripts shrink to lifec
|
|
|
436
514
|
|
|
437
515
|
```json
|
|
438
516
|
{
|
|
439
|
-
"prepare": "lefthook install"
|
|
440
|
-
"postinstall": "nmr report-overrides"
|
|
517
|
+
"prepare": "lefthook install"
|
|
441
518
|
}
|
|
442
519
|
```
|
|
443
520
|
|
|
444
521
|
Per-package `package.json` files no longer need script entries. Run `nmr <command>` directly.
|
|
445
522
|
|
|
446
|
-
##
|
|
523
|
+
## Workspace introspection
|
|
447
524
|
|
|
448
|
-
|
|
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:
|
|
449
526
|
|
|
450
527
|
```ts
|
|
451
|
-
// __tests__/
|
|
452
|
-
import {
|
|
528
|
+
// __tests__/packages.test.ts
|
|
529
|
+
import { findMonorepoRoot, getWorkspacePackageDirs } from '@williamthorsen/nmr/workspace';
|
|
530
|
+
|
|
531
|
+
const monorepoRoot = findMonorepoRoot();
|
|
453
532
|
|
|
454
|
-
|
|
533
|
+
for (const packageDir of getWorkspacePackageDirs(monorepoRoot)) {
|
|
534
|
+
// assert something about every workspace package
|
|
535
|
+
}
|
|
455
536
|
```
|
|
456
537
|
|
|
457
|
-
|
|
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`.
|
|
458
543
|
|
|
459
|
-
|
|
460
|
-
- 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.
|
|
@@ -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
|
};
|
|
@@ -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;
|
package/dist/esm/context.d.ts
CHANGED
|
@@ -5,7 +5,5 @@ export interface ResolvedContext {
|
|
|
5
5
|
packageDir?: string;
|
|
6
6
|
config: NmrConfig;
|
|
7
7
|
}
|
|
8
|
-
export declare function findMonorepoRoot(startDir?: string): string;
|
|
9
|
-
export declare function getWorkspacePackageDirs(monorepoRoot: string): string[];
|
|
10
8
|
export declare function findContainingPackageDir(dir: string, workspacePackageDirs: string[]): string | undefined;
|
|
11
9
|
export declare function resolveContext(cwd?: string): Promise<ResolvedContext>;
|
package/dist/esm/context.js
CHANGED
|
@@ -1,71 +1,18 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
|
-
import { parse } from 'yaml';
|
|
4
2
|
import { loadConfig } from "./config.js";
|
|
5
|
-
import {
|
|
6
|
-
export function findMonorepoRoot(startDir) {
|
|
7
|
-
let dir = path.resolve(startDir ?? process.cwd());
|
|
8
|
-
for (;;) {
|
|
9
|
-
if (existsSync(path.join(dir, 'pnpm-workspace.yaml'))) {
|
|
10
|
-
return dir;
|
|
11
|
-
}
|
|
12
|
-
const parent = path.dirname(dir);
|
|
13
|
-
if (parent === dir) {
|
|
14
|
-
throw new Error('Could not find monorepo root: no pnpm-workspace.yaml found in any parent directory');
|
|
15
|
-
}
|
|
16
|
-
dir = parent;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
export function getWorkspacePackageDirs(monorepoRoot) {
|
|
20
|
-
const workspaceFile = path.join(monorepoRoot, 'pnpm-workspace.yaml');
|
|
21
|
-
const content = readFileSync(workspaceFile, 'utf8');
|
|
22
|
-
const parsed = parse(content);
|
|
23
|
-
const packages = getPackagesFromParsedYaml(parsed);
|
|
24
|
-
if (!packages) {
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
const dirs = [];
|
|
28
|
-
for (const pattern of packages) {
|
|
29
|
-
if (pattern.endsWith('/*')) {
|
|
30
|
-
const prefix = pattern.slice(0, -2);
|
|
31
|
-
const prefixDir = path.resolve(monorepoRoot, prefix);
|
|
32
|
-
if (existsSync(prefixDir)) {
|
|
33
|
-
for (const entry of readdirSync(prefixDir)) {
|
|
34
|
-
const fullPath = path.join(prefixDir, entry);
|
|
35
|
-
if (statSync(fullPath).isDirectory() && existsSync(path.join(fullPath, 'package.json'))) {
|
|
36
|
-
dirs.push(fullPath);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
else if (!pattern.includes('*')) {
|
|
42
|
-
const fullPath = path.resolve(monorepoRoot, pattern);
|
|
43
|
-
if (existsSync(fullPath) && existsSync(path.join(fullPath, 'package.json'))) {
|
|
44
|
-
dirs.push(fullPath);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return dirs;
|
|
49
|
-
}
|
|
3
|
+
import { findMonorepoRoot, getWorkspacePackageDirs } from "./workspace.js";
|
|
50
4
|
export function findContainingPackageDir(dir, workspacePackageDirs) {
|
|
51
5
|
const resolved = path.resolve(dir);
|
|
6
|
+
let nearest;
|
|
52
7
|
for (const pkgDir of workspacePackageDirs) {
|
|
53
8
|
const resolvedPkgDir = path.resolve(pkgDir);
|
|
54
|
-
if (resolved
|
|
55
|
-
|
|
9
|
+
if (resolved !== resolvedPkgDir && !resolved.startsWith(resolvedPkgDir + path.sep))
|
|
10
|
+
continue;
|
|
11
|
+
if (nearest === undefined || resolvedPkgDir.length > nearest.length) {
|
|
12
|
+
nearest = resolvedPkgDir;
|
|
56
13
|
}
|
|
57
14
|
}
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
function getPackagesFromParsedYaml(parsed) {
|
|
61
|
-
if (!isObject(parsed))
|
|
62
|
-
return undefined;
|
|
63
|
-
const packages = parsed.packages;
|
|
64
|
-
if (!Array.isArray(packages))
|
|
65
|
-
return undefined;
|
|
66
|
-
if (!packages.every((p) => typeof p === 'string'))
|
|
67
|
-
return undefined;
|
|
68
|
-
return packages;
|
|
15
|
+
return nearest;
|
|
69
16
|
}
|
|
70
17
|
export async function resolveContext(cwd) {
|
|
71
18
|
const resolvedCwd = path.resolve(cwd ?? process.cwd());
|
|
@@ -55,10 +55,9 @@ export const rootScripts = {
|
|
|
55
55
|
'root:typecheck': 'tsgo --noEmit',
|
|
56
56
|
'root:upgrade': 'nmr-taze --include-locked',
|
|
57
57
|
'sync-agent-files': 'nmr-sync-agent-files',
|
|
58
|
-
'sync-pnpm-version': 'nmr-sync-pnpm-version',
|
|
59
58
|
test: 'nmr root:test && pnpm --recursive exec nmr test',
|
|
60
59
|
'test:coverage': 'nmr root:test && pnpm --recursive exec nmr test:coverage',
|
|
61
60
|
'test:watch': 'vitest --watch',
|
|
62
61
|
typecheck: 'nmr root:typecheck && pnpm --recursive exec nmr typecheck',
|
|
63
|
-
upgrade: 'nmr-taze --include-locked --recursive',
|
|
62
|
+
upgrade: 'nmr-report-overrides && nmr-taze --include-locked --recursive',
|
|
64
63
|
};
|
package/dist/esm/help.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isHookName } from "./helpers/hook-name.js";
|
|
2
|
-
import { buildRootRegistry, buildWorkspaceRegistry, describeScript, isSelfReferential, readPackageJsonScripts, } from "./resolver.js";
|
|
2
|
+
import { buildRootRegistry, buildWorkspaceRegistry, describeScript, hasIntegrationTestConfig, isSelfReferential, readPackageJsonScripts, } from "./resolver.js";
|
|
3
3
|
export function generateHelp(config, packageDir, useRoot) {
|
|
4
4
|
const lines = [
|
|
5
5
|
'Usage: nmr [flags] <command> [args...]',
|
|
@@ -7,15 +7,17 @@ export function generateHelp(config, packageDir, useRoot) {
|
|
|
7
7
|
'Flags:',
|
|
8
8
|
' -F, --filter <pattern> Run command in matching packages',
|
|
9
9
|
' -R, --recursive Run command in all packages',
|
|
10
|
-
' -w, --workspace-root
|
|
10
|
+
' -w, --workspace-root Use root scripts, running at the monorepo root',
|
|
11
11
|
' -q, --quiet Suppress output on success; show full output on failure',
|
|
12
12
|
' -?, --help Show this help',
|
|
13
|
+
' -V, --version Show version number',
|
|
13
14
|
'',
|
|
14
15
|
'Workspace commands:',
|
|
15
16
|
];
|
|
16
17
|
const overrides = packageDir === undefined ? {} : collectOverrides(packageDir);
|
|
17
18
|
let hadOverride = false;
|
|
18
|
-
const
|
|
19
|
+
const useIntTests = !useRoot && packageDir !== undefined && hasIntegrationTestConfig(packageDir);
|
|
20
|
+
const workspaceRegistry = filterHooks(buildWorkspaceRegistry(config, useIntTests));
|
|
19
21
|
const workspaceMarked = !useRoot ? applyOverrides(workspaceRegistry, overrides) : new Set();
|
|
20
22
|
if (workspaceMarked.size > 0)
|
|
21
23
|
hadOverride = true;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolvePackageDirs(monorepoRoot: string, patterns: string[]): string[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { globSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const MANIFEST = 'package.json';
|
|
4
|
+
const ALWAYS_EXCLUDED = ['**/node_modules/**'];
|
|
5
|
+
export function resolvePackageDirs(monorepoRoot, patterns) {
|
|
6
|
+
const included = [];
|
|
7
|
+
const excluded = [];
|
|
8
|
+
for (const pattern of patterns) {
|
|
9
|
+
if (pattern.trim() === '')
|
|
10
|
+
continue;
|
|
11
|
+
const isNegated = pattern.startsWith('!');
|
|
12
|
+
const target = isNegated ? excluded : included;
|
|
13
|
+
target.push(toManifestPattern(isNegated ? pattern.slice(1) : pattern));
|
|
14
|
+
}
|
|
15
|
+
if (included.length === 0) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const options = {
|
|
19
|
+
cwd: monorepoRoot,
|
|
20
|
+
exclude: [...excluded, ...ALWAYS_EXCLUDED],
|
|
21
|
+
followSymlinks: true,
|
|
22
|
+
};
|
|
23
|
+
const matches = globSync(included, options);
|
|
24
|
+
const dirs = matches.map((match) => path.dirname(path.resolve(monorepoRoot, match)));
|
|
25
|
+
return [...new Set(dirs)].toSorted();
|
|
26
|
+
}
|
|
27
|
+
function toManifestPattern(pattern) {
|
|
28
|
+
return `${pattern.replace(/\/?$/, '')}/${MANIFEST}`;
|
|
29
|
+
}
|
package/dist/esm/index.d.ts
CHANGED
package/dist/esm/runCli.js
CHANGED
|
@@ -20,9 +20,9 @@ export async function runCli(options) {
|
|
|
20
20
|
}
|
|
21
21
|
const context = await resolveContext(cwd);
|
|
22
22
|
const useRoot = parsed.workspaceRoot || context.isRoot;
|
|
23
|
-
const
|
|
23
|
+
const anchorDir = useRoot ? context.monorepoRoot : (context.packageDir ?? context.monorepoRoot);
|
|
24
24
|
if (parsed.help || !parsed.command) {
|
|
25
|
-
stdout.write(`${generateHelp(context.config,
|
|
25
|
+
stdout.write(`${generateHelp(context.config, anchorDir, useRoot)}\n`);
|
|
26
26
|
return { exitCode: 0 };
|
|
27
27
|
}
|
|
28
28
|
const { command } = parsed;
|
|
@@ -41,8 +41,8 @@ export async function runCli(options) {
|
|
|
41
41
|
}
|
|
42
42
|
const registry = useRoot
|
|
43
43
|
? buildRootRegistry(context.config)
|
|
44
|
-
: buildWorkspaceRegistry(context.config, hasIntegrationTestConfig(
|
|
45
|
-
const resolved = resolveScript(command, registry,
|
|
44
|
+
: buildWorkspaceRegistry(context.config, hasIntegrationTestConfig(anchorDir));
|
|
45
|
+
const resolved = resolveScript(command, registry, anchorDir, parsed.workspaceRoot);
|
|
46
46
|
if (!resolved) {
|
|
47
47
|
if (env.NMR_RUN_IF_PRESENT === '1') {
|
|
48
48
|
return { exitCode: 0 };
|
|
@@ -50,20 +50,20 @@ export async function runCli(options) {
|
|
|
50
50
|
reportError(`Unknown command: ${command}`, stderr);
|
|
51
51
|
return { exitCode: 1 };
|
|
52
52
|
}
|
|
53
|
-
const skipExitCode = handleSkipMessage(resolved.command,
|
|
53
|
+
const skipExitCode = handleSkipMessage(resolved.command, anchorDir, parsed.quiet, stdout);
|
|
54
54
|
if (skipExitCode !== undefined) {
|
|
55
55
|
return { exitCode: skipExitCode };
|
|
56
56
|
}
|
|
57
57
|
if (resolved.source === 'package' && !parsed.quiet && registry[command] !== undefined) {
|
|
58
|
-
stdout.write(`📦 ${path.basename(
|
|
58
|
+
stdout.write(`📦 ${path.basename(anchorDir)}: Using override script: ${resolved.command}\n`);
|
|
59
59
|
}
|
|
60
60
|
const substitutedCommand = applyDevBin(resolved.command, context.config.devBin, context.monorepoRoot);
|
|
61
61
|
const mainCommand = substitutedCommand + passthrough;
|
|
62
62
|
const isHookInvocation = isHookName(command);
|
|
63
63
|
const fullCommand = isHookInvocation
|
|
64
64
|
? mainCommand
|
|
65
|
-
: wrapWithHooks(command, mainCommand, registry,
|
|
66
|
-
const exitCode = runCommand(fullCommand,
|
|
65
|
+
: wrapWithHooks(command, mainCommand, registry, anchorDir, parsed.workspaceRoot);
|
|
66
|
+
const exitCode = runCommand(fullCommand, anchorDir, runOptions);
|
|
67
67
|
return { exitCode };
|
|
68
68
|
}
|
|
69
69
|
function parseArgs(args) {
|
|
@@ -121,35 +121,35 @@ function parseArgs(args) {
|
|
|
121
121
|
}
|
|
122
122
|
return { ok: true, parsed };
|
|
123
123
|
}
|
|
124
|
-
function handleSkipMessage(resolvedCommand,
|
|
124
|
+
function handleSkipMessage(resolvedCommand, anchorDir, quiet, stdout) {
|
|
125
125
|
if (resolvedCommand === '') {
|
|
126
126
|
if (!quiet) {
|
|
127
|
-
stdout.write(`⛔ ${path.basename(
|
|
127
|
+
stdout.write(`⛔ ${path.basename(anchorDir)}: Override script is defined but empty. Skipping.\n`);
|
|
128
128
|
}
|
|
129
129
|
return 0;
|
|
130
130
|
}
|
|
131
131
|
if (resolvedCommand === ':') {
|
|
132
132
|
if (!quiet) {
|
|
133
|
-
stdout.write(`⛔ ${path.basename(
|
|
133
|
+
stdout.write(`⛔ ${path.basename(anchorDir)}: Override script is a no-op. Skipping.\n`);
|
|
134
134
|
}
|
|
135
135
|
return 0;
|
|
136
136
|
}
|
|
137
137
|
return undefined;
|
|
138
138
|
}
|
|
139
|
-
function wrapWithHooks(command, mainCommand, registry,
|
|
139
|
+
function wrapWithHooks(command, mainCommand, registry, anchorDir, workspaceRoot) {
|
|
140
140
|
const segments = [];
|
|
141
141
|
const flag = workspaceRoot ? '-w ' : '';
|
|
142
|
-
if (hasRunnableHook(`${command}:pre`, registry,
|
|
142
|
+
if (hasRunnableHook(`${command}:pre`, registry, anchorDir, workspaceRoot)) {
|
|
143
143
|
segments.push(`nmr ${flag}${command}:pre`);
|
|
144
144
|
}
|
|
145
145
|
segments.push(mainCommand);
|
|
146
|
-
if (hasRunnableHook(`${command}:post`, registry,
|
|
146
|
+
if (hasRunnableHook(`${command}:post`, registry, anchorDir, workspaceRoot)) {
|
|
147
147
|
segments.push(`nmr ${flag}${command}:post`);
|
|
148
148
|
}
|
|
149
149
|
return segments.join(' && ');
|
|
150
150
|
}
|
|
151
|
-
function hasRunnableHook(hookName, registry,
|
|
152
|
-
const resolved = resolveScript(hookName, registry,
|
|
151
|
+
function hasRunnableHook(hookName, registry, anchorDir, workspaceRoot) {
|
|
152
|
+
const resolved = resolveScript(hookName, registry, anchorDir, workspaceRoot);
|
|
153
153
|
if (!resolved)
|
|
154
154
|
return false;
|
|
155
155
|
return resolved.command !== '' && resolved.command !== ':';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import { isObject } from "./helpers/type-guards.js";
|
|
5
|
+
import { resolvePackageDirs } from "./helpers/workspace-patterns.js";
|
|
6
|
+
export function findMonorepoRoot(startDir) {
|
|
7
|
+
let dir = path.resolve(startDir ?? process.cwd());
|
|
8
|
+
for (;;) {
|
|
9
|
+
if (existsSync(path.join(dir, 'pnpm-workspace.yaml'))) {
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
const parent = path.dirname(dir);
|
|
13
|
+
if (parent === dir) {
|
|
14
|
+
throw new Error('Could not find monorepo root: no pnpm-workspace.yaml found in any parent directory');
|
|
15
|
+
}
|
|
16
|
+
dir = parent;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function getWorkspacePackageDirs(monorepoRoot) {
|
|
20
|
+
const workspaceFile = path.join(monorepoRoot, 'pnpm-workspace.yaml');
|
|
21
|
+
const content = readFileSync(workspaceFile, 'utf8');
|
|
22
|
+
const parsed = parse(content);
|
|
23
|
+
const packages = getPackagesFromParsedYaml(parsed);
|
|
24
|
+
if (!packages) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return resolvePackageDirs(monorepoRoot, packages);
|
|
28
|
+
}
|
|
29
|
+
function getPackagesFromParsedYaml(parsed) {
|
|
30
|
+
if (!isObject(parsed))
|
|
31
|
+
return undefined;
|
|
32
|
+
const packages = parsed.packages;
|
|
33
|
+
if (!Array.isArray(packages))
|
|
34
|
+
return undefined;
|
|
35
|
+
if (!packages.every((p) => typeof p === 'string'))
|
|
36
|
+
return undefined;
|
|
37
|
+
return packages;
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Context-aware script runner for
|
|
5
|
+
"description": "Context-aware script runner for pnpm monorepos",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"monorepo",
|
|
8
8
|
"pnpm",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"types": "./dist/esm/taze.d.ts",
|
|
35
35
|
"default": "./dist/esm/taze.js"
|
|
36
36
|
},
|
|
37
|
-
"./
|
|
38
|
-
"types": "./dist/esm/
|
|
39
|
-
"default": "./dist/esm/
|
|
37
|
+
"./workspace": {
|
|
38
|
+
"types": "./dist/esm/workspace.d.ts",
|
|
39
|
+
"default": "./dist/esm/workspace.js"
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"bin": {
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
"nmr-compile": "bin/nmr-compile.js",
|
|
47
47
|
"nmr-report-overrides": "bin/nmr-report-overrides.js",
|
|
48
48
|
"nmr-sync-agent-files": "bin/nmr-sync-agent-files.js",
|
|
49
|
-
"nmr-sync-pnpm-version": "bin/nmr-sync-pnpm-version.js",
|
|
50
49
|
"nmr-taze": "bin/nmr-taze.js"
|
|
51
50
|
},
|
|
52
51
|
"files": [
|
|
@@ -59,17 +58,13 @@
|
|
|
59
58
|
"jiti": "2.7.0",
|
|
60
59
|
"taze": "19.15.1",
|
|
61
60
|
"yaml": "2.9.0",
|
|
62
|
-
"
|
|
63
|
-
"@williamthorsen/nmr-core": "0.7.1"
|
|
64
|
-
},
|
|
65
|
-
"devDependencies": {
|
|
66
|
-
"@williamthorsen/toolbelt.strings": "3.1.6"
|
|
61
|
+
"@williamthorsen/nmr-core": "0.7.2"
|
|
67
62
|
},
|
|
68
63
|
"peerDependencies": {
|
|
69
64
|
"typescript": ">=5.7.0 <7"
|
|
70
65
|
},
|
|
71
66
|
"engines": {
|
|
72
|
-
"node": ">=24"
|
|
67
|
+
"node": ">=24.16.0"
|
|
73
68
|
},
|
|
74
69
|
"publishConfig": {
|
|
75
70
|
"access": "public"
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
try {
|
|
3
|
-
await import('../dist/esm/cli-sync-pnpm-version.js');
|
|
4
|
-
} catch (error) {
|
|
5
|
-
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
-
process.stderr.write('nmr-sync-pnpm-version: build output not found — run `pnpm run build` first\n');
|
|
7
|
-
} else {
|
|
8
|
-
process.stderr.write(`nmr-sync-pnpm-version: failed to load: ${error.message}\n`);
|
|
9
|
-
}
|
|
10
|
-
process.exit(1);
|
|
11
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
|
-
import { syncPnpmVersion } from "./commands/sync-pnpm-version.js";
|
|
3
|
-
import { findMonorepoRoot } from "./context.js";
|
|
4
|
-
try {
|
|
5
|
-
const monorepoRoot = findMonorepoRoot();
|
|
6
|
-
syncPnpmVersion(monorepoRoot);
|
|
7
|
-
}
|
|
8
|
-
catch (error) {
|
|
9
|
-
reportError(error instanceof Error ? error.message : String(error));
|
|
10
|
-
process.exitCode = 1;
|
|
11
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { parseDocument } from 'yaml';
|
|
4
|
-
import { CodeQualityPnpmWorkflowSchema, getPnpmVersionNodes } from "../helpers/code-quality-pnpm-action.js";
|
|
5
|
-
import { readPackageJson } from "../helpers/package-json.js";
|
|
6
|
-
const WORKFLOW_RELATIVE_PATH = '.github/workflows/code-quality.yaml';
|
|
7
|
-
function extractPnpmVersion(packageManager) {
|
|
8
|
-
if (!packageManager) {
|
|
9
|
-
return null;
|
|
10
|
-
}
|
|
11
|
-
const match = /^pnpm@(.+)$/.exec(packageManager);
|
|
12
|
-
return match?.[1] ?? null;
|
|
13
|
-
}
|
|
14
|
-
export function syncPnpmVersion(monorepoRoot) {
|
|
15
|
-
console.info('Synchronizing pnpm version in GitHub workflow...');
|
|
16
|
-
const pkg = readPackageJson(monorepoRoot);
|
|
17
|
-
const pnpmVersion = extractPnpmVersion(pkg.packageManager);
|
|
18
|
-
if (!pnpmVersion) {
|
|
19
|
-
throw new Error(`Could not extract pnpm version from package.json packageManager field\npackageManager field: ${pkg.packageManager ?? '(not set)'}`);
|
|
20
|
-
}
|
|
21
|
-
console.info(`Package.json pnpm version: ${pnpmVersion}`);
|
|
22
|
-
const workflowPath = path.join(monorepoRoot, WORKFLOW_RELATIVE_PATH);
|
|
23
|
-
const doc = parseDocument(readFileSync(workflowPath, 'utf8'));
|
|
24
|
-
const [parseError] = doc.errors;
|
|
25
|
-
if (parseError) {
|
|
26
|
-
throw new Error(`Failed to parse workflow file: ${workflowPath}\n${parseError.message}`);
|
|
27
|
-
}
|
|
28
|
-
CodeQualityPnpmWorkflowSchema.parse(doc.toJS());
|
|
29
|
-
const outdatedNodes = getPnpmVersionNodes(doc).filter((node) => String(node.value) !== pnpmVersion);
|
|
30
|
-
if (outdatedNodes.length === 0) {
|
|
31
|
-
console.info('Workflow pnpm version is already up to date');
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
for (const node of outdatedNodes) {
|
|
35
|
-
node.value = pnpmVersion;
|
|
36
|
-
}
|
|
37
|
-
writeFileSync(workflowPath, doc.toString(), 'utf8');
|
|
38
|
-
console.info(`✓ Updated ${outdatedNodes.length} pnpm-version occurrence(s) → ${pnpmVersion}`);
|
|
39
|
-
}
|
|
40
|
-
export { extractPnpmVersion };
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { type Document, type Scalar } from 'yaml';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
export declare const CodeQualityPnpmWorkflowSchema: z.ZodObject<{
|
|
4
|
-
jobs: z.ZodObject<{
|
|
5
|
-
'code-quality': z.ZodObject<{
|
|
6
|
-
with: z.ZodObject<{
|
|
7
|
-
'pnpm-version': z.ZodString;
|
|
8
|
-
}, z.core.$loose>;
|
|
9
|
-
}, z.core.$loose>;
|
|
10
|
-
}, z.core.$loose>;
|
|
11
|
-
}, z.core.$loose>;
|
|
12
|
-
export type CodeQualityPnpmWorkflow = z.infer<typeof CodeQualityPnpmWorkflowSchema>;
|
|
13
|
-
export declare function getPnpmVersionNodes(doc: Document): Scalar[];
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { isScalar, visit } from 'yaml';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
export const CodeQualityPnpmWorkflowSchema = z.looseObject({
|
|
4
|
-
jobs: z.looseObject({
|
|
5
|
-
'code-quality': z.looseObject({
|
|
6
|
-
with: z.looseObject({
|
|
7
|
-
'pnpm-version': z.string(),
|
|
8
|
-
}),
|
|
9
|
-
}),
|
|
10
|
-
}),
|
|
11
|
-
});
|
|
12
|
-
export function getPnpmVersionNodes(doc) {
|
|
13
|
-
const nodes = [];
|
|
14
|
-
visit(doc, {
|
|
15
|
-
Pair(_, pair) {
|
|
16
|
-
if (isScalar(pair.key) && pair.key.value === 'pnpm-version' && isScalar(pair.value)) {
|
|
17
|
-
nodes.push(pair.value);
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
return nodes;
|
|
22
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { findMonorepoRoot } from '../context.js';
|
|
2
|
-
export declare function checkPnpmVersionConsistency(monorepoRoot: string): void;
|
|
3
|
-
export declare function checkNodeVersionConsistency(monorepoRoot: string): void;
|
|
4
|
-
export declare function getPnpmVersionFromAction(monorepoRoot: string): Promise<string>;
|
|
5
|
-
export declare function getPnpmVersionFromPackageJson(monorepoRoot: string): string;
|
|
6
|
-
export declare function getNodeVersionFromAction(monorepoRoot: string): Promise<string>;
|
|
7
|
-
export declare function runConsistencyChecks(): void;
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
-
import { findMonorepoRoot } from "../context.js";
|
|
5
|
-
export { findMonorepoRoot } from "../context.js";
|
|
6
|
-
import { getRuntimeVersionFromAsdf } from "./helpers/get-runtime-version-from-asdf.js";
|
|
7
|
-
import { getStringFromYamlFile } from "./helpers/get-string-from-yaml-file.js";
|
|
8
|
-
import { getValueAtPathOrThrow } from "./helpers/get-value-at-path.js";
|
|
9
|
-
const GITHUB_ACTION_FILE_PATH = '.github/workflows/code-quality.yaml';
|
|
10
|
-
export function checkPnpmVersionConsistency(monorepoRoot) {
|
|
11
|
-
describe('pnpm version consistency', () => {
|
|
12
|
-
it('pnpm version is the same in GitHub action and package.json', async () => {
|
|
13
|
-
const actionVersion = await getPnpmVersionFromAction(monorepoRoot);
|
|
14
|
-
const packageJsonVersion = getPnpmVersionFromPackageJson(monorepoRoot);
|
|
15
|
-
expect(actionVersion).toBe(packageJsonVersion);
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
export function checkNodeVersionConsistency(monorepoRoot) {
|
|
20
|
-
describe('Node.js version consistency', () => {
|
|
21
|
-
it('version is the same in GitHub action and .tool-versions', async () => {
|
|
22
|
-
const toolVersion = await getRuntimeVersionFromAsdf('nodejs', monorepoRoot);
|
|
23
|
-
const actionVersion = await getNodeVersionFromAction(monorepoRoot);
|
|
24
|
-
expect(toolVersion).toBe(actionVersion);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
export async function getPnpmVersionFromAction(monorepoRoot) {
|
|
29
|
-
const actionPath = path.join(monorepoRoot, GITHUB_ACTION_FILE_PATH);
|
|
30
|
-
return getStringFromYamlFile(actionPath, 'jobs.code-quality.with.pnpm-version', 'pnpm version');
|
|
31
|
-
}
|
|
32
|
-
export function getPnpmVersionFromPackageJson(monorepoRoot) {
|
|
33
|
-
const pkgPath = path.join(monorepoRoot, 'package.json');
|
|
34
|
-
const raw = fs.readFileSync(pkgPath, 'utf8');
|
|
35
|
-
const pkg = JSON.parse(raw);
|
|
36
|
-
const pm = getValueAtPathOrThrow(pkg, 'packageManager');
|
|
37
|
-
if (typeof pm !== 'string') {
|
|
38
|
-
throw new TypeError('"packageManager" field missing or not a string in package.json.');
|
|
39
|
-
}
|
|
40
|
-
const [name, version] = pm.split('@', 2);
|
|
41
|
-
if (name !== 'pnpm') {
|
|
42
|
-
throw new Error('packageManager is not pnpm.');
|
|
43
|
-
}
|
|
44
|
-
if (!version) {
|
|
45
|
-
throw new Error('pnpm version missing in package.json.');
|
|
46
|
-
}
|
|
47
|
-
return version;
|
|
48
|
-
}
|
|
49
|
-
export async function getNodeVersionFromAction(monorepoRoot) {
|
|
50
|
-
const actionPath = path.join(monorepoRoot, GITHUB_ACTION_FILE_PATH);
|
|
51
|
-
return getStringFromYamlFile(actionPath, 'jobs.code-quality.with.node-version', 'Node.js version');
|
|
52
|
-
}
|
|
53
|
-
export function runConsistencyChecks() {
|
|
54
|
-
const monorepoRoot = findMonorepoRoot();
|
|
55
|
-
checkPnpmVersionConsistency(monorepoRoot);
|
|
56
|
-
checkNodeVersionConsistency(monorepoRoot);
|
|
57
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function getRuntimeVersionFromAsdf(runtime: string, monorepoRoot: string): Promise<string>;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
export async function getRuntimeVersionFromAsdf(runtime, monorepoRoot) {
|
|
5
|
-
const toolVersionsPath = path.join(monorepoRoot, '.tool-versions');
|
|
6
|
-
const toolVersions = await fs.promises.readFile(toolVersionsPath, { encoding: 'utf8' });
|
|
7
|
-
const versionLine = toolVersions.split('\n').find((line) => line.trim().startsWith(runtime));
|
|
8
|
-
assert.ok(versionLine, `${runtime} not found in .tool-versions`);
|
|
9
|
-
const [, version] = versionLine.trim().split(/\s+/, 2);
|
|
10
|
-
assert.ok(version, `${runtime} version missing in .tool-versions`);
|
|
11
|
-
return version;
|
|
12
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function getStringFromYamlFile(filePath: string, keyPath: string, label: string): Promise<string>;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import { parse } from 'yaml';
|
|
4
|
-
import { getValueAtPathOrThrow } from "./get-value-at-path.js";
|
|
5
|
-
export async function getStringFromYamlFile(filePath, keyPath, label) {
|
|
6
|
-
const raw = await fs.promises.readFile(filePath, { encoding: 'utf8' });
|
|
7
|
-
const parsed = parse(raw);
|
|
8
|
-
assert.ok(parsed, `YAML content not found in ${filePath}`);
|
|
9
|
-
const value = getValueAtPathOrThrow(parsed, keyPath);
|
|
10
|
-
assert.ok(typeof value === 'string' && value.length > 0, `${label} not found at ${keyPath}`);
|
|
11
|
-
return value;
|
|
12
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function getValueAtPathOrThrow(obj: unknown, objPath: string): unknown;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
function isObject(value) {
|
|
2
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
-
}
|
|
4
|
-
export function getValueAtPathOrThrow(obj, objPath) {
|
|
5
|
-
if (!isObject(obj)) {
|
|
6
|
-
throw new Error('Expected an object as root value.');
|
|
7
|
-
}
|
|
8
|
-
const keys = objPath.split('.');
|
|
9
|
-
let current = obj;
|
|
10
|
-
for (const key of keys) {
|
|
11
|
-
if (Array.isArray(current)) {
|
|
12
|
-
const index = Number(key);
|
|
13
|
-
if (Number.isNaN(index)) {
|
|
14
|
-
throw new TypeError(`Expected array index at segment "${key}" in path "${objPath}"`);
|
|
15
|
-
}
|
|
16
|
-
if (index >= 0 && index < current.length) {
|
|
17
|
-
current = current[index];
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
throw new Error(`Array index out of bounds: "${key}" in path "${objPath}"`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
else if (isObject(current)) {
|
|
24
|
-
if (!(key in current)) {
|
|
25
|
-
throw new Error(`Missing key "${key}" in path "${objPath}"`);
|
|
26
|
-
}
|
|
27
|
-
current = current[key];
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
throw new Error(`Unexpected non-object/non-array at segment "${key}" in path "${objPath}"`);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return current;
|
|
34
|
-
}
|