@williamthorsen/nmr 0.32.1 → 0.32.2

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.
@@ -43,9 +43,8 @@ var workspaceScripts = {
43
43
  "test:unit": "pnpm exec vitest --project unit",
44
44
  "test:watch": `pnpm exec vitest ${GATE_PROJECTS} --watch`,
45
45
  typecheck: "tsgo --noEmit",
46
- // Without `--include-locked`, nothing would be reported in a repo that pins exact version numbers. The
47
- // command is a string because neither half names an nmr command: both are binaries.
48
- upgrade: "nmr-report-catalog && nmr-taze --include-locked",
46
+ // The command is a string because neither half names an nmr command: both are binaries.
47
+ upgrade: "nmr-report-catalog && nmr-taze",
49
48
  "view-coverage": "open coverage/index.html"
50
49
  };
51
50
  var rootScripts = {
@@ -81,7 +80,7 @@ var rootScripts = {
81
80
  "root:typecheck": "tsgo --noEmit",
82
81
  // Carries the override report for the same reason `upgrade` does: both end in the tool that rewrites a
83
82
  // `pnpm.overrides` block, so both need the reporter's rejection ahead of them.
84
- "root:upgrade": "nmr-report-overrides && nmr-taze --include-locked",
83
+ "root:upgrade": "nmr-report-overrides && nmr-taze",
85
84
  test: ["root:test", "-R test"],
86
85
  "test:all": ["root:test:all", "-R test:all"],
87
86
  "test:coverage": ["root:test", "-R test:coverage"],
@@ -93,7 +92,7 @@ var rootScripts = {
93
92
  typecheck: [ROOT_TYPECHECK_STEP, { run: "-R typecheck", declinesArgs: true }],
94
93
  // The command is a string because neither half names an nmr command: both are binaries, and a composite
95
94
  // element can name only a command.
96
- upgrade: "nmr-report-overrides && nmr-taze --include-locked --recursive"
95
+ upgrade: "nmr-report-overrides && nmr-taze --recursive"
97
96
  };
98
97
 
99
98
  // src/resolve-scripts.ts
@@ -272,6 +271,13 @@ var default_default = defineRdyKit({
272
271
  check: () => prettierConfigBuildsOnSharedConfig(),
273
272
  fix: "Replace the Prettier config with: import { definePrettierConfig } from '@williamthorsen/nmr/prettier'; export default definePrettierConfig();"
274
273
  },
274
+ // -- Shared upgrade policy -----------------------------------------------
275
+ {
276
+ name: "taze.config.ts builds on @williamthorsen/nmr/taze",
277
+ severity: "warn",
278
+ check: () => tazeConfigBuildsOnSharedConfig(),
279
+ fix: "Replace the taze config with: import { defineConfig } from '@williamthorsen/nmr/taze'; export default defineConfig(); \u2014 nmr's upgrade policy reaches a repo only through this file, so without it `nmr upgrade` reports nothing where dependencies are pinned to exact versions"
280
+ },
275
281
  // -- Audit dependency --------------------------------------------------------
276
282
  {
277
283
  name: "v11y-check in devDependencies",
@@ -303,6 +309,8 @@ var CONFIG_EXTENSIONS = "{ts,mts,cts,js,mjs,cjs}";
303
309
  var SHARED_VITEST_MODULE = "@williamthorsen/nmr/vitest";
304
310
  var SHARED_PRETTIER_MODULE = "@williamthorsen/nmr/prettier";
305
311
  var INERT_PRETTIER_CONFIGS = [".prettierrc", ".prettierrc.{json,json5,yaml,yml,toml}"];
312
+ var SHARED_TAZE_MODULE = "@williamthorsen/nmr/taze";
313
+ var INERT_TAZE_CONFIGS = [".tazerc", ".tazerc.json", "taze.config.json"];
306
314
  var RE_EXPORT_LINE_PATTERN = /^export\s*(?:\{\s*default\s*}|\*)\s*from\s*['"]\.\.\/[^'"]*['"];?$/;
307
315
  var MIN_ESLINT_VERSION = "10.0.0";
308
316
  var MIN_STRICT_LINT_VERSION = "9.3.0";
@@ -394,7 +402,7 @@ function formatPaths(paths) {
394
402
  ${paths.map((path2) => ` ${path2}`).join("\n")}`;
395
403
  }
396
404
  function getMinVersion() {
397
- const picked = { "version": "0.32.1" };
405
+ const picked = { "version": "0.32.2" };
398
406
  if (typeof picked["version"] !== "string") {
399
407
  throw new TypeError("nmr/package.json: 'version' must be a string");
400
408
  }
@@ -495,6 +503,22 @@ function readPnpmFieldKeys(content) {
495
503
  function resolvesVersionViaWorkspace(range) {
496
504
  return WORKSPACE_VERSION_MARKERS.some((marker) => range.startsWith(marker));
497
505
  }
506
+ function tazeConfigBuildsOnSharedConfig(cwd = process.cwd()) {
507
+ const configs = findFiles([`taze.config.${CONFIG_EXTENSIONS}`], cwd);
508
+ if (configs.length === 0) {
509
+ return { ok: false, detail: describeMissingTazeConfig(cwd) };
510
+ }
511
+ const stale = configs.filter(
512
+ (relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), "defineConfig", SHARED_TAZE_MODULE)
513
+ );
514
+ if (stale.length === 0) return true;
515
+ return { ok: false, detail: `does not import defineConfig from ${SHARED_TAZE_MODULE}: ${stale.join(", ")}` };
516
+ }
517
+ function describeMissingTazeConfig(cwd) {
518
+ const inert = findFiles(INERT_TAZE_CONFIGS, cwd);
519
+ if (inert.length > 0) return `holds no code to call the factory: ${inert.join(", ")}`;
520
+ return "taze.config.ts is missing";
521
+ }
498
522
  function toolVersionsHasNoPnpm() {
499
523
  const content = readFile(".tool-versions");
500
524
  if (content === void 0) return true;
@@ -515,6 +539,7 @@ export {
515
539
  noReExportOnlyVitestConfigs,
516
540
  noRetiredVitestConfigs,
517
541
  prettierConfigBuildsOnSharedConfig,
542
+ tazeConfigBuildsOnSharedConfig,
518
543
  vitestConfigBuildsOnSharedConfig,
519
544
  vitestRootConfigBuildsOnSharedConfig
520
545
  };
@@ -8,12 +8,12 @@
8
8
  "esbuildVersion": "0.28.1",
9
9
  "inputs": [
10
10
  {
11
- "hash": "67afdcb4",
11
+ "hash": "904ac753",
12
12
  "kind": "module",
13
13
  "path": "kits/default.ts"
14
14
  },
15
15
  {
16
- "hash": "01e1f853",
16
+ "hash": "cca25de2",
17
17
  "kind": "inline",
18
18
  "path": "../package.json",
19
19
  "paths": [
@@ -21,7 +21,7 @@
21
21
  ]
22
22
  },
23
23
  {
24
- "hash": "3dfc2ee7",
24
+ "hash": "94bb3c62",
25
25
  "kind": "module",
26
26
  "path": "../src/default-scripts.ts"
27
27
  },
@@ -40,8 +40,8 @@
40
40
  "path": "kits/default.js",
41
41
  "readyupVersion": "0.29.0",
42
42
  "source": "kits/default.ts",
43
- "sourceHash": "67afdcb4",
44
- "targetHash": "1fad8728"
43
+ "sourceHash": "904ac753",
44
+ "targetHash": "bc4d49d1"
45
45
  }
46
46
  ]
47
47
  }
package/README.md CHANGED
@@ -3,15 +3,19 @@
3
3
  Context-aware script runner for pnpm monorepos. Ships an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
4
4
 
5
5
  <!-- section:release-notes -->
6
- ## Release notes — v0.32.1 (2026-08-18)
6
+ ## Release notes — v0.32.2 (2026-08-19)
7
7
 
8
8
  ### 🐛 Bug fixes
9
9
 
10
- - Reject a `package.json` entry that chains steps onto a self-reference (#707)
10
+ - Recognize nmr behind a quoted environment assignment (#709)
11
11
 
12
- Fixes an issue where a `package.json` script re-invoking its own command alongside other steps (such as `"build": "nmr build && rdy compile"`) silently lost those steps: nmr discarded the entry and ran its own `build` in its place, so `rdy compile` never ran. nmr now refuses to run the entry and reports the error.
12
+ Fixes an issue where a `package.json` entry that re-invoked its own command from behind an environment assignment whose quoted value carried a space (e.g., `"build": "NODE_OPTIONS=\"--a --b\" nmr build"`) caused nmr to hang.
13
13
 
14
- Separately, fixes an issue where the same chain written the other way round (e.g., `"build": "rdy compile && nmr build"`) was honored as an ordinary override, causing infinite recursion.
14
+ - Restore upgrade reporting for repos that pin exact versions (#720)
15
+
16
+ Fixes an issue where `nmr upgrade` reported nothing in a repo that pins dependencies to exact versions, however far behind the tree was. nmr's shared upgrade policy now supplies both settings the upgrade tool needs to report a bare pin -- locked dependencies are included, and the range searched is `minor` -- and the default `upgrade` scripts no longer pass `--include-locked`, which went inert on its own in taze 20.0.1. The policy reaches a repo only through a `taze.config.ts` calling `defineConfig`, and nmr's ReadyUp kit now warns when a repo carries none.
17
+
18
+ The `minor` range mode applies to every dependency, not only a pinned one: a `~` range is searched as `^`, so minor upgrades are proposed for it, and a `packageMode` entry other than `minor` drops its dependency from a default pass rather than narrowing it.
15
19
  <!-- /section:release-notes -->
16
20
 
17
21
  ## Installation
@@ -135,7 +139,7 @@ A string value is spawned through a shell as one command, so an `nmr` invocation
135
139
 
136
140
  The line names the declaration it is an edit to — the file, the field, and the key — and its remedy follows from that site. A `.config/nmr.config.ts` entry keeps its nmr steps as a step list and moves anything else to a `<command>:pre` or `<command>:post` script: a step-list element is composed into `nmr <element>`, so it can name only an nmr command. A `package.json` entry has to go, since `package.json` holds no step list: delete it outright when it restates what nmr already runs for that command, and move the steps it adds to a `<command>:pre` or `<command>:post` script when they are the package's own. Where the registry defines no such command, both apply: name the command in `.config/nmr.config.ts`, and put the package-specific steps in a hook. The pair is named rather than `:post` alone because an entry's extra steps may run on either side of the chain: `rdy verify && nmr compile` puts them first. A hook is the exception at both tiers: nmr wraps a `:pre` or `:post` script in no hooks of its own, so a hook's other steps become a script of their own that the step list names.
137
141
 
138
- nmr recognizes `nmr` in command position: at the start of a step, after an unquoted `&&`, `||`, `;`, `|`, or a newline, and behind `npx`, `bunx`, or an `exec`, `dlx`, or `x` subcommand of `npm`, `pnpm`, `yarn`, or `bun`. This line reports a boundary rather than enforcing a rule — it covers a step nmr runs as written, and only the reporting of the nested run is degraded — and it stays partial in stated ways: a value-taking flag standing immediately before the program name hides it (`npx -p foo nmr`), a launcher outside that set goes unreported, and a `devBin` substitution that introduces `nmr` itself is not reported, the line being an edit to a declaration rather than to a substitution.
142
+ nmr recognizes `nmr` in command position: at the start of a step, after an unquoted `&&`, `||`, `;`, `|`, or a newline, past any leading `NAME=value` assignments, and behind `npx`, `bunx`, or an `exec`, `dlx`, or `x` subcommand of `npm`, `pnpm`, `yarn`, or `bun`. This line reports a boundary rather than enforcing a rule — it covers a step nmr runs as written, and only the reporting of the nested run is degraded — and it stays partial in stated ways: a value-taking flag standing immediately before the program name hides it (`npx -p foo nmr`), a launcher outside that set goes unreported, and a `devBin` substitution that introduces `nmr` itself is not reported, the line being an edit to a declaration rather than to a substitution.
139
143
 
140
144
  A [self-referential `package.json` entry](#three-tier-override-system) is the shape nmr cannot run as written, and it is rejected rather than reported. The same recognition finds it, so the same residuals apply — what goes unrecognized there re-enters nmr without bound, which hangs rather than passing quietly.
141
145
 
@@ -534,8 +538,8 @@ A `pnpm.overrides` block left in the root `package.json` fails the command inste
534
538
  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:
535
539
 
536
540
  ```bash
537
- nmr upgrade # upgrades available within each package's version ceilings
538
- nmr upgrade major # major upgrades, still inside the ceilings
541
+ nmr upgrade # minor and patch upgrades, regardless of the range each dependency declares
542
+ nmr upgrade major # major upgrades, for dependencies carrying no per-package ceiling
539
543
  nmr upgrade --write # apply the proposals to package.json
540
544
  ```
541
545
 
@@ -554,11 +558,16 @@ export default defineConfig({
554
558
  });
555
559
  ```
556
560
 
557
- `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`.
561
+ `defineConfig` supplies nmr's shared upgrade policy — a seven-day quarantine on brand-new releases, and the pair of settings that report a dependency pinned to an exact version: locked dependencies are included, and the range searched is `minor` — so your config carries only what is specific to your repo. Any setting you declare wins over nmr's default. Passing `undefined` clears a default rather than falling back to it, which is how you hand the quarantine policy back to `pnpm-workspace.yaml`'s `minimumReleaseAge`. `mode` is the exception: the upgrade tool carries a default of its own for it, and clearing nmr's leaves no valid range to search, so name a mode instead.
558
562
 
559
563
  Everything the [taze configuration](https://github.com/antfu-collective/taze#config-file) accepts is accepted here.
560
564
 
561
- > **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.
565
+ > **Note:** This file is what activates nmr's upgrade policy at all. A repo without one gets neither the release quarantine nor the settings that report a dependency pinned to an exact version, and `nmr upgrade` reports nothing in a repo using pnpm's `savePrefix: ''`. nmr's ReadyUp kit warns when the file is missing.
566
+
567
+ Two consequences of the `minor` range mode the policy declares:
568
+
569
+ - A `packageMode` entry is honored only by the pass whose mode matches it, or by a `default` pass; every other pass drops that dependency rather than narrowing it. Under the policy's `minor` mode, a `patch` ceiling hides its dependency until you run `nmr upgrade patch`, and a `minor` ceiling hides it from `nmr upgrade major`.
570
+ - A `~` range is searched as `^`, so minor upgrades are proposed for it and `--write` applies them under the original `~`. To hold one dependency to patches, give it a `packageMode` entry of `patch` and read it from `nmr upgrade patch`; to hold the whole repo there, declare `mode: 'patch'`.
562
571
 
563
572
  ## Default script registries
564
573
 
@@ -588,7 +597,7 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
588
597
  | `test:unit` | `pnpm exec vitest --project unit` |
589
598
  | `test:watch` | `pnpm exec vitest --project unit --project tool --watch` |
590
599
  | `typecheck` | `tsgo --noEmit` |
591
- | `upgrade` | `nmr-report-catalog && nmr-taze --include-locked` |
600
+ | `upgrade` | `nmr-report-catalog && nmr-taze` |
592
601
  | `view-coverage` | `open coverage/index.html` |
593
602
 
594
603
  `fmt` and `fmt:check` select their files through git rather than by walking the directory, which is what makes a package-level run and a root-level run apply the same ignore rules — see [file selection](#file-selection).
@@ -702,9 +711,9 @@ A file git ignores is never formatted, even when named directly. Trailing argume
702
711
 
703
712
  #### Dependencies
704
713
 
705
- | Command | Runs |
706
- | --------- | --------------------------------------------------------------- |
707
- | `upgrade` | `nmr-report-overrides && nmr-taze --include-locked --recursive` |
714
+ | Command | Runs |
715
+ | --------- | ---------------------------------------------- |
716
+ | `upgrade` | `nmr-report-overrides && nmr-taze --recursive` |
708
717
 
709
718
  See [dependency upgrades](#dependency-upgrades) for the workflow and its configuration.
710
719
 
@@ -723,7 +732,7 @@ These scripts operate on root-level code only (not workspace packages):
723
732
  | `root:test:tool` | `vitest --config ./vitest.root.config.ts --project tool` |
724
733
  | `root:test:unit` | `vitest --config ./vitest.root.config.ts --project unit` |
725
734
  | `root:typecheck` | `tsgo --noEmit` |
726
- | `root:upgrade` | `nmr-report-overrides && nmr-taze --include-locked` |
735
+ | `root:upgrade` | `nmr-report-overrides && nmr-taze` |
727
736
 
728
737
  #### Utilities
729
738
 
@@ -875,7 +884,7 @@ Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool
875
884
  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.
876
885
 
877
886
  ```bash
878
- nmr-taze --include-locked --recursive
887
+ nmr-taze --recursive
879
888
  ```
880
889
 
881
890
  ### `ensure-prepublish-hooks`
@@ -22,7 +22,7 @@ export const workspaceScripts = {
22
22
  'test:unit': 'pnpm exec vitest --project unit',
23
23
  'test:watch': `pnpm exec vitest ${GATE_PROJECTS} --watch`,
24
24
  typecheck: 'tsgo --noEmit',
25
- upgrade: 'nmr-report-catalog && nmr-taze --include-locked',
25
+ upgrade: 'nmr-report-catalog && nmr-taze',
26
26
  'view-coverage': 'open coverage/index.html',
27
27
  };
28
28
  export const rootScripts = {
@@ -52,7 +52,7 @@ export const rootScripts = {
52
52
  'root:test:tool': 'vitest --config ./vitest.root.config.ts --project tool',
53
53
  'root:test:unit': 'vitest --config ./vitest.root.config.ts --project unit',
54
54
  'root:typecheck': 'tsgo --noEmit',
55
- 'root:upgrade': 'nmr-report-overrides && nmr-taze --include-locked',
55
+ 'root:upgrade': 'nmr-report-overrides && nmr-taze',
56
56
  test: ['root:test', '-R test'],
57
57
  'test:all': ['root:test:all', '-R test:all'],
58
58
  'test:coverage': ['root:test', '-R test:coverage'],
@@ -60,5 +60,5 @@ export const rootScripts = {
60
60
  'test:unit': ['root:test:unit', '-R test:unit'],
61
61
  'test:watch': `vitest ${GATE_PROJECTS} --watch`,
62
62
  typecheck: [ROOT_TYPECHECK_STEP, { run: '-R typecheck', declinesArgs: true }],
63
- upgrade: 'nmr-report-overrides && nmr-taze --include-locked --recursive',
63
+ upgrade: 'nmr-report-overrides && nmr-taze --recursive',
64
64
  };
package/dist/esm/steps.js CHANGED
@@ -10,6 +10,7 @@ const LAUNCHERS = new Map([
10
10
  const QUOTES = new Set(['"', "'"]);
11
11
  const SEGMENT_SEPARATORS = new Set([';', '|', '&', '\n', '\r']);
12
12
  const SHELL_SAFE_TOKEN = /^[\w@%+=:,./-]+$/;
13
+ const TOKEN_SEPARATORS = new Set([' ', '\t', '\n', '\r']);
13
14
  export function composeNmrStep(element, workspaceRoot, declinesArgs = false) {
14
15
  const flags = workspaceRoot ? ['-w'] : [];
15
16
  return { kind: 'structural', argv: ['nmr', ...flags, ...tokenize(element)], ...(declinesArgs && { declinesArgs }) };
@@ -65,7 +66,7 @@ function quoteToken(token) {
65
66
  return "'" + token.replaceAll("'", String.raw `'\''`) + "'";
66
67
  }
67
68
  function readNmrTail(segment) {
68
- const [head, ...rest] = dropLeadingAssignments(tokenize(segment));
69
+ const [head, ...rest] = dropLeadingAssignments(tokenizeSegment(segment));
69
70
  if (head === undefined) {
70
71
  return undefined;
71
72
  }
@@ -167,3 +168,49 @@ function splitSegments(command) {
167
168
  function tokenize(element) {
168
169
  return element.split(/\s+/).filter((token) => token.length > 0);
169
170
  }
171
+ function tokenizeSegment(segment) {
172
+ const tokens = [];
173
+ let current = '';
174
+ let quote;
175
+ let index = 0;
176
+ while (index < segment.length) {
177
+ const char = segment[index] ?? '';
178
+ if (quote !== undefined) {
179
+ if (char === '\\' && quote === '"') {
180
+ current += char + (segment[index + 1] ?? '');
181
+ index += 2;
182
+ continue;
183
+ }
184
+ current += char;
185
+ if (char === quote)
186
+ quote = undefined;
187
+ index += 1;
188
+ continue;
189
+ }
190
+ if (char === '\\') {
191
+ current += char + (segment[index + 1] ?? '');
192
+ index += 2;
193
+ continue;
194
+ }
195
+ if (QUOTES.has(char)) {
196
+ quote = char;
197
+ current += char;
198
+ index += 1;
199
+ continue;
200
+ }
201
+ if (TOKEN_SEPARATORS.has(char)) {
202
+ if (current !== '') {
203
+ tokens.push(current);
204
+ }
205
+ current = '';
206
+ index += 1;
207
+ continue;
208
+ }
209
+ current += char;
210
+ index += 1;
211
+ }
212
+ if (current !== '') {
213
+ tokens.push(current);
214
+ }
215
+ return tokens;
216
+ }
package/dist/esm/taze.js CHANGED
@@ -1,5 +1,7 @@
1
1
  const SHARED_POLICY = {
2
+ includeLocked: true,
2
3
  maturityPeriod: 7,
4
+ mode: 'minor',
3
5
  };
4
6
  export function defineConfig(config) {
5
7
  return { ...SHARED_POLICY, ...config };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/nmr",
3
- "version": "0.32.1",
3
+ "version": "0.32.2",
4
4
  "private": false,
5
5
  "description": "Context-aware script runner for pnpm monorepos",
6
6
  "keywords": [
@@ -73,9 +73,10 @@
73
73
  "prettier-plugin-sh": "0.19.0",
74
74
  "taze": "20.0.1",
75
75
  "yaml": "2.9.0",
76
- "@williamthorsen/nmr-core": "0.10.1"
76
+ "@williamthorsen/nmr-core": "0.10.2"
77
77
  },
78
78
  "devDependencies": {
79
+ "@williamthorsen/toolbelt.filesystem": "0.6.0",
79
80
  "@williamthorsen/toolbelt.testing": "0.3.0",
80
81
  "@williamthorsen/toolbelt.vitest": "0.4.0",
81
82
  "prettier": "3.9.6",