@williamthorsen/nmr 0.26.0 → 0.27.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.
@@ -58,9 +58,9 @@ var rootScripts = {
58
58
  "fix:check": ["fmt:check", "lint:check"],
59
59
  fmt: "nmr-fmt --write",
60
60
  "fmt:check": "nmr-fmt --check",
61
- lint: "nmr root:lint && pnpm --recursive exec nmr lint",
62
- "lint:check": "nmr root:lint:check && pnpm --recursive exec nmr lint:check",
63
- "lint:strict": "nmr root:lint:strict && pnpm --recursive exec nmr lint:strict",
61
+ lint: "eslint --fix .",
62
+ "lint:check": "eslint .",
63
+ "lint:strict": "strict-lint",
64
64
  prepush: ["ci", "audit"],
65
65
  "report-overrides": "nmr-report-overrides",
66
66
  "root:check": ["root:typecheck", "fmt:check", "root:lint:check", "root:test"],
@@ -78,11 +78,9 @@ var rootScripts = {
78
78
  "test:coverage": "nmr root:test && pnpm --recursive exec nmr test:coverage",
79
79
  "test:tool": "nmr root:test:tool && pnpm --recursive exec nmr test:tool",
80
80
  "test:unit": "nmr root:test:unit && pnpm --recursive exec nmr test:unit",
81
- // Bare `vitest` at the monorepo root resolves the root `vitest.config.ts`, covering the whole tree in one
82
- // process; a chain like the others would never advance past its first watcher.
83
81
  "test:watch": `vitest ${GATE_PROJECTS} --watch`,
84
82
  typecheck: "nmr root:typecheck && pnpm --recursive exec nmr typecheck",
85
- // A string rather than a composite: Passthrough args attach to the chain's last command.
83
+ // The command must be a string rather than a composite: Passthrough args attach to the chain's last command.
86
84
  upgrade: "nmr-report-overrides && nmr-taze --include-locked --recursive"
87
85
  };
88
86
 
@@ -142,7 +140,7 @@ var default_default = defineRdyKit({
142
140
  },
143
141
  severity: "error",
144
142
  check: () => hasMinDevDependencyVersion("@williamthorsen/nmr", getMinVersion(), {
145
- exempt: (range) => range.startsWith("workspace:")
143
+ exempt: resolvesVersionViaWorkspace
146
144
  }),
147
145
  get fix() {
148
146
  return `pnpm add --save-dev @williamthorsen/nmr@^${getMinVersion()}`;
@@ -175,6 +173,30 @@ var default_default = defineRdyKit({
175
173
  check: () => fileContains(".config/nmr.config.ts", /defineConfig/),
176
174
  fix: "Wrap your config export with defineConfig() from @williamthorsen/nmr/config for type safety"
177
175
  },
176
+ // `error` because falling short produces wrong results rather than a failure. Names and fixes are
177
+ // getters because their version constants are declared below the kit.
178
+ {
179
+ get name() {
180
+ return `eslint >= ${MIN_ESLINT_VERSION}`;
181
+ },
182
+ severity: "error",
183
+ skip: () => !hasDevDependency("eslint") ? "eslint not installed" : false,
184
+ check: hasSupportedEslintVersion,
185
+ get fix() {
186
+ return `pnpm add --save-dev eslint@^${MIN_ESLINT_VERSION} \u2014 earlier releases resolve config from the working directory, so nmr's root lint and lint:check would apply the root config to every package`;
187
+ }
188
+ },
189
+ {
190
+ get name() {
191
+ return `@williamthorsen/strict-lint >= ${MIN_STRICT_LINT_VERSION}`;
192
+ },
193
+ severity: "error",
194
+ skip: () => !hasDevDependency("@williamthorsen/strict-lint") ? "strict-lint not installed" : false,
195
+ check: hasSupportedStrictLintVersion,
196
+ get fix() {
197
+ return `pnpm add --save-dev @williamthorsen/strict-lint@^${MIN_STRICT_LINT_VERSION} \u2014 earlier releases pin ESLint to one config and resolve ceilings from the working directory, so nmr's root lint:strict would report the wrong rules for every package`;
198
+ }
199
+ },
178
200
  // -- Root script cleanup -------------------------------------------------
179
201
  {
180
202
  name: "root package.json has no nmr-provided scripts",
@@ -270,7 +292,10 @@ var CONFIG_EXTENSIONS = "{ts,mts,cts,js,mjs,cjs}";
270
292
  var SHARED_VITEST_MODULE = "@williamthorsen/nmr/vitest";
271
293
  var SHARED_PRETTIER_MODULE = "@williamthorsen/nmr/prettier";
272
294
  var INERT_PRETTIER_CONFIGS = [".prettierrc", ".prettierrc.{json,json5,yaml,yml,toml}"];
273
- var RE_EXPORT_LINE_PATTERN = /^export\s*(?:\{\s*default\s*\}|\*)\s*from\s*['"]\.\.\/[^'"]*['"];?$/;
295
+ var RE_EXPORT_LINE_PATTERN = /^export\s*(?:\{\s*default\s*}|\*)\s*from\s*['"]\.\.\/[^'"]*['"];?$/;
296
+ var MIN_ESLINT_VERSION = "10.0.0";
297
+ var MIN_STRICT_LINT_VERSION = "9.3.0";
298
+ var WORKSPACE_VERSION_MARKERS = ["catalog:", "workspace:"];
274
299
  function allWorkspacePackagesCanBuild() {
275
300
  const packagesDir = join(process.cwd(), "packages");
276
301
  if (!existsSync(packagesDir)) return true;
@@ -363,12 +388,22 @@ function formatPaths(paths) {
363
388
  ${paths.map((path2) => ` ${path2}`).join("\n")}`;
364
389
  }
365
390
  function getMinVersion() {
366
- const picked = { "version": "0.26.0" };
391
+ const picked = { "version": "0.27.0" };
367
392
  if (typeof picked.version !== "string") {
368
393
  throw new TypeError("nmr/package.json: 'version' must be a string");
369
394
  }
370
395
  return picked.version;
371
396
  }
397
+ function hasSupportedEslintVersion() {
398
+ return hasMinDevDependencyVersion("eslint", MIN_ESLINT_VERSION, {
399
+ exempt: resolvesVersionViaWorkspace
400
+ });
401
+ }
402
+ function hasSupportedStrictLintVersion() {
403
+ return hasMinDevDependencyVersion("@williamthorsen/strict-lint", MIN_STRICT_LINT_VERSION, {
404
+ exempt: resolvesVersionViaWorkspace
405
+ });
406
+ }
372
407
  function importsSharedExport(content, exportName, moduleSpecifier) {
373
408
  if (content === void 0) return false;
374
409
  const pattern = new RegExp(String.raw`import\s*\{[^}]*\b${exportName}\b[^}]*\}\s*from\s*['"]${moduleSpecifier}['"]`);
@@ -430,11 +465,14 @@ function readFileIn(cwd, relativePath) {
430
465
  return void 0;
431
466
  }
432
467
  }
468
+ function resolvesVersionViaWorkspace(range) {
469
+ return WORKSPACE_VERSION_MARKERS.some((marker) => range.startsWith(marker));
470
+ }
433
471
  function scriptExists(name) {
434
472
  const pkg = readPackageJson();
435
473
  if (!pkg) return false;
436
474
  const scripts = pkg.scripts;
437
- return isRecord(scripts) && name in scripts;
475
+ return isRecord(scripts) && Object.hasOwn(scripts, name);
438
476
  }
439
477
  function scriptMatches(name, pattern) {
440
478
  const pkg = readPackageJson();
@@ -459,6 +497,8 @@ export {
459
497
  codeQualityWorkflowDoesNotUseNmrPrepush,
460
498
  default_default as default,
461
499
  everyTestFileNamesItsTier,
500
+ hasSupportedEslintVersion,
501
+ hasSupportedStrictLintVersion,
462
502
  noReExportOnlyVitestConfigs,
463
503
  noRetiredVitestConfigs,
464
504
  prettierConfigBuildsOnSharedConfig,
@@ -9,8 +9,8 @@
9
9
  "path": "kits/default.js",
10
10
  "readyupVersion": "0.23.0",
11
11
  "source": "kits/default.ts",
12
- "sourceHash": "f646e7fb",
13
- "targetHash": "56564046"
12
+ "sourceHash": "d95cb08e",
13
+ "targetHash": "e4affaeb"
14
14
  }
15
15
  ]
16
16
  }
package/README.md CHANGED
@@ -3,21 +3,15 @@
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.26.0 (2026-08-04)
6
+ ## Release notes — v0.27.0 (2026-08-05)
7
7
 
8
8
  ### 🎉 Features
9
9
 
10
- - Publish each package's readyup kit from the package that owns it (#590)
10
+ - 🚨 **Breaking:** Lint the whole tree in one process, not one per workspace (#604)
11
11
 
12
- ReadyUp checks for `@williamthorsen/nmr`, `@williamthorsen/release-kit`, and `v11y-check` are now run directly against these packages, guaranteeing a version-appropriate check, instead of against their host repo. The package must be installed as a direct dependency in the calling repo.
12
+ Linting of the entire monorepo now runs in a single call instead of via fan-out, eliminating the startup time previously incurred for each workspace.
13
13
 
14
- The syntax is `rdy run --from npm:<package>`. `rdy run --packages` runs the kits contained in any packages listed in `.config/readyup.config.ts`. Requires `readyup` 0.23 or later.
15
-
16
- - Ship agent guidance as CodeAssembly package content (#591)
17
-
18
- A repo that consumes `@williamthorsen/nmr` can now adopt `nmr`'s agent guidance by naming the package in `.agents/codeassembly.yaml`. `codeassembly sync` then delivers that guidance to every agent harness. Guidance updates are thus delivered directly with each package, eliminating the need to keep a guidance file in sync. The `nmr sync-agent-files` command has been removed.
19
-
20
- Migration guide: Delete `.agents/nmr/`, remove the `@nmr/AGENTS.md` include line, and remove all uses of `check:agent-files`.
14
+ As a result of the change, `lint:pre` and `lint:post` hooks in a workspace are no longer triggered by a run from the monorepo root. They continue to be triggered by a run targeting the workspace itself.
21
15
  <!-- /section:release-notes -->
22
16
 
23
17
  ## Installation
@@ -229,7 +223,7 @@ $ nmr ci
229
223
  # ... four minutes of build, typecheck, lint, and coverage ...
230
224
 
231
225
  $ nmr ci
232
- ⏭️ ci: passed 2m ago on this tree (🚀 saved ~4m). Re-run with --no-cache.
226
+ ⏭️ ci: passed 2m ago on this tree (🚀 saved ~4m).
233
227
  ```
234
228
 
235
229
  Composite commands expand into child `nmr` processes, so one green `nmr ci` records a pass for every cacheable command in the chain, at every scope it ran at. A later `nmr check`, `nmr typecheck`, or `nmr -F core test` on the same tree skips too. The skip line is suppressed by `-q`.
@@ -474,11 +468,17 @@ The same six names the workspace registry carries, so a command means the same t
474
468
 
475
469
  #### Lint
476
470
 
477
- | Command | Runs |
478
- | ------------- | --------------------------------------------------------------- |
479
- | `lint` | `nmr root:lint && pnpm --recursive exec nmr lint` |
480
- | `lint:check` | `nmr root:lint:check && pnpm --recursive exec nmr lint:check` |
481
- | `lint:strict` | `nmr root:lint:strict && pnpm --recursive exec nmr lint:strict` |
471
+ The lint commands respect each package's own `eslint.config.*`. Use the `root:` variants below to lint root-level code alone.
472
+
473
+ Requires `eslint` >= 10 and, for `lint:strict`, `@williamthorsen/strict-lint` >= 9.3.0.
474
+
475
+ | Command | Runs |
476
+ | ------------- | ---------------- |
477
+ | `lint` | `eslint --fix .` |
478
+ | `lint:check` | `eslint .` |
479
+ | `lint:strict` | `strict-lint` |
480
+
481
+ The run never enters a package, so per-package `lint:pre` and `lint:post` hooks do not fire from the root. They still run from inside the package and under `nmr -F <package> lint`.
482
482
 
483
483
  #### Format
484
484
 
@@ -10,5 +10,5 @@ delivery: ambient
10
10
  - 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.
11
11
  - Never `npx nmr`: inside a git worktree it can resolve a different nmr from outside the tree. Fall back to `pnpm exec nmr`.
12
12
  - `nmr ci` runs what a code-quality workflow runs (build, then strict checks) and not the dependency audit, which belongs to a workflow of its own; `nmr prepush` runs both. Neither installs or binds a git hook.
13
- - A check that already passed on the current working tree skips, printing `⏭️ <command>: passed …`. That is a real pass, not a stale one: any edit, addition, or deletion re-runs the command. A skipped command produces no files, so where you need an artifact rather than an exit status (a fresh `coverage/`), run `nmr --no-cache <command>`.
13
+ - A check that already passed on the current working tree skips, printing `⏭️ <command>: passed …`. Any edit, addition, or deletion re-runs it, so a skip is a real pass and needs no confirming: never pass `--no-cache` to get a "real" run, which costs the command's full runtime to reach the same answer. Its one use is a file rather than an exit status -- a skipped command writes none, so a fresh `coverage/` needs `nmr --no-cache test:coverage`.
14
14
  - 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`), the `nmr-compile` build (there is no repo-local build script), and the check-result cache's key, its `checkCache` config, and every condition that disables it.
@@ -93,7 +93,7 @@ export function formatSkipLine(command, entry, now) {
93
93
  const age = formatDuration(Math.max(0, now - Date.parse(entry.recordedAt)));
94
94
  const saving = formatSaving(entry.durationMs);
95
95
  const savingClause = saving === undefined ? '' : ` (${saving})`;
96
- return `⏭️ ${command}: passed ${age} ago on this tree${savingClause}. Re-run with --no-cache.`;
96
+ return `⏭️ ${command}: passed ${age} ago on this tree${savingClause}.`;
97
97
  }
98
98
  export async function readBuildOutputState(monorepoRoot, config) {
99
99
  const state = { missing: [], digests: {} };
@@ -81,7 +81,7 @@ function validateCheckCacheField(value, configPath) {
81
81
  const enabled = checkCache.enabled;
82
82
  if (enabled !== undefined) {
83
83
  if (typeof enabled !== 'boolean') {
84
- throw new Error(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
84
+ throw new TypeError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
85
85
  }
86
86
  config.enabled = enabled;
87
87
  }
@@ -35,9 +35,9 @@ export const rootScripts = {
35
35
  'fix:check': ['fmt:check', 'lint:check'],
36
36
  fmt: 'nmr-fmt --write',
37
37
  'fmt:check': 'nmr-fmt --check',
38
- lint: 'nmr root:lint && pnpm --recursive exec nmr lint',
39
- 'lint:check': 'nmr root:lint:check && pnpm --recursive exec nmr lint:check',
40
- 'lint:strict': 'nmr root:lint:strict && pnpm --recursive exec nmr lint:strict',
38
+ lint: 'eslint --fix .',
39
+ 'lint:check': 'eslint .',
40
+ 'lint:strict': 'strict-lint',
41
41
  prepush: ['ci', 'audit'],
42
42
  'report-overrides': 'nmr-report-overrides',
43
43
  'root:check': ['root:typecheck', 'fmt:check', 'root:lint:check', 'root:test'],
@@ -15,7 +15,7 @@ export function formatSaving(milliseconds) {
15
15
  }
16
16
  return `${SPEED_BOOST_ICON} saved ~${formatDuration(milliseconds)}`;
17
17
  }
18
- const MILLISECONDS_PER_SECOND = 1000;
18
+ const MILLISECONDS_PER_SECOND = 1_000;
19
19
  const MINUTES_PER_HOUR = 60;
20
20
  const SECONDS_PER_MINUTE = 60;
21
21
  const SPEED_BOOST_ICON = '🚀';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/nmr",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "private": false,
5
5
  "description": "Context-aware script runner for pnpm monorepos",
6
6
  "keywords": [
@@ -68,7 +68,7 @@
68
68
  "prettier-plugin-sh": "0.19.0",
69
69
  "taze": "19.16.0",
70
70
  "yaml": "2.9.0",
71
- "@williamthorsen/nmr-core": "0.9.0"
71
+ "@williamthorsen/nmr-core": "0.9.2"
72
72
  },
73
73
  "devDependencies": {
74
74
  "prettier": "3.9.6",