@williamthorsen/nmr 0.26.1 → 0.28.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 +59 -47
- package/.readyup/manifest.json +3 -3
- package/README.md +33 -9
- 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.js +7 -6
- 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/ensure-prepublish-hooks.js +5 -5
- package/dist/esm/commands/fmt.js +1 -1
- package/dist/esm/config.js +8 -8
- package/dist/esm/default-scripts.js +3 -3
- package/dist/esm/help.js +1 -1
- package/dist/esm/helpers/duration.js +1 -1
- package/dist/esm/helpers/package-json.js +15 -13
- package/dist/esm/resolver.js +5 -2
- package/dist/esm/runCli.js +3 -2
- 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.24.0";
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
// .readyup/kits/default.ts
|
|
@@ -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()}`;
|
|
@@ -165,6 +163,7 @@ var default_default = defineRdyKit({
|
|
|
165
163
|
{
|
|
166
164
|
name: ".tool-versions does not list pnpm",
|
|
167
165
|
severity: "warn",
|
|
166
|
+
quiet: true,
|
|
168
167
|
check: toolVersionsHasNoPnpm,
|
|
169
168
|
fix: "Remove pnpm from .tool-versions \u2014 manage via packageManager field and corepack"
|
|
170
169
|
},
|
|
@@ -175,20 +174,38 @@ var default_default = defineRdyKit({
|
|
|
175
174
|
check: () => fileContains(".config/nmr.config.ts", /defineConfig/),
|
|
176
175
|
fix: "Wrap your config export with defineConfig() from @williamthorsen/nmr/config for type safety"
|
|
177
176
|
},
|
|
177
|
+
// `error` because falling short produces wrong results rather than a failure. Names and fixes are
|
|
178
|
+
// getters because their version constants are declared below the kit.
|
|
179
|
+
{
|
|
180
|
+
get name() {
|
|
181
|
+
return `eslint >= ${MIN_ESLINT_VERSION}`;
|
|
182
|
+
},
|
|
183
|
+
severity: "error",
|
|
184
|
+
skip: () => !hasDevDependency("eslint") ? "eslint not installed" : false,
|
|
185
|
+
check: hasSupportedEslintVersion,
|
|
186
|
+
get fix() {
|
|
187
|
+
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`;
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
get name() {
|
|
192
|
+
return `@williamthorsen/strict-lint >= ${MIN_STRICT_LINT_VERSION}`;
|
|
193
|
+
},
|
|
194
|
+
severity: "error",
|
|
195
|
+
skip: () => !hasDevDependency("@williamthorsen/strict-lint") ? "strict-lint not installed" : false,
|
|
196
|
+
check: hasSupportedStrictLintVersion,
|
|
197
|
+
get fix() {
|
|
198
|
+
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`;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
178
201
|
// -- Root script cleanup -------------------------------------------------
|
|
179
202
|
{
|
|
180
203
|
name: "root package.json has no nmr-provided scripts",
|
|
181
204
|
severity: "warn",
|
|
205
|
+
quiet: true,
|
|
182
206
|
check: noRedundantRootScripts,
|
|
183
207
|
fix: "Remove scripts from root package.json that nmr provides as built-in root scripts \u2014 invoke via nmr directly"
|
|
184
208
|
},
|
|
185
|
-
{
|
|
186
|
-
name: "root:lint:strict does not use echo fallback",
|
|
187
|
-
severity: "warn",
|
|
188
|
-
skip: () => !scriptExists("root:lint:strict") ? "no root:lint:strict script" : false,
|
|
189
|
-
check: () => !scriptMatches("root:lint:strict", /\becho\b/),
|
|
190
|
-
fix: "Replace the echo fallback in root:lint:strict \u2014 strict-lint now supports path arguments"
|
|
191
|
-
},
|
|
192
209
|
// -- Workspace build readiness -------------------------------------------
|
|
193
210
|
{
|
|
194
211
|
name: "all workspace packages can build",
|
|
@@ -200,6 +217,7 @@ var default_default = defineRdyKit({
|
|
|
200
217
|
{
|
|
201
218
|
name: "no retired Vitest config variants",
|
|
202
219
|
severity: "error",
|
|
220
|
+
quiet: true,
|
|
203
221
|
check: () => noRetiredVitestConfigs(),
|
|
204
222
|
fix: "Delete every vitest.standalone.config.* and vitest.integration.config.*. nmr's test scripts select Vitest projects instead of naming config files"
|
|
205
223
|
},
|
|
@@ -224,6 +242,7 @@ var default_default = defineRdyKit({
|
|
|
224
242
|
{
|
|
225
243
|
name: "no package re-exports the ancestor Vitest config",
|
|
226
244
|
severity: "recommend",
|
|
245
|
+
quiet: true,
|
|
227
246
|
check: () => noReExportOnlyVitestConfigs(),
|
|
228
247
|
fix: "Delete these files. Vitest resolves config by walking up from the run root, so a per-package re-export is redundant"
|
|
229
248
|
},
|
|
@@ -241,23 +260,18 @@ var default_default = defineRdyKit({
|
|
|
241
260
|
check: () => hasDevDependency("v11y-check"),
|
|
242
261
|
fix: "pnpm add --save-dev v11y-check"
|
|
243
262
|
},
|
|
244
|
-
{
|
|
245
|
-
name: "code-quality workflow does not use nmr prepush",
|
|
246
|
-
severity: "warn",
|
|
247
|
-
skip: () => !fileExists(".github/workflows/code-quality.yaml") ? "no code-quality workflow" : false,
|
|
248
|
-
check: codeQualityWorkflowDoesNotUseNmrPrepush,
|
|
249
|
-
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'
|
|
250
|
-
},
|
|
251
263
|
// -- Legacy script runner ------------------------------------------------
|
|
252
264
|
{
|
|
253
265
|
name: "scripts/run-workspace-script.ts does not exist",
|
|
254
266
|
severity: "error",
|
|
267
|
+
quiet: true,
|
|
255
268
|
check: () => !fileExists("scripts/run-workspace-script.ts"),
|
|
256
269
|
fix: "Delete scripts/run-workspace-script.ts \u2014 nmr replaces this custom script runner"
|
|
257
270
|
},
|
|
258
271
|
{
|
|
259
272
|
name: 'no workspace packages reference run-workspace-script or "pnpm run ws"',
|
|
260
273
|
severity: "error",
|
|
274
|
+
quiet: true,
|
|
261
275
|
check: noWorkspaceRunScriptReferences,
|
|
262
276
|
fix: 'Remove "ws" script entries and replace any "pnpm run ws" invocations with nmr in each packages/*/package.json'
|
|
263
277
|
}
|
|
@@ -270,7 +284,10 @@ var CONFIG_EXTENSIONS = "{ts,mts,cts,js,mjs,cjs}";
|
|
|
270
284
|
var SHARED_VITEST_MODULE = "@williamthorsen/nmr/vitest";
|
|
271
285
|
var SHARED_PRETTIER_MODULE = "@williamthorsen/nmr/prettier";
|
|
272
286
|
var INERT_PRETTIER_CONFIGS = [".prettierrc", ".prettierrc.{json,json5,yaml,yml,toml}"];
|
|
273
|
-
var RE_EXPORT_LINE_PATTERN = /^export\s*(?:\{\s*default\s
|
|
287
|
+
var RE_EXPORT_LINE_PATTERN = /^export\s*(?:\{\s*default\s*}|\*)\s*from\s*['"]\.\.\/[^'"]*['"];?$/;
|
|
288
|
+
var MIN_ESLINT_VERSION = "10.0.0";
|
|
289
|
+
var MIN_STRICT_LINT_VERSION = "9.3.0";
|
|
290
|
+
var WORKSPACE_VERSION_MARKERS = ["catalog:", "workspace:"];
|
|
274
291
|
function allWorkspacePackagesCanBuild() {
|
|
275
292
|
const packagesDir = join(process.cwd(), "packages");
|
|
276
293
|
if (!existsSync(packagesDir)) return true;
|
|
@@ -340,16 +357,11 @@ function hasPrettierConfigKey(cwd) {
|
|
|
340
357
|
if (manifest === void 0) return false;
|
|
341
358
|
try {
|
|
342
359
|
const parsed = JSON.parse(manifest);
|
|
343
|
-
return isRecord(parsed) && parsed
|
|
360
|
+
return isRecord(parsed) && parsed["prettier"] !== void 0;
|
|
344
361
|
} catch {
|
|
345
362
|
return false;
|
|
346
363
|
}
|
|
347
364
|
}
|
|
348
|
-
function codeQualityWorkflowDoesNotUseNmrPrepush() {
|
|
349
|
-
const content = readFile(".github/workflows/code-quality.yaml");
|
|
350
|
-
if (content === void 0) return true;
|
|
351
|
-
return !/check-command:\s*pnpm exec nmr prepush(\s|$)/.test(content);
|
|
352
|
-
}
|
|
353
365
|
function everyTestFileNamesItsTier(cwd = process.cwd()) {
|
|
354
366
|
const untiered = findTestFiles(cwd).filter((path2) => !hasTierInfix(path2));
|
|
355
367
|
if (untiered.length === 0) return true;
|
|
@@ -363,11 +375,21 @@ function formatPaths(paths) {
|
|
|
363
375
|
${paths.map((path2) => ` ${path2}`).join("\n")}`;
|
|
364
376
|
}
|
|
365
377
|
function getMinVersion() {
|
|
366
|
-
const picked = { "version": "0.
|
|
367
|
-
if (typeof picked
|
|
378
|
+
const picked = { "version": "0.28.0" };
|
|
379
|
+
if (typeof picked["version"] !== "string") {
|
|
368
380
|
throw new TypeError("nmr/package.json: 'version' must be a string");
|
|
369
381
|
}
|
|
370
|
-
return picked
|
|
382
|
+
return picked["version"];
|
|
383
|
+
}
|
|
384
|
+
function hasSupportedEslintVersion() {
|
|
385
|
+
return hasMinDevDependencyVersion("eslint", MIN_ESLINT_VERSION, {
|
|
386
|
+
exempt: resolvesVersionViaWorkspace
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function hasSupportedStrictLintVersion() {
|
|
390
|
+
return hasMinDevDependencyVersion("@williamthorsen/strict-lint", MIN_STRICT_LINT_VERSION, {
|
|
391
|
+
exempt: resolvesVersionViaWorkspace
|
|
392
|
+
});
|
|
371
393
|
}
|
|
372
394
|
function importsSharedExport(content, exportName, moduleSpecifier) {
|
|
373
395
|
if (content === void 0) return false;
|
|
@@ -388,7 +410,7 @@ function noReExportOnlyVitestConfigs(cwd = process.cwd()) {
|
|
|
388
410
|
function noRedundantRootScripts() {
|
|
389
411
|
const pkg = readPackageJson();
|
|
390
412
|
if (!pkg) return true;
|
|
391
|
-
const scripts = pkg
|
|
413
|
+
const scripts = pkg["scripts"];
|
|
392
414
|
if (!isRecord(scripts)) return true;
|
|
393
415
|
const builtInNames = Object.keys(getDefaultRootScripts());
|
|
394
416
|
const redundant = Object.keys(scripts).filter((name) => builtInNames.includes(name));
|
|
@@ -430,19 +452,8 @@ function readFileIn(cwd, relativePath) {
|
|
|
430
452
|
return void 0;
|
|
431
453
|
}
|
|
432
454
|
}
|
|
433
|
-
function
|
|
434
|
-
|
|
435
|
-
if (!pkg) return false;
|
|
436
|
-
const scripts = pkg.scripts;
|
|
437
|
-
return isRecord(scripts) && name in scripts;
|
|
438
|
-
}
|
|
439
|
-
function scriptMatches(name, pattern) {
|
|
440
|
-
const pkg = readPackageJson();
|
|
441
|
-
if (!pkg) return false;
|
|
442
|
-
const scripts = pkg.scripts;
|
|
443
|
-
if (!isRecord(scripts)) return false;
|
|
444
|
-
const value = scripts[name];
|
|
445
|
-
return typeof value === "string" && pattern.test(value);
|
|
455
|
+
function resolvesVersionViaWorkspace(range) {
|
|
456
|
+
return WORKSPACE_VERSION_MARKERS.some((marker) => range.startsWith(marker));
|
|
446
457
|
}
|
|
447
458
|
function toolVersionsHasNoPnpm() {
|
|
448
459
|
const content = readFile(".tool-versions");
|
|
@@ -456,9 +467,10 @@ function vitestRootConfigBuildsOnSharedConfig(cwd = process.cwd()) {
|
|
|
456
467
|
return checkRootVitestConfig("vitest.root.config", "defineRootVitestConfig", cwd);
|
|
457
468
|
}
|
|
458
469
|
export {
|
|
459
|
-
codeQualityWorkflowDoesNotUseNmrPrepush,
|
|
460
470
|
default_default as default,
|
|
461
471
|
everyTestFileNamesItsTier,
|
|
472
|
+
hasSupportedEslintVersion,
|
|
473
|
+
hasSupportedStrictLintVersion,
|
|
462
474
|
noReExportOnlyVitestConfigs,
|
|
463
475
|
noRetiredVitestConfigs,
|
|
464
476
|
prettierConfigBuildsOnSharedConfig,
|
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.24.0",
|
|
11
11
|
"source": "kits/default.ts",
|
|
12
|
-
"sourceHash": "
|
|
13
|
-
"targetHash": "
|
|
12
|
+
"sourceHash": "7b2a223f",
|
|
13
|
+
"targetHash": "b7abeb12"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|
package/README.md
CHANGED
|
@@ -3,13 +3,29 @@
|
|
|
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.28.0 (2026-08-08)
|
|
7
|
+
|
|
8
|
+
### 🎉 Features
|
|
9
|
+
|
|
10
|
+
- Stop reporting absence checks that pass (#610)
|
|
11
|
+
|
|
12
|
+
A ReadyUp check that checks for a problem (such as an unwanted file or a stale config reference) now stays silent unless it finds one. The `nmr`, `release-kit`, and `v11y-check` ReadyUp kits previously printed a line for every such check on every run.
|
|
13
|
+
|
|
14
|
+
- Stop reporting non-issues in the readyup kits (#613)
|
|
15
|
+
|
|
16
|
+
Modifies the `release-kit` and `nmr` ReadyUp kits to avoid verbose reporting when an initial check determines that further checks aren't applicable. In particular, a repo with no `release-kit` config file gets a single line in the `release-kit` report, and a repo that publishes nothing now bypasses the `npm-auto-publish` kit instead of failing it.
|
|
7
17
|
|
|
8
18
|
### 🐛 Bug fixes
|
|
9
19
|
|
|
10
|
-
-
|
|
20
|
+
- Fix prototype-key hole in nmr command runner (#624)
|
|
21
|
+
|
|
22
|
+
Fixes an issue where running `nmr` with a command named for property inherited by JS objects (such as `constructor` or `toString`) failed with a type error instead of reporting the command as unknown.
|
|
11
23
|
|
|
12
|
-
|
|
24
|
+
Also fixes all lint issues in the `nmr` package.
|
|
25
|
+
|
|
26
|
+
- Publish build output atomically so a failed build cannot destroy it (#631)
|
|
27
|
+
|
|
28
|
+
Fixes an issue where a failed or still-running `nmr build` could leave a package with no build output at all, breaking the tooling that a subsequent build depends on. Separately, the error reported when a package's build output is missing now names the correct recovery command.
|
|
13
29
|
<!-- /section:release-notes -->
|
|
14
30
|
|
|
15
31
|
## Installation
|
|
@@ -466,11 +482,17 @@ The same six names the workspace registry carries, so a command means the same t
|
|
|
466
482
|
|
|
467
483
|
#### Lint
|
|
468
484
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
|
485
|
+
The lint commands respect each package's own `eslint.config.*`. Use the `root:` variants below to lint root-level code alone.
|
|
486
|
+
|
|
487
|
+
Requires `eslint` >= 10 and, for `lint:strict`, `@williamthorsen/strict-lint` >= 9.3.0.
|
|
488
|
+
|
|
489
|
+
| Command | Runs |
|
|
490
|
+
| ------------- | ---------------- |
|
|
491
|
+
| `lint` | `eslint --fix .` |
|
|
492
|
+
| `lint:check` | `eslint .` |
|
|
493
|
+
| `lint:strict` | `strict-lint` |
|
|
494
|
+
|
|
495
|
+
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
496
|
|
|
475
497
|
#### Format
|
|
476
498
|
|
|
@@ -620,7 +642,9 @@ Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler
|
|
|
620
642
|
|
|
621
643
|
**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.
|
|
622
644
|
|
|
623
|
-
**The build owns its output directory.** Every rebuild
|
|
645
|
+
**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.
|
|
646
|
+
|
|
647
|
+
**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.
|
|
624
648
|
|
|
625
649
|
`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.
|
|
626
650
|
|
|
@@ -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
|
}
|
package/dist/esm/check-cache.js
CHANGED
|
@@ -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) {
|
|
@@ -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,10 +78,10 @@ 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
|
-
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
|
}
|
|
@@ -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) {
|
|
@@ -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'],
|
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;
|
|
@@ -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 = '🚀';
|
|
@@ -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
|
@@ -53,7 +53,7 @@ export async function runCli(options) {
|
|
|
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`;
|
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.28.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.24.0",
|
|
76
76
|
"tinyglobby": "0.2.17",
|
|
77
77
|
"vitest": "4.1.10"
|
|
78
78
|
},
|