@williamthorsen/nmr 0.26.1 → 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.1" };
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": "8631c00a"
12
+ "sourceHash": "d95cb08e",
13
+ "targetHash": "e4affaeb"
14
14
  }
15
15
  ]
16
16
  }
package/README.md CHANGED
@@ -3,13 +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.1 (2026-08-04)
6
+ ## Release notes — v0.27.0 (2026-08-05)
7
7
 
8
- ### 🐛 Bug fixes
8
+ ### 🎉 Features
9
9
 
10
- - Prevent unnecessary bypasses of the check-result cache (#595)
10
+ - 🚨 **Breaking:** Lint the whole tree in one process, not one per workspace (#604)
11
11
 
12
- Agents are now instructed never to bypass `nmr`'s check-result cache in order to force a supposed "real" run of a check that has already passed in the working tree, unless the output (and not only the result) of that check is needed. The "Re-run with --no-cache" hint, which was being treated as an instruction by agents, has been removed.
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
+
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.
13
15
  <!-- /section:release-notes -->
14
16
 
15
17
  ## Installation
@@ -466,11 +468,17 @@ The same six names the workspace registry carries, so a command means the same t
466
468
 
467
469
  #### Lint
468
470
 
469
- | Command | Runs |
470
- | ------------- | --------------------------------------------------------------- |
471
- | `lint` | `nmr root:lint && pnpm --recursive exec nmr lint` |
472
- | `lint:check` | `nmr root:lint:check && pnpm --recursive exec nmr lint:check` |
473
- | `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`.
474
482
 
475
483
  #### Format
476
484
 
@@ -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.1",
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.1"
71
+ "@williamthorsen/nmr-core": "0.9.2"
72
72
  },
73
73
  "devDependencies": {
74
74
  "prettier": "3.9.6",