@williamthorsen/nmr 0.27.0 → 0.29.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 +14 -41
- package/.readyup/manifest.json +3 -3
- package/README.md +21 -9
- package/agents/guidance/rulebooks/nmr.md +1 -1
- package/bin/ensure-prepublish-hooks.js +1 -1
- package/bin/nmr-clean.js +1 -1
- package/bin/nmr-compile.js +1 -1
- package/bin/nmr-fmt.js +1 -1
- package/bin/nmr-report-overrides.js +1 -1
- package/bin/nmr-taze.js +1 -1
- package/bin/nmr.js +1 -1
- package/dist/esm/check-cache.d.ts +1 -1
- package/dist/esm/check-cache.js +10 -9
- package/dist/esm/commands/build-output.d.ts +5 -0
- package/dist/esm/commands/build-output.js +8 -0
- package/dist/esm/commands/build.js +77 -25
- package/dist/esm/commands/clean.js +1 -1
- package/dist/esm/commands/ensure-prepublish-hooks.js +5 -5
- package/dist/esm/commands/fmt.js +1 -1
- package/dist/esm/config.js +7 -7
- package/dist/esm/default-scripts.js +1 -1
- package/dist/esm/help.js +1 -1
- package/dist/esm/helpers/createBoundedBuffer.d.ts +9 -0
- package/dist/esm/helpers/createBoundedBuffer.js +60 -0
- package/dist/esm/helpers/package-json.js +15 -13
- package/dist/esm/resolver.js +5 -2
- package/dist/esm/runCli.js +24 -16
- package/dist/esm/runner.d.ts +18 -1
- package/dist/esm/runner.js +97 -24
- package/dist/esm/vitest.js +14 -4
- package/dist/esm/workspace.js +1 -1
- package/package.json +3 -3
package/.readyup/kits/default.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @noformat — @generated. Do not edit. Compiled by rdy. */
|
|
2
2
|
/* eslint-disable */
|
|
3
|
-
export const __readyupVersion = "0.
|
|
3
|
+
export const __readyupVersion = "0.26.0";
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
// .readyup/kits/default.ts
|
|
@@ -61,7 +61,8 @@ var rootScripts = {
|
|
|
61
61
|
lint: "eslint --fix .",
|
|
62
62
|
"lint:check": "eslint .",
|
|
63
63
|
"lint:strict": "strict-lint",
|
|
64
|
-
|
|
64
|
+
// The audit costs seconds and `ci` costs minutes, so the cheap gate fails first.
|
|
65
|
+
prepush: ["audit", "ci"],
|
|
65
66
|
"report-overrides": "nmr-report-overrides",
|
|
66
67
|
"root:check": ["root:typecheck", "fmt:check", "root:lint:check", "root:test"],
|
|
67
68
|
"root:lint": "eslint --fix --ignore-pattern 'packages/**' .",
|
|
@@ -163,6 +164,7 @@ var default_default = defineRdyKit({
|
|
|
163
164
|
{
|
|
164
165
|
name: ".tool-versions does not list pnpm",
|
|
165
166
|
severity: "warn",
|
|
167
|
+
quiet: true,
|
|
166
168
|
check: toolVersionsHasNoPnpm,
|
|
167
169
|
fix: "Remove pnpm from .tool-versions \u2014 manage via packageManager field and corepack"
|
|
168
170
|
},
|
|
@@ -201,16 +203,10 @@ var default_default = defineRdyKit({
|
|
|
201
203
|
{
|
|
202
204
|
name: "root package.json has no nmr-provided scripts",
|
|
203
205
|
severity: "warn",
|
|
206
|
+
quiet: true,
|
|
204
207
|
check: noRedundantRootScripts,
|
|
205
208
|
fix: "Remove scripts from root package.json that nmr provides as built-in root scripts \u2014 invoke via nmr directly"
|
|
206
209
|
},
|
|
207
|
-
{
|
|
208
|
-
name: "root:lint:strict does not use echo fallback",
|
|
209
|
-
severity: "warn",
|
|
210
|
-
skip: () => !scriptExists("root:lint:strict") ? "no root:lint:strict script" : false,
|
|
211
|
-
check: () => !scriptMatches("root:lint:strict", /\becho\b/),
|
|
212
|
-
fix: "Replace the echo fallback in root:lint:strict \u2014 strict-lint now supports path arguments"
|
|
213
|
-
},
|
|
214
210
|
// -- Workspace build readiness -------------------------------------------
|
|
215
211
|
{
|
|
216
212
|
name: "all workspace packages can build",
|
|
@@ -222,6 +218,7 @@ var default_default = defineRdyKit({
|
|
|
222
218
|
{
|
|
223
219
|
name: "no retired Vitest config variants",
|
|
224
220
|
severity: "error",
|
|
221
|
+
quiet: true,
|
|
225
222
|
check: () => noRetiredVitestConfigs(),
|
|
226
223
|
fix: "Delete every vitest.standalone.config.* and vitest.integration.config.*. nmr's test scripts select Vitest projects instead of naming config files"
|
|
227
224
|
},
|
|
@@ -246,6 +243,7 @@ var default_default = defineRdyKit({
|
|
|
246
243
|
{
|
|
247
244
|
name: "no package re-exports the ancestor Vitest config",
|
|
248
245
|
severity: "recommend",
|
|
246
|
+
quiet: true,
|
|
249
247
|
check: () => noReExportOnlyVitestConfigs(),
|
|
250
248
|
fix: "Delete these files. Vitest resolves config by walking up from the run root, so a per-package re-export is redundant"
|
|
251
249
|
},
|
|
@@ -263,23 +261,18 @@ var default_default = defineRdyKit({
|
|
|
263
261
|
check: () => hasDevDependency("v11y-check"),
|
|
264
262
|
fix: "pnpm add --save-dev v11y-check"
|
|
265
263
|
},
|
|
266
|
-
{
|
|
267
|
-
name: "code-quality workflow does not use nmr prepush",
|
|
268
|
-
severity: "warn",
|
|
269
|
-
skip: () => !fileExists(".github/workflows/code-quality.yaml") ? "no code-quality workflow" : false,
|
|
270
|
-
check: codeQualityWorkflowDoesNotUseNmrPrepush,
|
|
271
|
-
fix: 'Change the check-command in .github/workflows/code-quality.yaml from "pnpm exec nmr prepush" to "pnpm exec nmr ci", which runs the same checks without the audit that audit.yaml already runs'
|
|
272
|
-
},
|
|
273
264
|
// -- Legacy script runner ------------------------------------------------
|
|
274
265
|
{
|
|
275
266
|
name: "scripts/run-workspace-script.ts does not exist",
|
|
276
267
|
severity: "error",
|
|
268
|
+
quiet: true,
|
|
277
269
|
check: () => !fileExists("scripts/run-workspace-script.ts"),
|
|
278
270
|
fix: "Delete scripts/run-workspace-script.ts \u2014 nmr replaces this custom script runner"
|
|
279
271
|
},
|
|
280
272
|
{
|
|
281
273
|
name: 'no workspace packages reference run-workspace-script or "pnpm run ws"',
|
|
282
274
|
severity: "error",
|
|
275
|
+
quiet: true,
|
|
283
276
|
check: noWorkspaceRunScriptReferences,
|
|
284
277
|
fix: 'Remove "ws" script entries and replace any "pnpm run ws" invocations with nmr in each packages/*/package.json'
|
|
285
278
|
}
|
|
@@ -365,16 +358,11 @@ function hasPrettierConfigKey(cwd) {
|
|
|
365
358
|
if (manifest === void 0) return false;
|
|
366
359
|
try {
|
|
367
360
|
const parsed = JSON.parse(manifest);
|
|
368
|
-
return isRecord(parsed) && parsed
|
|
361
|
+
return isRecord(parsed) && parsed["prettier"] !== void 0;
|
|
369
362
|
} catch {
|
|
370
363
|
return false;
|
|
371
364
|
}
|
|
372
365
|
}
|
|
373
|
-
function codeQualityWorkflowDoesNotUseNmrPrepush() {
|
|
374
|
-
const content = readFile(".github/workflows/code-quality.yaml");
|
|
375
|
-
if (content === void 0) return true;
|
|
376
|
-
return !/check-command:\s*pnpm exec nmr prepush(\s|$)/.test(content);
|
|
377
|
-
}
|
|
378
366
|
function everyTestFileNamesItsTier(cwd = process.cwd()) {
|
|
379
367
|
const untiered = findTestFiles(cwd).filter((path2) => !hasTierInfix(path2));
|
|
380
368
|
if (untiered.length === 0) return true;
|
|
@@ -388,11 +376,11 @@ function formatPaths(paths) {
|
|
|
388
376
|
${paths.map((path2) => ` ${path2}`).join("\n")}`;
|
|
389
377
|
}
|
|
390
378
|
function getMinVersion() {
|
|
391
|
-
const picked = { "version": "0.
|
|
392
|
-
if (typeof picked
|
|
379
|
+
const picked = { "version": "0.29.0" };
|
|
380
|
+
if (typeof picked["version"] !== "string") {
|
|
393
381
|
throw new TypeError("nmr/package.json: 'version' must be a string");
|
|
394
382
|
}
|
|
395
|
-
return picked
|
|
383
|
+
return picked["version"];
|
|
396
384
|
}
|
|
397
385
|
function hasSupportedEslintVersion() {
|
|
398
386
|
return hasMinDevDependencyVersion("eslint", MIN_ESLINT_VERSION, {
|
|
@@ -423,7 +411,7 @@ function noReExportOnlyVitestConfigs(cwd = process.cwd()) {
|
|
|
423
411
|
function noRedundantRootScripts() {
|
|
424
412
|
const pkg = readPackageJson();
|
|
425
413
|
if (!pkg) return true;
|
|
426
|
-
const scripts = pkg
|
|
414
|
+
const scripts = pkg["scripts"];
|
|
427
415
|
if (!isRecord(scripts)) return true;
|
|
428
416
|
const builtInNames = Object.keys(getDefaultRootScripts());
|
|
429
417
|
const redundant = Object.keys(scripts).filter((name) => builtInNames.includes(name));
|
|
@@ -468,20 +456,6 @@ function readFileIn(cwd, relativePath) {
|
|
|
468
456
|
function resolvesVersionViaWorkspace(range) {
|
|
469
457
|
return WORKSPACE_VERSION_MARKERS.some((marker) => range.startsWith(marker));
|
|
470
458
|
}
|
|
471
|
-
function scriptExists(name) {
|
|
472
|
-
const pkg = readPackageJson();
|
|
473
|
-
if (!pkg) return false;
|
|
474
|
-
const scripts = pkg.scripts;
|
|
475
|
-
return isRecord(scripts) && Object.hasOwn(scripts, name);
|
|
476
|
-
}
|
|
477
|
-
function scriptMatches(name, pattern) {
|
|
478
|
-
const pkg = readPackageJson();
|
|
479
|
-
if (!pkg) return false;
|
|
480
|
-
const scripts = pkg.scripts;
|
|
481
|
-
if (!isRecord(scripts)) return false;
|
|
482
|
-
const value = scripts[name];
|
|
483
|
-
return typeof value === "string" && pattern.test(value);
|
|
484
|
-
}
|
|
485
459
|
function toolVersionsHasNoPnpm() {
|
|
486
460
|
const content = readFile(".tool-versions");
|
|
487
461
|
if (content === void 0) return true;
|
|
@@ -494,7 +468,6 @@ function vitestRootConfigBuildsOnSharedConfig(cwd = process.cwd()) {
|
|
|
494
468
|
return checkRootVitestConfig("vitest.root.config", "defineRootVitestConfig", cwd);
|
|
495
469
|
}
|
|
496
470
|
export {
|
|
497
|
-
codeQualityWorkflowDoesNotUseNmrPrepush,
|
|
498
471
|
default_default as default,
|
|
499
472
|
everyTestFileNamesItsTier,
|
|
500
473
|
hasSupportedEslintVersion,
|
package/.readyup/manifest.json
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
],
|
|
8
8
|
"name": "default",
|
|
9
9
|
"path": "kits/default.js",
|
|
10
|
-
"readyupVersion": "0.
|
|
10
|
+
"readyupVersion": "0.26.0",
|
|
11
11
|
"source": "kits/default.ts",
|
|
12
|
-
"sourceHash": "
|
|
13
|
-
"targetHash": "
|
|
12
|
+
"sourceHash": "7b2a223f",
|
|
13
|
+
"targetHash": "26c3d9eb"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|
package/README.md
CHANGED
|
@@ -3,15 +3,23 @@
|
|
|
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.29.0 (2026-08-09)
|
|
7
7
|
|
|
8
8
|
### 🎉 Features
|
|
9
9
|
|
|
10
|
-
-
|
|
10
|
+
- Run the dependency audit before ci in prepush (#634)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
`nmr prepush` now runs the dependency audit before the code-quality gate, so a vulnerability surfaces within seconds instead of after a full build-and-check pass. A failing audit now stops the run before the code-quality gate.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
### 🐛 Bug fixes
|
|
15
|
+
|
|
16
|
+
- Record no pass when build output changes while a check runs (#641)
|
|
17
|
+
|
|
18
|
+
Fixes an issue where rebuilding a package in one terminal while `nmr prepush` ran in another could mark a check as passed against build output it never saw, causing a later run on the same tree to skip it. Because `nmr ci` builds during its own run, it now needs one further run before it can skip, and that run repeats only the build phase.
|
|
19
|
+
|
|
20
|
+
- Stream command output so a large writer is no longer killed (#642)
|
|
21
|
+
|
|
22
|
+
Fixes an issue where a command that wrote more than a mebibyte of output under `nmr -q` was cut off and reported as failed while the tool itself kept running. When `nmr`'s output is piped or redirected to a file, a command's output now appears as it is produced instead of arriving all at once when the command exits. Piping `nmr` into a reader that stops early, such as `head`, now ends the run cleanly. A command killed by a signal now reports 128 plus the signal number rather than a bare 1.
|
|
15
23
|
<!-- /section:release-notes -->
|
|
16
24
|
|
|
17
25
|
## Installation
|
|
@@ -263,7 +271,7 @@ A hit requires all of these to match the run that recorded the pass:
|
|
|
263
271
|
|
|
264
272
|
A hit additionally requires the build output of every package nmr's own build covers to be present, and to have been built from this tree. Output is git-ignored, so neither its removal nor its replacement moves the hash: restoring a tree with `git stash` or `git checkout` restores none of the output that tree was built with. nmr compares each covered package's recorded build digest against the one on disk, so a `dist` compiled from another tree is a miss the following run repairs. A package that overrides `build` or `compile` emits on terms nmr does not know and is exempt.
|
|
265
273
|
|
|
266
|
-
A pass is recorded only when the command exits 0, only when the tree still matches the one the run started against, only when every covered package's output is present, and never for a run that executed nothing (a `""`/`":"` skip override, or an `NMR_RUN_IF_PRESENT` miss).
|
|
274
|
+
A pass is recorded only when the command exits 0, only when the tree still matches the one the run started against, only when every covered package's output is present and unchanged since the run started, and never for a run that executed nothing (a `""`/`":"` skip override, or an `NMR_RUN_IF_PRESENT` miss). Output that moves mid-run leaves no answer to which output the pass was earned over, whether another process rebuilt it or the chain built it itself: `nmr ci` is the one default command whose chain does, and it records nothing on a run whose build did work. The `nmr check:strict` inside that chain still records, because its own window opens after the build.
|
|
267
275
|
|
|
268
276
|
### Out of contract
|
|
269
277
|
|
|
@@ -424,9 +432,11 @@ A run that collects no test files passes. That is what lets `nmr test:tool` fan
|
|
|
424
432
|
| `build` | `pnpm --recursive exec nmr build` |
|
|
425
433
|
| `ci` | `build`, `check:strict` |
|
|
426
434
|
| `clean` | `nmr-clean` |
|
|
427
|
-
| `prepush` | `
|
|
435
|
+
| `prepush` | `audit`, `ci` |
|
|
436
|
+
|
|
437
|
+
`ci` is what a code-quality workflow runs; it leaves out the audit, which belongs in a workflow of its own. `prepush` is what a developer runs before pushing: both gates, audit first. The audit takes seconds and `ci` takes minutes, so a vulnerability surfaces before the long gate runs. It composes `ci` rather than restating its stages, so a stage added to `ci` joins the pre-push run too.
|
|
428
438
|
|
|
429
|
-
|
|
439
|
+
The audit reaches the network. With no network, `ci` is the gate that still runs.
|
|
430
440
|
|
|
431
441
|
Neither is bound to a git hook. `prepush` is named for when you run it, not for a hook nmr installs.
|
|
432
442
|
|
|
@@ -562,7 +572,7 @@ nmr build # Compile to .js and .d.ts in one pass
|
|
|
562
572
|
# From the monorepo root
|
|
563
573
|
nmr test # Root tests + recursive workspace tests
|
|
564
574
|
nmr ci # build + check:strict
|
|
565
|
-
nmr prepush #
|
|
575
|
+
nmr prepush # audit + ci
|
|
566
576
|
|
|
567
577
|
# Target specific packages
|
|
568
578
|
nmr --filter core test # Test only the core package
|
|
@@ -628,7 +638,9 @@ Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler
|
|
|
628
638
|
|
|
629
639
|
**What it compiles.** Entry points are `src/**/*.ts` less the directories that hold test scaffolding rather than shipped code: `__fixtures__/`, `__mocks__/`, `__tests__/`, and `test-utils/`. Add to that list per package with [`build.extraIgnorePatterns`](#package-level-configuration). Ignoring a directory drops it as an _entry point_, not from the emit: the compiler still emits whatever the surviving entry points import, so a helper that production code uses is still compiled and its importer never emits a dangling specifier. This list is deliberately not the Vitest [coverage exclusions](#what-the-config-excludes) — helpers live in `test-utils/` precisely so they stay covered.
|
|
630
640
|
|
|
631
|
-
**The build owns its output directory.** Every rebuild
|
|
641
|
+
**The build owns its output directory.** Every rebuild replaces `dist/esm` wholesale, so the directory holds exactly the current emit: a source that was deleted, or that a widened ignore set now covers, leaves nothing behind. Assets therefore belong outside the output directory, or in a `build:post` hook, which runs after the output is published and so composes correctly. Note that a bare `nmr compile` runs no hook, so assets a `build:post` step copies in are absent until a full `nmr build` restores them. An `outdir` that does not resolve inside the package is rejected rather than replaced.
|
|
642
|
+
|
|
643
|
+
**The replacement is atomic.** The emit is buffered in memory, written to a staging directory beside the output directory, and swapped into place by rename. A build that fails for any reason -- a malformed tsconfig, an unresolvable alias, a full disk -- therefore leaves the previous output exactly as it was. `dist/esm` is never observed mid-write: it holds the previous build or the new one, and is absent only between the two renames. This matters most when the package being rebuilt supplies a binary that other packages in the same recursive build invoke.
|
|
632
644
|
|
|
633
645
|
`typescript` is a peer dependency (`>=5.7.0 <7`); the consuming repo provides it. The floor is what `rewriteRelativeImportExtensions` requires; the ceiling is because TypeScript 7 ships no compiler API — its root export is a version constant, so `nmr-compile` cannot run on it. Relative imports in source must carry explicit `.ts` extensions for them to be rewritten.
|
|
634
646
|
|
|
@@ -9,6 +9,6 @@ delivery: ambient
|
|
|
9
9
|
- 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.
|
|
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
|
-
- `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
|
|
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 the audit first, then `ci`, so a vulnerability stops the run before the long gate. Neither installs or binds a git hook.
|
|
13
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.
|
|
@@ -3,7 +3,7 @@ try {
|
|
|
3
3
|
await import('../dist/esm/cli-ensure-prepublish-hooks.js');
|
|
4
4
|
} catch (error) {
|
|
5
5
|
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
-
process.stderr.write('ensure-prepublish-hooks: build output not found — run `pnpm run
|
|
6
|
+
process.stderr.write('ensure-prepublish-hooks: build output not found — run `pnpm run bootstrap` first\n');
|
|
7
7
|
} else {
|
|
8
8
|
process.stderr.write(`ensure-prepublish-hooks: failed to load: ${error.message}\n`);
|
|
9
9
|
}
|
package/bin/nmr-clean.js
CHANGED
|
@@ -3,7 +3,7 @@ try {
|
|
|
3
3
|
await import('../dist/esm/cli-clean.js');
|
|
4
4
|
} catch (error) {
|
|
5
5
|
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
-
process.stderr.write('nmr-clean: build output not found — run `pnpm run
|
|
6
|
+
process.stderr.write('nmr-clean: build output not found — run `pnpm run bootstrap` first\n');
|
|
7
7
|
} else {
|
|
8
8
|
process.stderr.write(`nmr-clean: failed to load: ${error.message}\n`);
|
|
9
9
|
}
|
package/bin/nmr-compile.js
CHANGED
|
@@ -3,7 +3,7 @@ try {
|
|
|
3
3
|
await import('../dist/esm/cli-build.js');
|
|
4
4
|
} catch (error) {
|
|
5
5
|
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
-
process.stderr.write('nmr-compile: build output not found — run `pnpm run
|
|
6
|
+
process.stderr.write('nmr-compile: build output not found — run `pnpm run bootstrap` first\n');
|
|
7
7
|
} else {
|
|
8
8
|
process.stderr.write(`nmr-compile: failed to load: ${error.message}\n`);
|
|
9
9
|
}
|
package/bin/nmr-fmt.js
CHANGED
|
@@ -3,7 +3,7 @@ try {
|
|
|
3
3
|
await import('../dist/esm/cli-fmt.js');
|
|
4
4
|
} catch (error) {
|
|
5
5
|
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
-
process.stderr.write('nmr-fmt: build output not found — run `pnpm run
|
|
6
|
+
process.stderr.write('nmr-fmt: build output not found — run `pnpm run bootstrap` first\n');
|
|
7
7
|
} else {
|
|
8
8
|
process.stderr.write(`nmr-fmt: failed to load: ${error.message}\n`);
|
|
9
9
|
}
|
|
@@ -3,7 +3,7 @@ try {
|
|
|
3
3
|
await import('../dist/esm/cli-report-overrides.js');
|
|
4
4
|
} catch (error) {
|
|
5
5
|
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
-
process.stderr.write('nmr-report-overrides: build output not found — run `pnpm run
|
|
6
|
+
process.stderr.write('nmr-report-overrides: build output not found — run `pnpm run bootstrap` first\n');
|
|
7
7
|
} else {
|
|
8
8
|
process.stderr.write(`nmr-report-overrides: failed to load: ${error.message}\n`);
|
|
9
9
|
}
|
package/bin/nmr-taze.js
CHANGED
|
@@ -3,7 +3,7 @@ try {
|
|
|
3
3
|
await import('../dist/esm/cli-taze.js');
|
|
4
4
|
} catch (error) {
|
|
5
5
|
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
-
process.stderr.write('nmr-taze: build output not found — run `pnpm run
|
|
6
|
+
process.stderr.write('nmr-taze: build output not found — run `pnpm run bootstrap` first\n');
|
|
7
7
|
} else {
|
|
8
8
|
process.stderr.write(`nmr-taze: failed to load: ${error.message}\n`);
|
|
9
9
|
}
|
package/bin/nmr.js
CHANGED
|
@@ -3,7 +3,7 @@ try {
|
|
|
3
3
|
await import('../dist/esm/cli.js');
|
|
4
4
|
} catch (error) {
|
|
5
5
|
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
-
process.stderr.write('nmr: build output not found — run `pnpm run
|
|
6
|
+
process.stderr.write('nmr: build output not found — run `pnpm run bootstrap` first\n');
|
|
7
7
|
} else {
|
|
8
8
|
process.stderr.write(`nmr: failed to load: ${error.message}\n`);
|
|
9
9
|
}
|
|
@@ -46,7 +46,7 @@ export declare function computeCacheKey(options: {
|
|
|
46
46
|
reason: string;
|
|
47
47
|
};
|
|
48
48
|
export declare function encodeTreeSnapshot(snapshot: TreeSnapshot): string;
|
|
49
|
-
export declare function findStaleBuildOutput(
|
|
49
|
+
export declare function findStaleBuildOutput(earlier: Record<string, string>, later: Record<string, string>): string | undefined;
|
|
50
50
|
export declare function formatMisplacedNoCacheWarning(command: string): string;
|
|
51
51
|
export declare function formatSkipLine(command: string, entry: CheckCacheEntry, now: number): string;
|
|
52
52
|
export declare function readBuildOutputState(monorepoRoot: string, config: NmrConfig): Promise<BuildOutputState>;
|
package/dist/esm/check-cache.js
CHANGED
|
@@ -81,9 +81,9 @@ export function computeCacheKey(options) {
|
|
|
81
81
|
export function encodeTreeSnapshot(snapshot) {
|
|
82
82
|
return `${snapshot.hash} ${snapshot.headSha}`;
|
|
83
83
|
}
|
|
84
|
-
export function findStaleBuildOutput(
|
|
85
|
-
const names = [...new Set([...Object.keys(
|
|
86
|
-
return names.find((name) =>
|
|
84
|
+
export function findStaleBuildOutput(earlier, later) {
|
|
85
|
+
const names = [...new Set([...Object.keys(earlier), ...Object.keys(later)])].toSorted();
|
|
86
|
+
return names.find((name) => earlier[name] !== later[name]);
|
|
87
87
|
}
|
|
88
88
|
export function formatMisplacedNoCacheWarning(command) {
|
|
89
89
|
return (`⚠️ --no-cache after the command name is passed to \`${command}\`, not read by nmr. ` +
|
|
@@ -105,7 +105,7 @@ export async function readBuildOutputState(monorepoRoot, config) {
|
|
|
105
105
|
return state;
|
|
106
106
|
}
|
|
107
107
|
const registry = buildWorkspaceRegistry(config);
|
|
108
|
-
if (JSON.stringify(registry
|
|
108
|
+
if (JSON.stringify(registry['build']) !== JSON.stringify(getDefaultWorkspaceScripts()['build'])) {
|
|
109
109
|
return state;
|
|
110
110
|
}
|
|
111
111
|
for (const packageDir of packageDirs) {
|
|
@@ -132,7 +132,8 @@ export async function removeCheckCache(scopeDir) {
|
|
|
132
132
|
}
|
|
133
133
|
export function resolveCacheableCommands(checkCache) {
|
|
134
134
|
const commands = new Set([...DEFAULT_CACHEABLE_COMMANDS, ...(checkCache?.extraCommands ?? [])]);
|
|
135
|
-
|
|
135
|
+
const excludedCommands = checkCache?.excludeCommands ?? [];
|
|
136
|
+
for (const excluded of excludedCommands) {
|
|
136
137
|
commands.delete(excluded);
|
|
137
138
|
}
|
|
138
139
|
return commands;
|
|
@@ -184,10 +185,10 @@ function isCheckCacheEntry(value) {
|
|
|
184
185
|
if (stringFields.some((field) => typeof value[field] !== 'string')) {
|
|
185
186
|
return false;
|
|
186
187
|
}
|
|
187
|
-
return (typeof value
|
|
188
|
-
Number.isFinite(value
|
|
189
|
-
!Number.isNaN(Date.parse(String(value
|
|
190
|
-
isStringRecord(value
|
|
188
|
+
return (typeof value['durationMs'] === 'number' &&
|
|
189
|
+
Number.isFinite(value['durationMs']) &&
|
|
190
|
+
!Number.isNaN(Date.parse(String(value['recordedAt']))) &&
|
|
191
|
+
isStringRecord(value['buildDigests']));
|
|
191
192
|
}
|
|
192
193
|
function isProbeSubject(packageDir, registry) {
|
|
193
194
|
const build = resolveScript('build', registry, packageDir, false);
|
|
@@ -4,6 +4,10 @@ export interface BuildOptions {
|
|
|
4
4
|
ignorePatterns?: string[];
|
|
5
5
|
outdir?: string;
|
|
6
6
|
}
|
|
7
|
+
export interface ScratchDirs {
|
|
8
|
+
previous: string;
|
|
9
|
+
staging: string;
|
|
10
|
+
}
|
|
7
11
|
export declare const DEFAULT_ENTRY_GLOBS: string[];
|
|
8
12
|
export declare const DEFAULT_IGNORE_PATTERNS: string[];
|
|
9
13
|
export declare const DEFAULT_OUTDIR = "dist/esm/";
|
|
@@ -11,3 +15,4 @@ export declare function hasBuildOutput(packageDir: string, options?: BuildOption
|
|
|
11
15
|
export declare function hasExpectedBuildOutput(packageDir: string, outdir: string, entryPoints: string[]): boolean;
|
|
12
16
|
export declare function readBuildDigest(packageDir: string): Promise<string | undefined>;
|
|
13
17
|
export declare function resolveBuildCachePath(packageDir: string): string;
|
|
18
|
+
export declare function resolveScratchDirs(emitDir: string): ScratchDirs;
|
|
@@ -34,3 +34,11 @@ export function resolveBuildCachePath(packageDir) {
|
|
|
34
34
|
extension: '.hash',
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
+
export function resolveScratchDirs(emitDir) {
|
|
38
|
+
const parent = path.dirname(emitDir);
|
|
39
|
+
const name = path.basename(emitDir);
|
|
40
|
+
return {
|
|
41
|
+
previous: path.join(parent, `.${name}.previous`),
|
|
42
|
+
staging: path.join(parent, `.${name}.staging`),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync
|
|
3
|
-
import { readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readFile, rename, rm } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { readCacheEntry, writeCacheEntry } from '@williamthorsen/nmr-core';
|
|
6
6
|
import { glob } from 'glob';
|
|
7
7
|
import * as ts from 'typescript';
|
|
8
8
|
import { resolveConfigPath } from "../config.js";
|
|
9
|
-
import { DEFAULT_ENTRY_GLOBS, DEFAULT_IGNORE_PATTERNS, DEFAULT_OUTDIR, hasExpectedBuildOutput, resolveBuildCachePath, } from "./build-output.js";
|
|
9
|
+
import { DEFAULT_ENTRY_GLOBS, DEFAULT_IGNORE_PATTERNS, DEFAULT_OUTDIR, hasExpectedBuildOutput, resolveBuildCachePath, resolveScratchDirs, } from "./build-output.js";
|
|
10
10
|
const PACKAGE_ICON = '📦';
|
|
11
11
|
const SKIPPED_ICON = '⏭️';
|
|
12
12
|
const SOURCE_ROOT = 'src';
|
|
@@ -18,7 +18,7 @@ export async function buildPackage(packageDir, options = {}) {
|
|
|
18
18
|
assertSupportedTypeScript();
|
|
19
19
|
const cachePath = resolveBuildCachePath(packageDir);
|
|
20
20
|
const outdir = options.outdir ?? DEFAULT_OUTDIR;
|
|
21
|
-
resolveEmitDir(packageDir, outdir);
|
|
21
|
+
const emitDir = resolveEmitDir(packageDir, outdir);
|
|
22
22
|
const emitConfig = { outdir, declaration: true, rewriteRelativeImportExtensions: true };
|
|
23
23
|
const entryPoints = await glob(options.entryGlobs ?? DEFAULT_ENTRY_GLOBS, {
|
|
24
24
|
cwd: packageDir,
|
|
@@ -32,6 +32,7 @@ export async function buildPackage(packageDir, options = {}) {
|
|
|
32
32
|
];
|
|
33
33
|
const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, ts.version, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints));
|
|
34
34
|
if (!changed) {
|
|
35
|
+
await discardScratchDirs(resolveScratchDirs(emitDir));
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
await emitPackage(packageDir, entryPoints, outdir);
|
|
@@ -64,10 +65,7 @@ export function resolveTsconfigChain(packageDir, configFileName = 'tsconfig.json
|
|
|
64
65
|
throw new Error(`nmr-compile: failed to read ${normalized}.\n${formatDiagnostics([configFile.error])}`);
|
|
65
66
|
}
|
|
66
67
|
for (const entry of normalizeExtendsField(configFile.config)) {
|
|
67
|
-
|
|
68
|
-
if (baseConfig !== undefined) {
|
|
69
|
-
walk(baseConfig);
|
|
70
|
-
}
|
|
68
|
+
walk(resolveExtendsTarget(entry, normalized));
|
|
71
69
|
}
|
|
72
70
|
}
|
|
73
71
|
walk(path.resolve(packageDir, configFileName));
|
|
@@ -77,29 +75,64 @@ async function emitPackage(packageDir, entryPoints, outdir) {
|
|
|
77
75
|
const compilerOptions = synthesizeCompilerOptions(packageDir, outdir);
|
|
78
76
|
const rootNames = entryPoints.map((entry) => path.resolve(packageDir, entry));
|
|
79
77
|
const sourceRoot = path.resolve(packageDir, SOURCE_ROOT);
|
|
78
|
+
const emitDir = resolveEmitDir(packageDir, outdir);
|
|
79
|
+
const scratchDirs = resolveScratchDirs(emitDir);
|
|
80
|
+
await removeScratchDirs(scratchDirs);
|
|
80
81
|
const program = ts.createProgram(rootNames, compilerOptions);
|
|
81
|
-
|
|
82
|
-
const emittedFiles = [];
|
|
82
|
+
const emitted = new Map();
|
|
83
83
|
const emitResult = program.emit(undefined, (fileName, text, writeByteOrderMark) => {
|
|
84
|
-
|
|
85
|
-
emittedFiles.push(fileName);
|
|
84
|
+
emitted.set(fileName, { text, writeByteOrderMark });
|
|
86
85
|
});
|
|
87
86
|
if (emitResult.emitSkipped) {
|
|
88
87
|
throw new Error(`nmr-compile: emit failed.\n${formatDiagnostics(emitResult.diagnostics)}`);
|
|
89
88
|
}
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
const staged = new Map();
|
|
90
|
+
for (const [fileName, file] of emitted) {
|
|
91
|
+
staged.set(fileName, { ...file, text: rewriteSpecifiers(fileName, file.text, compilerOptions, sourceRoot) });
|
|
92
|
+
}
|
|
93
|
+
if (staged.size === 0) {
|
|
94
|
+
await rm(emitDir, { force: true, recursive: true });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
writeStagedOutput(staged, emitDir, scratchDirs.staging);
|
|
98
|
+
await swapIntoPlace(emitDir, scratchDirs);
|
|
99
|
+
}
|
|
100
|
+
async function discardScratchDirs(scratchDirs) {
|
|
101
|
+
try {
|
|
102
|
+
await removeScratchDirs(scratchDirs);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
92
105
|
}
|
|
93
106
|
}
|
|
107
|
+
async function removeScratchDirs(scratchDirs) {
|
|
108
|
+
await rm(scratchDirs.previous, { force: true, recursive: true });
|
|
109
|
+
await rm(scratchDirs.staging, { force: true, recursive: true });
|
|
110
|
+
}
|
|
94
111
|
function resolveEmitDir(packageDir, outdir) {
|
|
95
112
|
const resolved = path.resolve(packageDir, outdir);
|
|
96
113
|
const relative = path.relative(packageDir, resolved);
|
|
97
114
|
if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
98
115
|
throw new Error(`nmr-compile: refusing to build into '${outdir}', which does not resolve inside the package. ` +
|
|
99
|
-
'The build
|
|
116
|
+
'The build replaces its output directory on each emit, so the directory must sit below the package root.');
|
|
100
117
|
}
|
|
101
118
|
return resolved;
|
|
102
119
|
}
|
|
120
|
+
async function swapIntoPlace(emitDir, scratchDirs) {
|
|
121
|
+
const hadPreviousOutput = existsSync(emitDir);
|
|
122
|
+
if (hadPreviousOutput) {
|
|
123
|
+
await rename(emitDir, scratchDirs.previous);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await rename(scratchDirs.staging, emitDir);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (hadPreviousOutput && !existsSync(emitDir)) {
|
|
130
|
+
await rename(scratchDirs.previous, emitDir);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
await discardScratchDirs(scratchDirs);
|
|
135
|
+
}
|
|
103
136
|
function synthesizeCompilerOptions(packageDir, outdir) {
|
|
104
137
|
const configPath = path.join(packageDir, 'tsconfig.json');
|
|
105
138
|
const configFile = ts.readConfigFile(configPath, (fileName) => ts.sys.readFile(fileName));
|
|
@@ -125,12 +158,20 @@ function synthesizeCompilerOptions(packageDir, outdir) {
|
|
|
125
158
|
noEmitOnError: false,
|
|
126
159
|
};
|
|
127
160
|
}
|
|
128
|
-
function
|
|
161
|
+
function writeStagedOutput(staged, emitDir, stagingDir) {
|
|
162
|
+
for (const [fileName, file] of staged) {
|
|
163
|
+
if (!isWithin(emitDir, fileName)) {
|
|
164
|
+
throw new Error(`nmr-compile: the compiler emitted ${fileName}, which is outside the output directory ${emitDir}. ` +
|
|
165
|
+
"Verify the resolved tsconfig's 'outDir' and 'declarationDir'.");
|
|
166
|
+
}
|
|
167
|
+
ts.sys.writeFile(path.join(stagingDir, path.relative(emitDir, fileName)), file.text, file.writeByteOrderMark);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function rewriteSpecifiers(outputFile, text, compilerOptions, sourceRoot) {
|
|
129
171
|
if (!isRewritableOutput(outputFile)) {
|
|
130
|
-
return;
|
|
172
|
+
return text;
|
|
131
173
|
}
|
|
132
|
-
const
|
|
133
|
-
const sourceFile = ts.createSourceFile(outputFile, originalText, ts.ScriptTarget.Latest, false, scriptKindFor(outputFile));
|
|
174
|
+
const sourceFile = ts.createSourceFile(outputFile, text, ts.ScriptTarget.Latest, false, scriptKindFor(outputFile));
|
|
134
175
|
const aliasPrefixes = collectAliasPrefixes(compilerOptions);
|
|
135
176
|
const sourceContainingFile = mapOutputToSource(outputFile, compilerOptions, sourceRoot);
|
|
136
177
|
const edits = [];
|
|
@@ -140,18 +181,18 @@ function rewriteOutputSpecifiers(outputFile, compilerOptions, sourceRoot) {
|
|
|
140
181
|
return;
|
|
141
182
|
}
|
|
142
183
|
const start = literal.getStart(sourceFile);
|
|
143
|
-
const quote =
|
|
184
|
+
const quote = text[start] ?? '"';
|
|
144
185
|
edits.push({ start, end: literal.getEnd(), text: `${quote}${replacement}${quote}` });
|
|
145
186
|
});
|
|
146
187
|
if (edits.length === 0) {
|
|
147
|
-
return;
|
|
188
|
+
return text;
|
|
148
189
|
}
|
|
149
190
|
const orderedEdits = [...edits].sort((a, b) => b.start - a.start);
|
|
150
|
-
let updatedText =
|
|
191
|
+
let updatedText = text;
|
|
151
192
|
for (const edit of orderedEdits) {
|
|
152
193
|
updatedText = updatedText.slice(0, edit.start) + edit.text + updatedText.slice(edit.end);
|
|
153
194
|
}
|
|
154
|
-
|
|
195
|
+
return updatedText;
|
|
155
196
|
}
|
|
156
197
|
function resolveSpecifierReplacement(specifier, sourceContainingFile, compilerOptions, sourceRoot, aliasPrefixes) {
|
|
157
198
|
if (isRelativeSpecifier(specifier)) {
|
|
@@ -267,9 +308,20 @@ function resolveExtendsTarget(extendsEntry, fromConfigPath) {
|
|
|
267
308
|
return base;
|
|
268
309
|
}
|
|
269
310
|
const withJsonExtension = `${base}.json`;
|
|
270
|
-
|
|
311
|
+
if (ts.sys.fileExists(withJsonExtension)) {
|
|
312
|
+
return withJsonExtension;
|
|
313
|
+
}
|
|
314
|
+
throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not exist.`);
|
|
315
|
+
}
|
|
316
|
+
const resolved = resolvePackageSpecifier(extendsEntry, fromConfigPath) ??
|
|
317
|
+
resolvePackageSpecifier(`${extendsEntry}/tsconfig.json`, fromConfigPath);
|
|
318
|
+
if (resolved === undefined) {
|
|
319
|
+
throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not resolve to a file.`);
|
|
271
320
|
}
|
|
272
|
-
|
|
321
|
+
return resolved;
|
|
322
|
+
}
|
|
323
|
+
function resolvePackageSpecifier(specifier, fromConfigPath) {
|
|
324
|
+
const { resolvedModule } = ts.resolveModuleName(specifier, fromConfigPath, { moduleResolution: ts.ModuleResolutionKind.NodeNext, resolveJsonModule: true }, ts.sys);
|
|
273
325
|
return resolvedModule?.resolvedFileName;
|
|
274
326
|
}
|
|
275
327
|
function isRelativeSpecifier(specifier) {
|
|
@@ -52,7 +52,7 @@ async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
|
|
|
52
52
|
continue;
|
|
53
53
|
}
|
|
54
54
|
const command = applyDevBin(resolved.command, config.devBin, monorepoRoot);
|
|
55
|
-
const exitCode = runCommand(command, packageDir);
|
|
55
|
+
const { exitCode } = await runCommand(command, packageDir);
|
|
56
56
|
if (exitCode !== 0) {
|
|
57
57
|
throw new Error(`nmr-clean: \`${command}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
|
|
58
58
|
}
|
|
@@ -17,12 +17,12 @@ export function ensurePrepublishHooks(monorepoRoot, options) {
|
|
|
17
17
|
packageName,
|
|
18
18
|
packageDir,
|
|
19
19
|
isPrivate: true,
|
|
20
|
-
prepublishOnly: pkg.scripts?.prepublishOnly,
|
|
20
|
+
prepublishOnly: pkg.scripts?.['prepublishOnly'],
|
|
21
21
|
action: 'ok',
|
|
22
22
|
});
|
|
23
23
|
continue;
|
|
24
24
|
}
|
|
25
|
-
const existing = pkg.scripts?.prepublishOnly;
|
|
25
|
+
const existing = pkg.scripts?.['prepublishOnly'];
|
|
26
26
|
if (existing) {
|
|
27
27
|
packages.push({
|
|
28
28
|
packageName,
|
|
@@ -66,8 +66,8 @@ function addPrepublishOnly(packageDir, command) {
|
|
|
66
66
|
if (!isObject(parsed)) {
|
|
67
67
|
throw new TypeError(`Invalid package.json in ${packageDir}: expected an object`);
|
|
68
68
|
}
|
|
69
|
-
const scripts = isObject(parsed
|
|
70
|
-
scripts
|
|
71
|
-
parsed
|
|
69
|
+
const scripts = isObject(parsed['scripts']) ? parsed['scripts'] : {};
|
|
70
|
+
scripts['prepublishOnly'] = command;
|
|
71
|
+
parsed['scripts'] = scripts;
|
|
72
72
|
writeFileSync(filePath, JSON.stringify(parsed, null, 2) + '\n', 'utf8');
|
|
73
73
|
}
|
package/dist/esm/commands/fmt.js
CHANGED
|
@@ -108,7 +108,7 @@ function resolvePrettierCli() {
|
|
|
108
108
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
109
109
|
if (!isObject(manifest))
|
|
110
110
|
return missing;
|
|
111
|
-
const bin = isObject(manifest
|
|
111
|
+
const bin = isObject(manifest['bin']) ? manifest['bin'][PRETTIER_PACKAGE] : manifest['bin'];
|
|
112
112
|
if (typeof bin !== 'string')
|
|
113
113
|
return missing;
|
|
114
114
|
return { ok: true, cliPath: path.resolve(path.dirname(manifestPath), bin) };
|
package/dist/esm/config.js
CHANGED
|
@@ -42,7 +42,7 @@ function isScriptRecord(value) {
|
|
|
42
42
|
return true;
|
|
43
43
|
}
|
|
44
44
|
function validateScriptField(value, fieldName, configPath) {
|
|
45
|
-
if (!(fieldName
|
|
45
|
+
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
46
46
|
return undefined;
|
|
47
47
|
}
|
|
48
48
|
if (!isScriptRecord(value[fieldName])) {
|
|
@@ -54,7 +54,7 @@ function isStringArray(value) {
|
|
|
54
54
|
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
55
55
|
}
|
|
56
56
|
function validateBuildField(value, configPath) {
|
|
57
|
-
const build = value
|
|
57
|
+
const build = value['build'];
|
|
58
58
|
if (build === undefined) {
|
|
59
59
|
return undefined;
|
|
60
60
|
}
|
|
@@ -62,14 +62,14 @@ function validateBuildField(value, configPath) {
|
|
|
62
62
|
throw new Error(`Invalid nmr config at ${configPath}: \`build\` must be an object`);
|
|
63
63
|
}
|
|
64
64
|
assertRecognizedKeys(build, RECOGNIZED_BUILD_KEYS, configPath, 'build.');
|
|
65
|
-
const extraIgnorePatterns = build
|
|
65
|
+
const extraIgnorePatterns = build['extraIgnorePatterns'];
|
|
66
66
|
if (extraIgnorePatterns !== undefined && !isStringArray(extraIgnorePatterns)) {
|
|
67
67
|
throw new Error(`Invalid nmr config at ${configPath}: \`build.extraIgnorePatterns\` must be a string[]`);
|
|
68
68
|
}
|
|
69
69
|
return extraIgnorePatterns === undefined ? {} : { extraIgnorePatterns };
|
|
70
70
|
}
|
|
71
71
|
function validateCheckCacheField(value, configPath) {
|
|
72
|
-
const checkCache = value
|
|
72
|
+
const checkCache = value['checkCache'];
|
|
73
73
|
if (checkCache === undefined) {
|
|
74
74
|
return undefined;
|
|
75
75
|
}
|
|
@@ -78,7 +78,7 @@ function validateCheckCacheField(value, configPath) {
|
|
|
78
78
|
}
|
|
79
79
|
assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, 'checkCache.');
|
|
80
80
|
const config = {};
|
|
81
|
-
const enabled = checkCache
|
|
81
|
+
const enabled = checkCache['enabled'];
|
|
82
82
|
if (enabled !== undefined) {
|
|
83
83
|
if (typeof enabled !== 'boolean') {
|
|
84
84
|
throw new TypeError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
|
|
@@ -98,7 +98,7 @@ function validateCheckCacheField(value, configPath) {
|
|
|
98
98
|
return config;
|
|
99
99
|
}
|
|
100
100
|
function validateStringRecordField(value, fieldName, configPath) {
|
|
101
|
-
if (!(fieldName
|
|
101
|
+
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
102
102
|
return undefined;
|
|
103
103
|
}
|
|
104
104
|
if (!isStringRecord(value[fieldName])) {
|
|
@@ -135,7 +135,7 @@ export async function loadConfig(baseDir) {
|
|
|
135
135
|
return {};
|
|
136
136
|
}
|
|
137
137
|
const imported = await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href));
|
|
138
|
-
const loaded = isObject(imported) ? imported
|
|
138
|
+
const loaded = isObject(imported) ? imported['default'] : undefined;
|
|
139
139
|
return validateConfig(loaded, configPath);
|
|
140
140
|
}
|
|
141
141
|
export async function loadRootConfig(monorepoRoot) {
|
|
@@ -38,7 +38,7 @@ export const rootScripts = {
|
|
|
38
38
|
lint: 'eslint --fix .',
|
|
39
39
|
'lint:check': 'eslint .',
|
|
40
40
|
'lint:strict': 'strict-lint',
|
|
41
|
-
prepush: ['
|
|
41
|
+
prepush: ['audit', 'ci'],
|
|
42
42
|
'report-overrides': 'nmr-report-overrides',
|
|
43
43
|
'root:check': ['root:typecheck', 'fmt:check', 'root:lint:check', 'root:test'],
|
|
44
44
|
'root:lint': "eslint --fix --ignore-pattern 'packages/**' .",
|
package/dist/esm/help.js
CHANGED
|
@@ -50,7 +50,7 @@ function collectOverrides(packageDir) {
|
|
|
50
50
|
function applyOverrides(registry, overrides) {
|
|
51
51
|
const marked = new Set();
|
|
52
52
|
for (const [name, value] of Object.entries(overrides)) {
|
|
53
|
-
if (!(name
|
|
53
|
+
if (!Object.hasOwn(registry, name)) {
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
56
|
registry[name] = value;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface BoundedBufferOptions {
|
|
2
|
+
headLimitBytes?: number;
|
|
3
|
+
tailLimitBytes?: number;
|
|
4
|
+
}
|
|
5
|
+
export interface BoundedBuffer {
|
|
6
|
+
append: (chunk: Buffer) => void;
|
|
7
|
+
toBuffer: () => Buffer;
|
|
8
|
+
}
|
|
9
|
+
export declare function createBoundedBuffer(options?: BoundedBufferOptions): BoundedBuffer;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const DEFAULT_HEAD_LIMIT_BYTES = 1_048_576;
|
|
2
|
+
const DEFAULT_TAIL_LIMIT_BYTES = 1_048_576;
|
|
3
|
+
export function createBoundedBuffer(options = {}) {
|
|
4
|
+
const headLimitBytes = options.headLimitBytes ?? DEFAULT_HEAD_LIMIT_BYTES;
|
|
5
|
+
const tailLimitBytes = options.tailLimitBytes ?? DEFAULT_TAIL_LIMIT_BYTES;
|
|
6
|
+
const headChunks = [];
|
|
7
|
+
let headBytes = 0;
|
|
8
|
+
let tailRing;
|
|
9
|
+
let tailOffset = 0;
|
|
10
|
+
let tailBytes = 0;
|
|
11
|
+
let elidedBytes = 0;
|
|
12
|
+
function append(chunk) {
|
|
13
|
+
let remainder = chunk;
|
|
14
|
+
if (headBytes < headLimitBytes) {
|
|
15
|
+
const takenBytes = Math.min(headLimitBytes - headBytes, remainder.length);
|
|
16
|
+
headChunks.push(remainder.subarray(0, takenBytes));
|
|
17
|
+
headBytes += takenBytes;
|
|
18
|
+
remainder = remainder.subarray(takenBytes);
|
|
19
|
+
}
|
|
20
|
+
if (remainder.length > 0) {
|
|
21
|
+
appendToTail(remainder);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function appendToTail(chunk) {
|
|
25
|
+
const ring = (tailRing ??= Buffer.alloc(tailLimitBytes));
|
|
26
|
+
if (chunk.length >= tailLimitBytes) {
|
|
27
|
+
elidedBytes += tailBytes + chunk.length - tailLimitBytes;
|
|
28
|
+
chunk.copy(ring, 0, chunk.length - tailLimitBytes);
|
|
29
|
+
tailOffset = 0;
|
|
30
|
+
tailBytes = tailLimitBytes;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
elidedBytes += Math.max(0, tailBytes + chunk.length - tailLimitBytes);
|
|
34
|
+
const untilWrapBytes = Math.min(chunk.length, tailLimitBytes - tailOffset);
|
|
35
|
+
chunk.copy(ring, tailOffset, 0, untilWrapBytes);
|
|
36
|
+
if (untilWrapBytes < chunk.length) {
|
|
37
|
+
chunk.copy(ring, 0, untilWrapBytes);
|
|
38
|
+
}
|
|
39
|
+
tailOffset = (tailOffset + chunk.length) % tailLimitBytes;
|
|
40
|
+
tailBytes = Math.min(tailLimitBytes, tailBytes + chunk.length);
|
|
41
|
+
}
|
|
42
|
+
function toBuffer() {
|
|
43
|
+
const headBuffer = Buffer.concat(headChunks);
|
|
44
|
+
if (tailRing === undefined) {
|
|
45
|
+
return headBuffer;
|
|
46
|
+
}
|
|
47
|
+
const tailBuffer = tailBytes < tailLimitBytes
|
|
48
|
+
? tailRing.subarray(0, tailBytes)
|
|
49
|
+
: Buffer.concat([tailRing.subarray(tailOffset), tailRing.subarray(0, tailOffset)]);
|
|
50
|
+
if (elidedBytes === 0) {
|
|
51
|
+
return Buffer.concat([headBuffer, tailBuffer]);
|
|
52
|
+
}
|
|
53
|
+
return Buffer.concat([headBuffer, Buffer.from(formatElisionMarker(elidedBytes)), tailBuffer]);
|
|
54
|
+
}
|
|
55
|
+
return { append, toBuffer };
|
|
56
|
+
}
|
|
57
|
+
function formatElisionMarker(byteCount) {
|
|
58
|
+
const unit = byteCount === 1 ? 'byte' : 'bytes';
|
|
59
|
+
return `\n… nmr elided ${byteCount.toLocaleString('en-US')} ${unit} …\n`;
|
|
60
|
+
}
|
|
@@ -7,27 +7,29 @@ export function readPackageJson(dir) {
|
|
|
7
7
|
throw new TypeError(`Invalid package.json in ${dir}: expected an object`);
|
|
8
8
|
}
|
|
9
9
|
const pkg = {};
|
|
10
|
-
if (typeof parsed
|
|
11
|
-
pkg.name = parsed
|
|
12
|
-
if (parsed
|
|
10
|
+
if (typeof parsed['name'] === 'string')
|
|
11
|
+
pkg.name = parsed['name'];
|
|
12
|
+
if (parsed['private'] === true)
|
|
13
13
|
pkg.private = true;
|
|
14
|
-
if (typeof parsed
|
|
15
|
-
pkg.version = parsed
|
|
16
|
-
if (typeof parsed
|
|
17
|
-
pkg.packageManager = parsed
|
|
18
|
-
if (isObject(parsed
|
|
14
|
+
if (typeof parsed['version'] === 'string')
|
|
15
|
+
pkg.version = parsed['version'];
|
|
16
|
+
if (typeof parsed['packageManager'] === 'string')
|
|
17
|
+
pkg.packageManager = parsed['packageManager'];
|
|
18
|
+
if (isObject(parsed['scripts'])) {
|
|
19
19
|
const scripts = {};
|
|
20
|
-
|
|
20
|
+
const scriptEntries = Object.entries(parsed['scripts']);
|
|
21
|
+
for (const [key, val] of scriptEntries) {
|
|
21
22
|
if (typeof val === 'string')
|
|
22
23
|
scripts[key] = val;
|
|
23
24
|
}
|
|
24
25
|
pkg.scripts = scripts;
|
|
25
26
|
}
|
|
26
|
-
if (isObject(parsed
|
|
27
|
-
const pnpm = parsed
|
|
28
|
-
if (isObject(pnpm
|
|
27
|
+
if (isObject(parsed['pnpm'])) {
|
|
28
|
+
const pnpm = parsed['pnpm'];
|
|
29
|
+
if (isObject(pnpm['overrides'])) {
|
|
29
30
|
const overrides = {};
|
|
30
|
-
|
|
31
|
+
const overrideEntries = Object.entries(pnpm['overrides']);
|
|
32
|
+
for (const [key, val] of overrideEntries) {
|
|
31
33
|
if (typeof val === 'string')
|
|
32
34
|
overrides[key] = val;
|
|
33
35
|
}
|
package/dist/esm/resolver.js
CHANGED
|
@@ -46,7 +46,7 @@ export function readPackageJsonScripts(packageDir) {
|
|
|
46
46
|
const parsed = JSON.parse(raw);
|
|
47
47
|
if (!isObject(parsed))
|
|
48
48
|
return undefined;
|
|
49
|
-
const scripts = parsed
|
|
49
|
+
const scripts = parsed['scripts'];
|
|
50
50
|
if (!isObject(scripts))
|
|
51
51
|
return undefined;
|
|
52
52
|
const result = {};
|
|
@@ -82,13 +82,16 @@ export function isSelfReferential(script, commandName) {
|
|
|
82
82
|
export function resolveScript(commandName, registry, packageDir, workspaceRoot) {
|
|
83
83
|
if (packageDir) {
|
|
84
84
|
const pkgScripts = readPackageJsonScripts(packageDir);
|
|
85
|
-
if (pkgScripts && commandName
|
|
85
|
+
if (pkgScripts && Object.hasOwn(pkgScripts, commandName)) {
|
|
86
86
|
const override = pkgScripts[commandName];
|
|
87
87
|
if (override !== undefined && !isSelfReferential(override, commandName)) {
|
|
88
88
|
return { command: override, source: 'package' };
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
if (!Object.hasOwn(registry, commandName)) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
92
95
|
const registryEntry = registry[commandName];
|
|
93
96
|
if (registryEntry === undefined) {
|
|
94
97
|
return undefined;
|
package/dist/esm/runCli.js
CHANGED
|
@@ -41,19 +41,19 @@ export async function runCli(options) {
|
|
|
41
41
|
const runOptions = { quiet: parsed.quiet, stdout, stderr, env: childEnv };
|
|
42
42
|
if (parsed.filter) {
|
|
43
43
|
const delegateCmd = `pnpm --filter ${shellQuote(parsed.filter)} exec nmr ${command}${passthrough}`;
|
|
44
|
-
const exitCode = runCommand(delegateCmd, context.monorepoRoot, runOptions);
|
|
44
|
+
const { exitCode } = await runCommand(delegateCmd, context.monorepoRoot, runOptions);
|
|
45
45
|
return { exitCode };
|
|
46
46
|
}
|
|
47
47
|
if (parsed.recursive) {
|
|
48
48
|
const delegateEnv = { ...childEnv, NMR_RUN_IF_PRESENT: '1' };
|
|
49
49
|
const delegateCmd = `pnpm --recursive exec nmr ${command}${passthrough}`;
|
|
50
|
-
const exitCode = runCommand(delegateCmd, context.monorepoRoot, { ...runOptions, env: delegateEnv });
|
|
50
|
+
const { exitCode } = await runCommand(delegateCmd, context.monorepoRoot, { ...runOptions, env: delegateEnv });
|
|
51
51
|
return { exitCode };
|
|
52
52
|
}
|
|
53
53
|
const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
|
|
54
54
|
const resolved = resolveScript(command, registry, anchorDir, parsed.workspaceRoot);
|
|
55
55
|
if (!resolved) {
|
|
56
|
-
if (env
|
|
56
|
+
if (env['NMR_RUN_IF_PRESENT'] === '1') {
|
|
57
57
|
return { exitCode: 0 };
|
|
58
58
|
}
|
|
59
59
|
reportError(`Unknown command: ${command}`, stderr);
|
|
@@ -105,7 +105,8 @@ function buildChildEnv(env, snapshot, noCache) {
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
function formatOverrideNotice(resolved, registry, command, anchorDir, quiet) {
|
|
108
|
-
|
|
108
|
+
const registryEntry = Object.hasOwn(registry, command) ? registry[command] : undefined;
|
|
109
|
+
if (quiet || resolved.source !== 'package' || registryEntry === undefined) {
|
|
109
110
|
return undefined;
|
|
110
111
|
}
|
|
111
112
|
return `📦 ${path.basename(anchorDir)}: Using override script: ${resolved.command}\n`;
|
|
@@ -132,7 +133,7 @@ function hasRunnableHook(hookName, registry, anchorDir, workspaceRoot) {
|
|
|
132
133
|
return resolved.command !== '' && resolved.command !== ':';
|
|
133
134
|
}
|
|
134
135
|
async function lookUpRecordedPass(options) {
|
|
135
|
-
const { anchorDir, command, env, key, monorepoRoot, stderr } = options;
|
|
136
|
+
const { anchorDir, buildOutput, command, env, key, monorepoRoot, stderr } = options;
|
|
136
137
|
const entry = await readCheckCacheEntry({ anchorDir, command, monorepoRoot });
|
|
137
138
|
if (entry === undefined) {
|
|
138
139
|
writeDebugNote(`running ${command}: no pass recorded for this scope`, env, stderr);
|
|
@@ -142,13 +143,12 @@ async function lookUpRecordedPass(options) {
|
|
|
142
143
|
writeDebugNote(`running ${command}: the tree or its inputs changed since the last pass`, env, stderr);
|
|
143
144
|
return undefined;
|
|
144
145
|
}
|
|
145
|
-
const
|
|
146
|
-
const [missing] = output.missing;
|
|
146
|
+
const [missing] = buildOutput.missing;
|
|
147
147
|
if (missing !== undefined) {
|
|
148
148
|
writeDebugNote(`running ${command}: ${missing} has no build output`, env, stderr);
|
|
149
149
|
return undefined;
|
|
150
150
|
}
|
|
151
|
-
const stale = findStaleBuildOutput(entry.buildDigests,
|
|
151
|
+
const stale = findStaleBuildOutput(entry.buildDigests, buildOutput.digests);
|
|
152
152
|
if (stale !== undefined) {
|
|
153
153
|
writeDebugNote(`running ${command}: ${stale}'s build output came from a different tree`, env, stderr);
|
|
154
154
|
return undefined;
|
|
@@ -254,6 +254,11 @@ async function recordPass(options) {
|
|
|
254
254
|
writeDebugNote(`not recording ${command}: ${missing} has no build output`, env, stderr);
|
|
255
255
|
return;
|
|
256
256
|
}
|
|
257
|
+
const changed = findStaleBuildOutput(options.buildOutputBefore.digests, output.digests);
|
|
258
|
+
if (changed !== undefined) {
|
|
259
|
+
writeDebugNote(`not recording ${command}: ${changed}'s build output changed while it ran`, env, stderr);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
257
262
|
try {
|
|
258
263
|
await writeCheckCacheEntry({
|
|
259
264
|
anchorDir,
|
|
@@ -303,14 +308,16 @@ function resolveCacheKey(options) {
|
|
|
303
308
|
}
|
|
304
309
|
async function runGated(options) {
|
|
305
310
|
const { anchorDir, command, commandString, env, key, monorepoRoot, snapshot, stderr, stdout } = options;
|
|
306
|
-
const
|
|
307
|
-
|
|
311
|
+
const gate = key !== undefined && snapshot !== undefined
|
|
312
|
+
? { buildOutputBefore: await readBuildOutputState(monorepoRoot, options.config), key, snapshot }
|
|
313
|
+
: undefined;
|
|
314
|
+
if (gate !== undefined && !options.noCache) {
|
|
308
315
|
const skipLine = await lookUpRecordedPass({
|
|
309
316
|
anchorDir,
|
|
317
|
+
buildOutput: gate.buildOutputBefore,
|
|
310
318
|
command,
|
|
311
|
-
config: options.config,
|
|
312
319
|
env,
|
|
313
|
-
key,
|
|
320
|
+
key: gate.key,
|
|
314
321
|
monorepoRoot,
|
|
315
322
|
stderr,
|
|
316
323
|
});
|
|
@@ -325,18 +332,19 @@ async function runGated(options) {
|
|
|
325
332
|
stdout.write(options.overrideNotice);
|
|
326
333
|
}
|
|
327
334
|
const startedAt = Date.now();
|
|
328
|
-
const exitCode = runCommand(commandString, anchorDir, options.runOptions);
|
|
329
|
-
if (exitCode === 0 &&
|
|
335
|
+
const { exitCode } = await runCommand(commandString, anchorDir, options.runOptions);
|
|
336
|
+
if (exitCode === 0 && gate !== undefined) {
|
|
330
337
|
await recordPass({
|
|
331
338
|
anchorDir,
|
|
339
|
+
buildOutputBefore: gate.buildOutputBefore,
|
|
332
340
|
command,
|
|
333
341
|
commandString,
|
|
334
342
|
config: options.config,
|
|
335
343
|
durationMs: Date.now() - startedAt,
|
|
336
344
|
env,
|
|
337
|
-
key,
|
|
345
|
+
key: gate.key,
|
|
338
346
|
monorepoRoot,
|
|
339
|
-
snapshot,
|
|
347
|
+
snapshot: gate.snapshot,
|
|
340
348
|
stderr,
|
|
341
349
|
});
|
|
342
350
|
}
|
package/dist/esm/runner.d.ts
CHANGED
|
@@ -5,4 +5,21 @@ export interface RunCommandOptions {
|
|
|
5
5
|
stderr?: Writable;
|
|
6
6
|
env?: NodeJS.ProcessEnv;
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
type CommandCompletion = {
|
|
9
|
+
outcome: 'exited';
|
|
10
|
+
exitCode: number;
|
|
11
|
+
} | {
|
|
12
|
+
outcome: 'signaled';
|
|
13
|
+
exitCode: number;
|
|
14
|
+
signal: NodeJS.Signals;
|
|
15
|
+
} | {
|
|
16
|
+
outcome: 'spawn-failed';
|
|
17
|
+
exitCode: number;
|
|
18
|
+
error: Error;
|
|
19
|
+
};
|
|
20
|
+
export type RunCommandResult = CommandCompletion & {
|
|
21
|
+
stdout: Buffer | undefined;
|
|
22
|
+
stderr: Buffer | undefined;
|
|
23
|
+
};
|
|
24
|
+
export declare function runCommand(command: string, cwd?: string, options?: RunCommandOptions): Promise<RunCommandResult>;
|
|
25
|
+
export {};
|
package/dist/esm/runner.js
CHANGED
|
@@ -1,41 +1,114 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import process from 'node:process';
|
|
3
4
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
4
|
-
|
|
5
|
+
import { createBoundedBuffer } from "./helpers/createBoundedBuffer.js";
|
|
6
|
+
const SIGNAL_EXIT_BASE = 128;
|
|
7
|
+
const PIPE_DRAIN_GRACE_MS = 2_000;
|
|
8
|
+
export async function runCommand(command, cwd, options) {
|
|
5
9
|
const quiet = options?.quiet === true;
|
|
6
10
|
const stdout = options?.stdout ?? process.stdout;
|
|
7
11
|
const stderr = options?.stderr ?? process.stderr;
|
|
8
12
|
const env = options?.env ?? process.env;
|
|
9
|
-
const
|
|
10
|
-
const stderrChannel = quiet ? 'pipe' : streamFdOrPipe(stderr);
|
|
11
|
-
const result = spawnSync(command, [], {
|
|
13
|
+
const child = spawn(command, [], {
|
|
12
14
|
shell: true,
|
|
13
|
-
stdio: ['inherit',
|
|
15
|
+
stdio: ['inherit', resolveChannel(stdout, quiet), resolveChannel(stderr, quiet)],
|
|
14
16
|
cwd,
|
|
15
17
|
env,
|
|
16
18
|
});
|
|
17
|
-
|
|
19
|
+
const stdoutCapture = child.stdout === null ? undefined : captureStream(child.stdout, quiet ? undefined : stdout);
|
|
20
|
+
const stderrCapture = child.stderr === null ? undefined : captureStream(child.stderr, quiet ? undefined : stderr);
|
|
21
|
+
const completion = await awaitCompletion(child, () => {
|
|
22
|
+
stdoutCapture?.stopReading();
|
|
23
|
+
stderrCapture?.stopReading();
|
|
24
|
+
});
|
|
25
|
+
stdoutCapture?.detach();
|
|
26
|
+
stderrCapture?.detach();
|
|
27
|
+
const result = {
|
|
28
|
+
...completion,
|
|
29
|
+
stdout: stdoutCapture?.toBuffer(),
|
|
30
|
+
stderr: stderrCapture?.toBuffer(),
|
|
31
|
+
};
|
|
32
|
+
if (result.outcome === 'spawn-failed') {
|
|
18
33
|
reportError(result.error.message, stderr);
|
|
19
|
-
return
|
|
34
|
+
return result;
|
|
20
35
|
}
|
|
21
|
-
if (quiet) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
if (quiet && result.exitCode !== 0) {
|
|
37
|
+
writeBuffer(result.stdout, stderr);
|
|
38
|
+
writeBuffer(result.stderr, stderr);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
function captureStream(source, destination) {
|
|
43
|
+
const retained = createBoundedBuffer();
|
|
44
|
+
let isComplete = true;
|
|
45
|
+
function stopReading() {
|
|
46
|
+
if (destination !== undefined)
|
|
47
|
+
source.unpipe(destination);
|
|
48
|
+
source.destroy();
|
|
49
|
+
}
|
|
50
|
+
function abandon() {
|
|
51
|
+
isComplete = false;
|
|
52
|
+
stopReading();
|
|
26
53
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
writeBuffer(result.stderr, stderr);
|
|
54
|
+
source.on('error', abandon);
|
|
55
|
+
if (destination !== undefined) {
|
|
56
|
+
destination.on('error', abandon);
|
|
57
|
+
source.pipe(destination, { end: false });
|
|
32
58
|
}
|
|
33
|
-
|
|
59
|
+
source.on('data', (chunk) => retained.append(chunk));
|
|
60
|
+
return {
|
|
61
|
+
toBuffer: () => (isComplete ? retained.toBuffer() : undefined),
|
|
62
|
+
detach: () => {
|
|
63
|
+
destination?.off('error', abandon);
|
|
64
|
+
},
|
|
65
|
+
stopReading,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function awaitCompletion(child, stopReading) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
let exitCompletion;
|
|
71
|
+
let graceTimer;
|
|
72
|
+
let isSettled = false;
|
|
73
|
+
function settle(completion) {
|
|
74
|
+
if (isSettled)
|
|
75
|
+
return;
|
|
76
|
+
isSettled = true;
|
|
77
|
+
if (graceTimer !== undefined)
|
|
78
|
+
clearTimeout(graceTimer);
|
|
79
|
+
resolve(completion);
|
|
80
|
+
}
|
|
81
|
+
child.on('error', (error) => {
|
|
82
|
+
settle({ outcome: 'spawn-failed', exitCode: 1, error });
|
|
83
|
+
});
|
|
84
|
+
child.on('exit', (code, signal) => {
|
|
85
|
+
const completion = describeExit(code, signal);
|
|
86
|
+
exitCompletion = completion;
|
|
87
|
+
graceTimer = setTimeout(() => {
|
|
88
|
+
stopReading();
|
|
89
|
+
settle(completion);
|
|
90
|
+
}, PIPE_DRAIN_GRACE_MS);
|
|
91
|
+
});
|
|
92
|
+
child.on('close', () => {
|
|
93
|
+
settle(exitCompletion ?? { outcome: 'exited', exitCode: 1 });
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function describeExit(code, signal) {
|
|
98
|
+
if (signal !== null) {
|
|
99
|
+
return { outcome: 'signaled', exitCode: SIGNAL_EXIT_BASE + os.constants.signals[signal], signal };
|
|
100
|
+
}
|
|
101
|
+
return { outcome: 'exited', exitCode: code ?? 1 };
|
|
102
|
+
}
|
|
103
|
+
function resolveChannel(stream, quiet) {
|
|
104
|
+
if (quiet)
|
|
105
|
+
return 'pipe';
|
|
106
|
+
return isTerminal(stream) ? stream.fd : 'pipe';
|
|
34
107
|
}
|
|
35
|
-
function
|
|
36
|
-
return '
|
|
108
|
+
function isTerminal(stream) {
|
|
109
|
+
return 'isTTY' in stream && stream.isTTY === true && 'fd' in stream && typeof stream.fd === 'number';
|
|
37
110
|
}
|
|
38
|
-
function writeBuffer(buffer,
|
|
39
|
-
if (buffer.length > 0)
|
|
40
|
-
|
|
111
|
+
function writeBuffer(buffer, destination) {
|
|
112
|
+
if (buffer !== undefined && buffer.length > 0)
|
|
113
|
+
destination.write(buffer);
|
|
41
114
|
}
|
package/dist/esm/vitest.js
CHANGED
|
@@ -15,7 +15,7 @@ export function defineVitestConfig(...layers) {
|
|
|
15
15
|
}
|
|
16
16
|
export function defineRootVitestConfig(...layers) {
|
|
17
17
|
const lastLayer = layers.at(-1);
|
|
18
|
-
const monorepoRoot = isObject(lastLayer) ? lastLayer
|
|
18
|
+
const monorepoRoot = isObject(lastLayer) ? lastLayer['monorepoRoot'] : undefined;
|
|
19
19
|
if (typeof monorepoRoot !== 'string' || !path.isAbsolute(monorepoRoot)) {
|
|
20
20
|
throw new TypeError(MISSING_MONOREPO_ROOT);
|
|
21
21
|
}
|
|
@@ -42,7 +42,12 @@ function buildConfig(declaredLayers, { coverageInclude, projectExclude = [], pro
|
|
|
42
42
|
watch: false,
|
|
43
43
|
},
|
|
44
44
|
};
|
|
45
|
-
|
|
45
|
+
let merged = config;
|
|
46
|
+
for (const { root } of layers) {
|
|
47
|
+
if (root)
|
|
48
|
+
merged = mergeConfig(merged, root);
|
|
49
|
+
}
|
|
50
|
+
return merged;
|
|
46
51
|
}
|
|
47
52
|
function buildProjects(layers, extraExclude, projectRoot) {
|
|
48
53
|
const projectTiers = [
|
|
@@ -65,7 +70,11 @@ function buildProjects(layers, extraExclude, projectRoot) {
|
|
|
65
70
|
...(timeout !== undefined && { hookTimeout: timeout, testTimeout: timeout }),
|
|
66
71
|
},
|
|
67
72
|
};
|
|
68
|
-
|
|
73
|
+
let merged = project;
|
|
74
|
+
for (const layer of layers) {
|
|
75
|
+
merged = applyLayer(merged, layer, name);
|
|
76
|
+
}
|
|
77
|
+
return merged;
|
|
69
78
|
});
|
|
70
79
|
}
|
|
71
80
|
function applyLayer(project, layer, tier) {
|
|
@@ -76,7 +85,8 @@ function applyLayer(project, layer, tier) {
|
|
|
76
85
|
function assertKnownTiers(layers) {
|
|
77
86
|
const known = TIER_NAMES;
|
|
78
87
|
for (const { tiers } of layers) {
|
|
79
|
-
|
|
88
|
+
const declaredTiers = Object.keys(tiers ?? {});
|
|
89
|
+
for (const name of declaredTiers) {
|
|
80
90
|
if (!known.includes(name)) {
|
|
81
91
|
throw new TypeError(`Unknown tier "${name}" in \`tiers\`. Valid tiers: ${TIER_NAMES.join(', ')}.`);
|
|
82
92
|
}
|
package/dist/esm/workspace.js
CHANGED
|
@@ -33,7 +33,7 @@ export function getWorkspacePackageDirs(monorepoRoot) {
|
|
|
33
33
|
function getPackagesFromParsedYaml(parsed) {
|
|
34
34
|
if (!isObject(parsed))
|
|
35
35
|
return undefined;
|
|
36
|
-
const packages = parsed
|
|
36
|
+
const packages = parsed['packages'];
|
|
37
37
|
if (!Array.isArray(packages))
|
|
38
38
|
return undefined;
|
|
39
39
|
if (!packages.every((p) => typeof p === 'string'))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for pnpm monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -68,11 +68,11 @@
|
|
|
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.3"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"prettier": "3.9.6",
|
|
75
|
-
"readyup": "0.
|
|
75
|
+
"readyup": "0.26.0",
|
|
76
76
|
"tinyglobby": "0.2.17",
|
|
77
77
|
"vitest": "4.1.10"
|
|
78
78
|
},
|