@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.
- package/.readyup/kits/default.js +50 -10
- package/.readyup/manifest.json +2 -2
- package/README.md +16 -16
- package/agents/guidance/rulebooks/nmr.md +1 -1
- package/dist/esm/check-cache.js +1 -1
- package/dist/esm/config.js +1 -1
- package/dist/esm/default-scripts.js +3 -3
- package/dist/esm/helpers/duration.js +1 -1
- package/package.json +2 -2
package/.readyup/kits/default.js
CHANGED
|
@@ -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: "
|
|
62
|
-
"lint:check": "
|
|
63
|
-
"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
|
-
//
|
|
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:
|
|
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
|
|
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.
|
|
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
|
|
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,
|
package/.readyup/manifest.json
CHANGED
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.
|
|
6
|
+
## Release notes — v0.27.0 (2026-08-05)
|
|
7
7
|
|
|
8
8
|
### 🎉 Features
|
|
9
9
|
|
|
10
|
-
-
|
|
10
|
+
- 🚨 **Breaking:** Lint the whole tree in one process, not one per workspace (#604)
|
|
11
11
|
|
|
12
|
-
|
|
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
|
-
|
|
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).
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
|
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 …`.
|
|
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.
|
package/dist/esm/check-cache.js
CHANGED
|
@@ -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}
|
|
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: {} };
|
package/dist/esm/config.js
CHANGED
|
@@ -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
|
|
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: '
|
|
39
|
-
'lint:check': '
|
|
40
|
-
'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 =
|
|
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.
|
|
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.
|
|
71
|
+
"@williamthorsen/nmr-core": "0.9.2"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"prettier": "3.9.6",
|