@williamthorsen/nmr 0.32.2 → 0.34.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 +33 -10
- package/.readyup/manifest.json +6 -6
- package/README.md +89 -20
- package/dist/esm/commands/taze.js +17 -1
- package/dist/esm/default-scripts.js +2 -2
- package/dist/esm/vitest-git-isolation.d.ts +1 -0
- package/dist/esm/vitest-git-isolation.js +4 -0
- package/dist/esm/vitest.d.ts +2 -0
- package/dist/esm/vitest.js +23 -0
- package/package.json +11 -9
package/.readyup/kits/default.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/** @noformat — @generated. Do not edit. Compiled by rdy. */
|
|
2
2
|
/* eslint-disable */
|
|
3
|
-
export const __readyupVersion = "0.
|
|
3
|
+
export const __readyupVersion = "0.32.0";
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
// .readyup/kits/default.ts
|
|
7
|
-
import { existsSync, globSync, readdirSync as readdirSync2
|
|
7
|
+
import { existsSync, globSync, readdirSync as readdirSync2 } from "node:fs";
|
|
8
8
|
import { basename, join, sep } from "node:path";
|
|
9
9
|
import { defineRdyKit } from "readyup";
|
|
10
10
|
import {
|
|
@@ -25,7 +25,7 @@ var ROOT_TYPECHECK_STEP = { run: "root:typecheck", declinesArgs: true };
|
|
|
25
25
|
var workspaceScripts = {
|
|
26
26
|
build: ["compile"],
|
|
27
27
|
check: [TYPECHECK_STEP, "fmt:check", "lint:check", "test"],
|
|
28
|
-
"check:strict": [TYPECHECK_STEP, "fmt:check", "lint:strict", "test
|
|
28
|
+
"check:strict": [TYPECHECK_STEP, "fmt:check", "lint:strict", "test"],
|
|
29
29
|
clean: "nmr-clean",
|
|
30
30
|
compile: "nmr-compile",
|
|
31
31
|
fix: ["lint", "fmt"],
|
|
@@ -53,7 +53,7 @@ var rootScripts = {
|
|
|
53
53
|
"audit:prod": "pnpm exec v11y --prod",
|
|
54
54
|
build: ["-R build"],
|
|
55
55
|
check: [TYPECHECK_STEP, "fmt:check", "lint:check", "test"],
|
|
56
|
-
"check:strict": [TYPECHECK_STEP, "fmt:check", "lint:strict", "test
|
|
56
|
+
"check:strict": [TYPECHECK_STEP, "fmt:check", "lint:strict", "test"],
|
|
57
57
|
// Excludes the audit, which in CI has a workflow of its own. The build is what the narrowed check runs
|
|
58
58
|
// against, so it declines the arguments rather than being narrowed by them.
|
|
59
59
|
ci: [{ run: "build", declinesArgs: true }, "check:strict"],
|
|
@@ -278,6 +278,13 @@ var default_default = defineRdyKit({
|
|
|
278
278
|
check: () => tazeConfigBuildsOnSharedConfig(),
|
|
279
279
|
fix: "Replace the taze config with: import { defineConfig } from '@williamthorsen/nmr/taze'; export default defineConfig(); \u2014 nmr's upgrade policy reaches a repo only through this file, so without it `nmr upgrade` reports nothing where dependencies are pinned to exact versions"
|
|
280
280
|
},
|
|
281
|
+
{
|
|
282
|
+
name: "taze config declares no option taze discards",
|
|
283
|
+
severity: "warn",
|
|
284
|
+
quiet: true,
|
|
285
|
+
check: () => tazeConfigAvoidsClobberedOptions(),
|
|
286
|
+
fix: "Set these through the upgrade script instead, as rootScripts: { upgrade: 'nmr-report-overrides && nmr-taze --recursive --request-timeout 90000' } in .config/nmr.config.ts, keeping the rest of the default script \u2014 taze's CLI writes a default for each of them over whatever the config file declares, so the value there never reaches taze (antfu-collective/taze#317). nmr already forwards a 30-second request timeout"
|
|
287
|
+
},
|
|
281
288
|
// -- Audit dependency --------------------------------------------------------
|
|
282
289
|
{
|
|
283
290
|
name: "v11y-check in devDependencies",
|
|
@@ -311,6 +318,13 @@ var SHARED_PRETTIER_MODULE = "@williamthorsen/nmr/prettier";
|
|
|
311
318
|
var INERT_PRETTIER_CONFIGS = [".prettierrc", ".prettierrc.{json,json5,yaml,yml,toml}"];
|
|
312
319
|
var SHARED_TAZE_MODULE = "@williamthorsen/nmr/taze";
|
|
313
320
|
var INERT_TAZE_CONFIGS = [".tazerc", ".tazerc.json", "taze.config.json"];
|
|
321
|
+
var CLOBBERED_TAZE_OPTIONS = [
|
|
322
|
+
{ key: "concurrency", pattern: /\bconcurrency\s*:/ },
|
|
323
|
+
{ key: "githubActions", pattern: /\bgithubActions\s*:\s*(?:false|\{)/ },
|
|
324
|
+
{ key: "ignoreOtherWorkspaces", pattern: /\bignoreOtherWorkspaces\s*:\s*false/ },
|
|
325
|
+
{ key: "nodeVersion", pattern: /\bnodeVersion\s*:\s*false/ },
|
|
326
|
+
{ key: "requestTimeout", pattern: /\brequestTimeout\s*:/ }
|
|
327
|
+
];
|
|
314
328
|
var RE_EXPORT_LINE_PATTERN = /^export\s*(?:\{\s*default\s*}|\*)\s*from\s*['"]\.\.\/[^'"]*['"];?$/;
|
|
315
329
|
var MIN_ESLINT_VERSION = "10.0.0";
|
|
316
330
|
var MIN_STRICT_LINT_VERSION = "9.3.0";
|
|
@@ -402,7 +416,7 @@ function formatPaths(paths) {
|
|
|
402
416
|
${paths.map((path2) => ` ${path2}`).join("\n")}`;
|
|
403
417
|
}
|
|
404
418
|
function getMinVersion() {
|
|
405
|
-
const picked = { "version": "0.
|
|
419
|
+
const picked = { "version": "0.34.0" };
|
|
406
420
|
if (typeof picked["version"] !== "string") {
|
|
407
421
|
throw new TypeError("nmr/package.json: 'version' must be a string");
|
|
408
422
|
}
|
|
@@ -482,11 +496,7 @@ function noWorkspaceRunScriptReferences() {
|
|
|
482
496
|
};
|
|
483
497
|
}
|
|
484
498
|
function readFileIn(cwd, relativePath) {
|
|
485
|
-
|
|
486
|
-
return readFileSync(join(cwd, relativePath), "utf8");
|
|
487
|
-
} catch {
|
|
488
|
-
return void 0;
|
|
489
|
-
}
|
|
499
|
+
return readFile(join(cwd, relativePath));
|
|
490
500
|
}
|
|
491
501
|
function readPnpmFieldKeys(content) {
|
|
492
502
|
if (content === void 0) return void 0;
|
|
@@ -514,6 +524,18 @@ function tazeConfigBuildsOnSharedConfig(cwd = process.cwd()) {
|
|
|
514
524
|
if (stale.length === 0) return true;
|
|
515
525
|
return { ok: false, detail: `does not import defineConfig from ${SHARED_TAZE_MODULE}: ${stale.join(", ")}` };
|
|
516
526
|
}
|
|
527
|
+
function tazeConfigAvoidsClobberedOptions(cwd = process.cwd()) {
|
|
528
|
+
const configs = findFiles([`taze.config.${CONFIG_EXTENSIONS}`], cwd);
|
|
529
|
+
const findings = [];
|
|
530
|
+
for (const relativePath of configs) {
|
|
531
|
+
const content = readFileIn(cwd, relativePath);
|
|
532
|
+
if (content === void 0) continue;
|
|
533
|
+
const discarded = CLOBBERED_TAZE_OPTIONS.filter(({ pattern }) => pattern.test(content)).map(({ key }) => key);
|
|
534
|
+
if (discarded.length > 0) findings.push(`${relativePath}: ${discarded.join(", ")}`);
|
|
535
|
+
}
|
|
536
|
+
if (findings.length === 0) return true;
|
|
537
|
+
return { ok: false, detail: formatPaths(findings) };
|
|
538
|
+
}
|
|
517
539
|
function describeMissingTazeConfig(cwd) {
|
|
518
540
|
const inert = findFiles(INERT_TAZE_CONFIGS, cwd);
|
|
519
541
|
if (inert.length > 0) return `holds no code to call the factory: ${inert.join(", ")}`;
|
|
@@ -539,6 +561,7 @@ export {
|
|
|
539
561
|
noReExportOnlyVitestConfigs,
|
|
540
562
|
noRetiredVitestConfigs,
|
|
541
563
|
prettierConfigBuildsOnSharedConfig,
|
|
564
|
+
tazeConfigAvoidsClobberedOptions,
|
|
542
565
|
tazeConfigBuildsOnSharedConfig,
|
|
543
566
|
vitestConfigBuildsOnSharedConfig,
|
|
544
567
|
vitestRootConfigBuildsOnSharedConfig
|
package/.readyup/manifest.json
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"esbuildVersion": "0.28.1",
|
|
9
9
|
"inputs": [
|
|
10
10
|
{
|
|
11
|
-
"hash": "
|
|
11
|
+
"hash": "f58e8934",
|
|
12
12
|
"kind": "module",
|
|
13
13
|
"path": "kits/default.ts"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
"hash": "
|
|
16
|
+
"hash": "cbbd2a2c",
|
|
17
17
|
"kind": "inline",
|
|
18
18
|
"path": "../package.json",
|
|
19
19
|
"paths": [
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
]
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
|
-
"hash": "
|
|
24
|
+
"hash": "74bd7994",
|
|
25
25
|
"kind": "module",
|
|
26
26
|
"path": "../src/default-scripts.ts"
|
|
27
27
|
},
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
],
|
|
39
39
|
"name": "default",
|
|
40
40
|
"path": "kits/default.js",
|
|
41
|
-
"readyupVersion": "0.
|
|
41
|
+
"readyupVersion": "0.32.0",
|
|
42
42
|
"source": "kits/default.ts",
|
|
43
|
-
"sourceHash": "
|
|
44
|
-
"targetHash": "
|
|
43
|
+
"sourceHash": "f58e8934",
|
|
44
|
+
"targetHash": "debdcb6e"
|
|
45
45
|
}
|
|
46
46
|
]
|
|
47
47
|
}
|
package/README.md
CHANGED
|
@@ -3,19 +3,25 @@
|
|
|
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.34.0 (2026-08-24)
|
|
7
7
|
|
|
8
|
-
###
|
|
8
|
+
### 🎉 Features
|
|
9
|
+
|
|
10
|
+
- 🚨 **Breaking:** Make defineVitestConfig resolve from source and isolate git (#749)
|
|
11
|
+
|
|
12
|
+
Adds two defaults to `defineVitestConfig` and `defineRootVitestConfig` in `@williamthorsen/nmr/vitest`: Workspace packages resolve through their `source` export condition, so a suite runs without a prior build; and every project loads a setup file that keeps the developer's git configuration out of the subprocesses a test spawns.
|
|
9
13
|
|
|
10
|
-
-
|
|
14
|
+
Each is individually opt-out-able through the new `isolateGit` and `resolveFromSource` options. A consumer's own `resolve.conditions` and `setupFiles` compose alongside what the factory supplies.
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
Migration: A workspace package declaring a `source` condition is resolved from source in tests that previously read its `dist`. A package that publishes only its build output takes a bespoke condition name instead, as `@williamthorsen/nmr-core` does with `nmr-source`.
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
### 🐛 Bug fixes
|
|
19
|
+
|
|
20
|
+
- Stop nmr upgrade from timing out against a proxying registry (#744)
|
|
15
21
|
|
|
16
|
-
Fixes an issue where `nmr upgrade`
|
|
22
|
+
Fixes an issue where `nmr upgrade` timed out intermittently against a registry that proxies npm. Every `upgrade` script now calls taze with a 30-second request timeout in place of the 5-second default; a different request timeout specified by the caller is respected.
|
|
17
23
|
|
|
18
|
-
The
|
|
24
|
+
The ReadyUp kit bundled with nmr now warns when a taze config sets a value that the taze CLI will not use.
|
|
19
25
|
<!-- /section:release-notes -->
|
|
20
26
|
|
|
21
27
|
## Installation
|
|
@@ -311,7 +317,7 @@ A check that passed on a working tree passes again on the same working tree. nmr
|
|
|
311
317
|
|
|
312
318
|
```console
|
|
313
319
|
$ nmr ci
|
|
314
|
-
# ... four minutes of build, typecheck, lint, and
|
|
320
|
+
# ... four minutes of build, typecheck, lint, and tests ...
|
|
315
321
|
|
|
316
322
|
$ nmr ci
|
|
317
323
|
⏭️ node-monorepo-tools: ci: passed 2m ago on this tree, saved ~4m
|
|
@@ -406,7 +412,7 @@ $ nmr check
|
|
|
406
412
|
⏭️ nmr-core: check: passed 3m ago on this tree, saved ~48s — replayed: nmr-core: fmt:check: All matched files use Prettier code style!; nmr-core: test: Test Files 6 passed (6) Tests 41 passed (41)
|
|
407
413
|
```
|
|
408
414
|
|
|
409
|
-
The assembly is flat: a constituent that is itself a composite contributes its own leaves' lines, so a skipped `ci` replays a package's `test
|
|
415
|
+
The assembly is flat: a constituent that is itself a composite contributes its own leaves' lines, so a skipped `ci` replays a package's `test` rather than one opaque `check:strict` line. A delegate expands into the scopes it fans out to, and a constituent that recorded no excerpt is absent rather than inferred: `typecheck` and `lint:check` print nothing on success and contribute nothing above, and a command outside the [cacheable set](#what-is-cached) records nothing at all, so `ci` replays what `check:strict` earned and nothing for `build`.
|
|
410
416
|
|
|
411
417
|
It is assembled when the composite records its pass rather than when it skips, so every excerpt in it was certified during that one run, on that one tree. `NMR_RUN_ID` is what makes that provable; see [reserved environment variables](#reserved-environment-variables).
|
|
412
418
|
|
|
@@ -560,7 +566,7 @@ export default defineConfig({
|
|
|
560
566
|
|
|
561
567
|
`defineConfig` supplies nmr's shared upgrade policy — a seven-day quarantine on brand-new releases, and the pair of settings that report a dependency pinned to an exact version: locked dependencies are included, and the range searched is `minor` — so your config carries only what is specific to your repo. Any setting you declare wins over nmr's default. Passing `undefined` clears a default rather than falling back to it, which is how you hand the quarantine policy back to `pnpm-workspace.yaml`'s `minimumReleaseAge`. `mode` is the exception: the upgrade tool carries a default of its own for it, and clearing nmr's leaves no valid range to search, so name a mode instead.
|
|
562
568
|
|
|
563
|
-
Everything the [taze configuration](https://github.com/antfu-collective/taze#config-file) accepts is accepted here.
|
|
569
|
+
Everything the [taze configuration](https://github.com/antfu-collective/taze#config-file) accepts is accepted here, with five exceptions that reach the upgrade tool from no config file at all: `concurrency`, `githubActions`, `ignoreOtherWorkspaces`, `nodeVersion`, and `requestTimeout`. The tool's CLI writes a default for each of them over whatever the config declares ([taze#317](https://github.com/antfu-collective/taze/issues/317)), so the command line is the only place they can be set. nmr's ReadyUp kit warns when your config declares one, and [request timeout](#request-timeout) below shows how to set it instead.
|
|
564
570
|
|
|
565
571
|
> **Note:** This file is what activates nmr's upgrade policy at all. A repo without one gets neither the release quarantine nor the settings that report a dependency pinned to an exact version, and `nmr upgrade` reports nothing in a repo using pnpm's `savePrefix: ''`. nmr's ReadyUp kit warns when the file is missing.
|
|
566
572
|
|
|
@@ -569,6 +575,32 @@ Two consequences of the `minor` range mode the policy declares:
|
|
|
569
575
|
- A `packageMode` entry is honored only by the pass whose mode matches it, or by a `default` pass; every other pass drops that dependency rather than narrowing it. Under the policy's `minor` mode, a `patch` ceiling hides its dependency until you run `nmr upgrade patch`, and a `minor` ceiling hides it from `nmr upgrade major`.
|
|
570
576
|
- A `~` range is searched as `^`, so minor upgrades are proposed for it and `--write` applies them under the original `~`. To hold one dependency to patches, give it a `packageMode` entry of `patch` and read it from `nmr upgrade patch`; to hold the whole repo there, declare `mode: 'patch'`.
|
|
571
577
|
|
|
578
|
+
### Request timeout
|
|
579
|
+
|
|
580
|
+
Every `upgrade` script reaches the upgrade tool with a 30-second request timeout, in place of the tool's own 5 seconds.
|
|
581
|
+
|
|
582
|
+
Five seconds is too tight for a registry that proxies npm. The tool fetches a full packument whenever the resolved registry is not `registry.npmjs.org`, and a large one runs to tens of megabytes; the budget covers the whole retry chain rather than a single attempt, so one slow response ends the run with `Timeout requesting "<package>"`. The symptom is an upgrade pass that succeeds or fails according to how warm the tool's cache happens to be.
|
|
583
|
+
|
|
584
|
+
Pass a different value for one run:
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
nmr upgrade --request-timeout 90000
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
To change it for every run, override the `upgrade` script in `.config/nmr.config.ts`, keeping the rest of the default. This is nmr's own config, not the `taze.config.ts` above; taze ignores a `rootScripts` key, and nothing reports it:
|
|
591
|
+
|
|
592
|
+
```ts
|
|
593
|
+
import { defineConfig } from '@williamthorsen/nmr/config';
|
|
594
|
+
|
|
595
|
+
export default defineConfig({
|
|
596
|
+
rootScripts: {
|
|
597
|
+
upgrade: 'nmr-report-overrides && nmr-taze --recursive --request-timeout 90000',
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
The override is one command string rather than a list of steps, because a composite element names an nmr command and both halves here are binaries.
|
|
603
|
+
|
|
572
604
|
## Default script registries
|
|
573
605
|
|
|
574
606
|
These scripts are available out of the box. Repo-wide config (tier 2) and per-package overrides (tier 3) can add to or replace any of them.
|
|
@@ -579,7 +611,7 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
|
|
|
579
611
|
| ---------------- | ----------------------------------------------------------- |
|
|
580
612
|
| `build` | `compile` |
|
|
581
613
|
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
582
|
-
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test
|
|
614
|
+
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test` |
|
|
583
615
|
| `clean` | `nmr-clean` |
|
|
584
616
|
| `compile` | `nmr-compile` |
|
|
585
617
|
| `fix` | `lint`, `fmt` |
|
|
@@ -640,10 +672,12 @@ Neither is bound to a git hook. `prepush` is named for when you run it, not for
|
|
|
640
672
|
|
|
641
673
|
#### Check and quality
|
|
642
674
|
|
|
643
|
-
| Command | Runs
|
|
644
|
-
| -------------- |
|
|
645
|
-
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test`
|
|
646
|
-
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test
|
|
675
|
+
| Command | Runs |
|
|
676
|
+
| -------------- | ----------------------------------------------- |
|
|
677
|
+
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
678
|
+
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test` |
|
|
679
|
+
|
|
680
|
+
Neither gate computes coverage: Instrumentation multiplies the wall time of the run they exist to keep short. Coverage is [`test:coverage`](#test-selections), run as a step of its own; a repo that wants it inside the gate [overrides](#configuration) `check:strict`.
|
|
647
681
|
|
|
648
682
|
#### Fix
|
|
649
683
|
|
|
@@ -879,7 +913,7 @@ nmr-fmt --write packages/nmr
|
|
|
879
913
|
|
|
880
914
|
### `nmr-taze`
|
|
881
915
|
|
|
882
|
-
Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched. This is what every `upgrade` chain ends with, `root:upgrade` included — see [dependency upgrades](#dependency-upgrades) for the workflow.
|
|
916
|
+
Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched and adding one of its own: a 30-second request timeout, unless the invocation already carries one. This is what every `upgrade` chain ends with, `root:upgrade` included — see [dependency upgrades](#dependency-upgrades) for the workflow, and [request timeout](#request-timeout) for why the timeout is set here.
|
|
883
917
|
|
|
884
918
|
Under pnpm's isolated `node_modules`, a transitive package's binary is absent from the consuming repo's `node_modules/.bin`, so a repo that depends on nmr cannot run `taze` directly. `nmr-taze` can, because nmr is a direct dependency, and it resolves the tool from the tree nmr controls.
|
|
885
919
|
|
|
@@ -1060,6 +1094,25 @@ Every tier above `unit` carries a 30-second `testTimeout` and `hookTimeout`, whe
|
|
|
1060
1094
|
|
|
1061
1095
|
To raise a budget for one tier and no other, use the `tiers` seam below. The `project` seam merges over both budgets as well, but like every option passed through it the value reaches all four projects at once -- raising a tier's budget raises `unit`'s with it. To lift the ceiling for a single file rather than a whole tier, pass a timeout to the individual test or hook, which stays the narrower tool.
|
|
1062
1096
|
|
|
1097
|
+
### What the config supplies
|
|
1098
|
+
|
|
1099
|
+
Two settings a workspace test runner needs almost universally are part of what the factory produces, so a config file declares neither:
|
|
1100
|
+
|
|
1101
|
+
- **Workspace packages resolve from source.** `source` leads the emitted `resolve.conditions` and `ssr.resolve.conditions`, so a cross-package import reaches the dependency's `.ts` rather than its `dist/` and a suite runs without a prior build. A package declaring no such condition is unaffected.
|
|
1102
|
+
- **Git subprocesses are isolated.** Every project loads a setup file that points `GIT_CONFIG_GLOBAL` and `GIT_CONFIG_SYSTEM` at the null device and sets `GIT_CONFIG_NOSYSTEM`, so a test that spawns `git` reads neither the developer's identity nor a signing config that can block on a passphrase. It leads each project's `setupFiles`, ahead of every entry the layers add, because a shared setup establishes the environment the rest run in.
|
|
1103
|
+
|
|
1104
|
+
Both defaults exist because omitting either is silent rather than loud: the tiers still exist and the suite still runs green, testing `dist/` and the developer's git identity instead. Turn either off through its own option, which folds across layers like any other, the last layer to declare it winning:
|
|
1105
|
+
|
|
1106
|
+
```ts
|
|
1107
|
+
export default defineVitestConfig({ isolateGit: false, resolveFromSource: false });
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
`resolveFromSource: false` is for a repo that resolves a `source` condition it does not want in tests; `isolateGit: false` is for a suite meant to read the developer's own git configuration.
|
|
1111
|
+
|
|
1112
|
+
**Vite replaces its condition defaults rather than extending them.** A config writing `conditions: ['source']` by hand therefore drops `module`, so a dependency exposing a `module` entry falls through to whatever its `exports` lists next. A replaced list still resolves `node` and `development` under Vitest, which is why nothing in a test run reports the loss. The factory emits `source` alongside Vite's defaults for each environment, so a config declaring no conditions of its own starts from a complete list.
|
|
1113
|
+
|
|
1114
|
+
**A package declaring a `source` condition must ship its sources.** Every nmr consumer now resolves that condition, so a published package whose `files` carries `dist` alone resolves to a path that is not in the tarball. A package that publishes only its build output takes a bespoke condition name instead, the way `@williamthorsen/nmr-core` uses `nmr-source` for the build's own bootstrap.
|
|
1115
|
+
|
|
1063
1116
|
### Customizing by scope
|
|
1064
1117
|
|
|
1065
1118
|
Vitest applies some options at the root of a `projects` config and others per project, and placing one at the wrong level is silent rather than loud. The factory therefore takes separate override surfaces instead of one merged config:
|
|
@@ -1067,7 +1120,7 @@ Vitest applies some options at the root of a `projects` config and others per pr
|
|
|
1067
1120
|
```ts
|
|
1068
1121
|
export default defineVitestConfig({
|
|
1069
1122
|
// Vite-level options, plus the test options Vitest honours only at the root.
|
|
1070
|
-
root: { resolve: { conditions: ['
|
|
1123
|
+
root: { ssr: { resolve: { conditions: ['my-condition'] } } },
|
|
1071
1124
|
// Applied to every project.
|
|
1072
1125
|
project: { setupFiles: ['./vitest.setup.ts'] },
|
|
1073
1126
|
// Applied to one tier, after the `project` block above.
|
|
@@ -1081,7 +1134,9 @@ export default defineVitestConfig({
|
|
|
1081
1134
|
|
|
1082
1135
|
Arrays concatenate rather than replace. `exclude` and `setupFiles` therefore add to what the config already declares, and no surface can narrow `include` or drop a default exclusion. Adding an `include` pattern through `project` widens all four projects at once, so a file matching it is collected by each and runs four times.
|
|
1083
1136
|
|
|
1084
|
-
`resolve.conditions` concatenates too, but layer order carries no meaning there: Vite consumes conditions as a set, and which one wins is decided by the key order of the consumed package's own `exports`. A later layer can add a condition and can never remove or outrank one an earlier layer contributed. `resolve.alias` is the one key that merges override-first, so a later alias takes precedence over an earlier one.
|
|
1137
|
+
`resolve.conditions` concatenates too, onto the `source` condition and the Vite defaults the factory already emits, but layer order carries no meaning there: Vite consumes conditions as a set, and which one wins is decided by the key order of the consumed package's own `exports`. A later layer can add a condition and can never remove or outrank one an earlier layer contributed; removing `source` is what `resolveFromSource: false` is for. `resolve.alias` is the one key that merges override-first, so a later alias takes precedence over an earlier one.
|
|
1138
|
+
|
|
1139
|
+
**A condition for the tests' own resolution goes under `ssr`.** `resolve` is per-environment, and Vitest resolves a test's imports through the server environment, so `root: { ssr: { resolve: { conditions: ['my-condition'] } } }` is the seam that reaches them. A top-level `root: { resolve: { conditions: [...] } }` entry reaches the client environment, which only browser-mode tests resolve through; it composes into that array and never reports that a node test did not see it. `resolve.alias` is not per-environment, so a top-level alias reaches both.
|
|
1085
1140
|
|
|
1086
1141
|
### Sharing options across config files
|
|
1087
1142
|
|
|
@@ -1094,7 +1149,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
1094
1149
|
import type { VitestConfigOptions } from '@williamthorsen/nmr/vitest';
|
|
1095
1150
|
|
|
1096
1151
|
export const shared: VitestConfigOptions = {
|
|
1097
|
-
root: { resolve: {
|
|
1152
|
+
root: { resolve: { alias: { '~': fileURLToPath(new URL('.', import.meta.url)) } } },
|
|
1098
1153
|
project: { setupFiles: [fileURLToPath(new URL('./vitest.setup.ts', import.meta.url))] },
|
|
1099
1154
|
};
|
|
1100
1155
|
```
|
|
@@ -1116,7 +1171,7 @@ Order is the point where `setupFiles` is concerned, since a shared setup file es
|
|
|
1116
1171
|
|
|
1117
1172
|
**Do not merge two returned configs.** `mergeConfig(defineVitestConfig(), defineVitestConfig(mine))` looks like the idiomatic recovery and fails at startup: both sides declare the same four project names, which Vitest rejects with `Project name "unit" ... is not unique`. Layers merge the factory's inputs instead, which is why they yield four projects however many fold.
|
|
1118
1173
|
|
|
1119
|
-
A config file that omits the shared layer still loses those settings, silently -- Vitest's own resolution contract, not something the factory can intercept. Guard it with a test that fails when a package's suite goes missing, which also catches a shared `exclude` pattern swallowing one package's tests.
|
|
1174
|
+
A config file that omits the shared layer still loses those settings, silently -- Vitest's own resolution contract, not something the factory can intercept. Guard it with a test that fails when a package's suite goes missing, which also catches a shared `exclude` pattern swallowing one package's tests. Source resolution and git isolation need no such guard: the factory supplies both, so a config file cannot omit them by forgetting a layer.
|
|
1120
1175
|
|
|
1121
1176
|
### What the config excludes
|
|
1122
1177
|
|
|
@@ -1173,3 +1228,17 @@ nmr once selected a package's test scripts by looking for a `vitest.integration.
|
|
|
1173
1228
|
Until step 1 is done, every test command that names a tier fails against a config declaring no projects, with `Error: No projects matched the filter "unit", "tool".` at startup. `test:all` is the exception: it names no project, so it runs everything the config collects.
|
|
1174
1229
|
|
|
1175
1230
|
That is a change for the better. While `test` selected by negation, an unmigrated config produced a _green_ run, because `--project '!integration'` had nothing to exclude, so the separation was lost with no error anywhere. Positive selection turns the same state into a startup failure that names the missing projects.
|
|
1231
|
+
|
|
1232
|
+
### Migrating a repo that layered source resolution or git isolation by hand
|
|
1233
|
+
|
|
1234
|
+
The factory supplies both, so the layer a repo wrote for them, and any guard test that checked every config file imported it, can go.
|
|
1235
|
+
|
|
1236
|
+
1. Delete the shared module's `resolve.conditions` entry for `source` and its `ssr.resolve` twin. Keep the module for whatever else it carries; delete it outright once it carries nothing.
|
|
1237
|
+
2. Delete the git-isolation setup file and its `setupFiles` entry. nmr's runs in its place, ahead of every entry a layer adds.
|
|
1238
|
+
3. Delete the test that asserted each config file imports the shared module, where the module is gone.
|
|
1239
|
+
|
|
1240
|
+
Two behaviors change on upgrade even in a repo that layered neither.
|
|
1241
|
+
|
|
1242
|
+
A suite whose tests run `git commit` while relying on the developer's global identity now fails with git's own `Please tell me who you are`. Set the identity locally in the fixture repo the test builds -- `git -C <repo> config user.email`, `user.name`, and `commit.gpgsign false` -- which is what makes the test report the same result on every machine.
|
|
1243
|
+
|
|
1244
|
+
A workspace package declaring a `source` condition is now resolved from source in tests that previously read its `dist`. That is the point of the default, and it means a suite stops exercising build output it may have been exercising by accident.
|
|
@@ -4,6 +4,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
5
5
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
6
6
|
const TAZE_CLI_SPECIFIER = 'taze/cli';
|
|
7
|
+
const REQUEST_TIMEOUT_FLAG = '--request-timeout';
|
|
8
|
+
const REQUEST_TIMEOUT_FLAGS = [REQUEST_TIMEOUT_FLAG, '--requestTimeout'];
|
|
9
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
10
|
+
const ARGUMENT_TERMINATOR = '--';
|
|
7
11
|
export function runTaze(args, options = {}) {
|
|
8
12
|
const stderr = options.stderr ?? process.stderr;
|
|
9
13
|
const resolveCliPath = options.resolveCliPath ?? resolveTazeCliPath;
|
|
@@ -17,7 +21,7 @@ export function runTaze(args, options = {}) {
|
|
|
17
21
|
reportError(`Could not resolve '${TAZE_CLI_SPECIFIER}' from nmr's dependencies. Reinstall the workspace to restore it. (${detail})`, stderr);
|
|
18
22
|
return 1;
|
|
19
23
|
}
|
|
20
|
-
const outcome = spawn(process.execPath, [cliPath, ...args]);
|
|
24
|
+
const outcome = spawn(process.execPath, [cliPath, ...composeRequestTimeoutArgs(args), ...args]);
|
|
21
25
|
if (outcome.error) {
|
|
22
26
|
reportError(`Failed to run taze: ${outcome.error.message}`, stderr);
|
|
23
27
|
return 1;
|
|
@@ -27,6 +31,18 @@ export function runTaze(args, options = {}) {
|
|
|
27
31
|
export function resolveTazeCliPath() {
|
|
28
32
|
return fileURLToPath(import.meta.resolve(TAZE_CLI_SPECIFIER));
|
|
29
33
|
}
|
|
34
|
+
function carriesRequestTimeout(args) {
|
|
35
|
+
for (const arg of args) {
|
|
36
|
+
if (arg === ARGUMENT_TERMINATOR)
|
|
37
|
+
return false;
|
|
38
|
+
if (REQUEST_TIMEOUT_FLAGS.some((flag) => arg === flag || arg.startsWith(`${flag}=`)))
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function composeRequestTimeoutArgs(args) {
|
|
44
|
+
return carriesRequestTimeout(args) ? [] : [REQUEST_TIMEOUT_FLAG, String(REQUEST_TIMEOUT_MS)];
|
|
45
|
+
}
|
|
30
46
|
function spawnNode(nodePath, argv) {
|
|
31
47
|
const result = spawnSync(nodePath, argv, { stdio: 'inherit' });
|
|
32
48
|
return { status: result.status, error: result.error };
|
|
@@ -4,7 +4,7 @@ const ROOT_TYPECHECK_STEP = { run: 'root:typecheck', declinesArgs: true };
|
|
|
4
4
|
export const workspaceScripts = {
|
|
5
5
|
build: ['compile'],
|
|
6
6
|
check: [TYPECHECK_STEP, 'fmt:check', 'lint:check', 'test'],
|
|
7
|
-
'check:strict': [TYPECHECK_STEP, 'fmt:check', 'lint:strict', 'test
|
|
7
|
+
'check:strict': [TYPECHECK_STEP, 'fmt:check', 'lint:strict', 'test'],
|
|
8
8
|
clean: 'nmr-clean',
|
|
9
9
|
compile: 'nmr-compile',
|
|
10
10
|
fix: ['lint', 'fmt'],
|
|
@@ -31,7 +31,7 @@ export const rootScripts = {
|
|
|
31
31
|
'audit:prod': 'pnpm exec v11y --prod',
|
|
32
32
|
build: ['-R build'],
|
|
33
33
|
check: [TYPECHECK_STEP, 'fmt:check', 'lint:check', 'test'],
|
|
34
|
-
'check:strict': [TYPECHECK_STEP, 'fmt:check', 'lint:strict', 'test
|
|
34
|
+
'check:strict': [TYPECHECK_STEP, 'fmt:check', 'lint:strict', 'test'],
|
|
35
35
|
ci: [{ run: 'build', declinesArgs: true }, 'check:strict'],
|
|
36
36
|
clean: 'nmr-clean',
|
|
37
37
|
fix: ['lint', 'fmt'],
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/esm/vitest.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface VitestConfigOptions {
|
|
|
10
10
|
root?: RootOverrides;
|
|
11
11
|
project?: ProjectConfig;
|
|
12
12
|
tiers?: Partial<Record<TierName, ProjectConfig>>;
|
|
13
|
+
isolateGit?: boolean;
|
|
14
|
+
resolveFromSource?: boolean;
|
|
13
15
|
}
|
|
14
16
|
export interface RootVitestConfigOptions extends VitestConfigOptions {
|
|
15
17
|
monorepoRoot: string;
|
package/dist/esm/vitest.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
2
3
|
import { defaultExclude, mergeConfig } from 'vitest/config';
|
|
3
4
|
import { isObject } from "./helpers/type-guards.js";
|
|
4
5
|
import { ALL_TEST_PATTERNS, buildTierPatterns, TEST_COLLECTION_EXCLUDE, TIER_NAMES } from "./tiers.js";
|
|
@@ -6,6 +7,9 @@ import { getWorkspacePackageDirs } from "./workspace.js";
|
|
|
6
7
|
const [RESIDUAL_TIER, ...NAMED_TIERS] = TIER_NAMES;
|
|
7
8
|
const TIERED_PATTERNS = NAMED_TIERS.flatMap(buildTierPatterns);
|
|
8
9
|
const TIER_TIMEOUT = 30_000;
|
|
10
|
+
const GIT_ISOLATION_SETUP_FILE = resolveGitIsolationSetupFile();
|
|
11
|
+
const SOURCE_CLIENT_CONDITIONS = ['source', 'module', 'browser', 'development|production'];
|
|
12
|
+
const SOURCE_SERVER_CONDITIONS = ['source', 'module', 'node', 'development|production'];
|
|
9
13
|
const COVERAGE_EXCLUDE = ['**/__{fixtures,mocks,tests}__/**', '**/index.ts', '**/*.d.ts'];
|
|
10
14
|
const COLLECTION_EXCLUDE = [...new Set([...defaultExclude, ...TEST_COLLECTION_EXCLUDE.map((dir) => `**/${dir}/**`)])];
|
|
11
15
|
const PACKAGE_COVERAGE_INCLUDE = ['**/src/**/*.{ts,tsx}'];
|
|
@@ -29,6 +33,10 @@ function buildConfig(declaredLayers, { coverageInclude, projectExclude = [], pro
|
|
|
29
33
|
const layers = declaredLayers.filter((layer) => layer !== undefined);
|
|
30
34
|
assertKnownTiers(layers);
|
|
31
35
|
const config = {
|
|
36
|
+
...(resolveFlag(layers, 'resolveFromSource') && {
|
|
37
|
+
resolve: { conditions: SOURCE_CLIENT_CONDITIONS },
|
|
38
|
+
ssr: { resolve: { conditions: SOURCE_SERVER_CONDITIONS } },
|
|
39
|
+
}),
|
|
32
40
|
test: {
|
|
33
41
|
coverage: {
|
|
34
42
|
enabled: false,
|
|
@@ -59,6 +67,7 @@ function buildProjects(layers, extraExclude, projectRoot) {
|
|
|
59
67
|
timeout: TIER_TIMEOUT,
|
|
60
68
|
})),
|
|
61
69
|
];
|
|
70
|
+
const isolateGit = resolveFlag(layers, 'isolateGit');
|
|
62
71
|
return projectTiers.map(({ exclude, include, name, timeout }) => {
|
|
63
72
|
const project = {
|
|
64
73
|
extends: true,
|
|
@@ -67,6 +76,7 @@ function buildProjects(layers, extraExclude, projectRoot) {
|
|
|
67
76
|
exclude: [...COLLECTION_EXCLUDE, ...exclude, ...extraExclude],
|
|
68
77
|
include,
|
|
69
78
|
name,
|
|
79
|
+
...(isolateGit && { setupFiles: [GIT_ISOLATION_SETUP_FILE] }),
|
|
70
80
|
...(timeout !== undefined && { hookTimeout: timeout, testTimeout: timeout }),
|
|
71
81
|
},
|
|
72
82
|
};
|
|
@@ -98,3 +108,16 @@ function getWorkspaceExcludePatterns(monorepoRoot) {
|
|
|
98
108
|
.map((dir) => `${path.relative(monorepoRoot, dir).split(path.sep).join('/')}/**`)
|
|
99
109
|
.toSorted();
|
|
100
110
|
}
|
|
111
|
+
function resolveFlag(layers, name) {
|
|
112
|
+
let resolved = true;
|
|
113
|
+
for (const layer of layers) {
|
|
114
|
+
const declared = layer[name];
|
|
115
|
+
if (declared !== undefined)
|
|
116
|
+
resolved = declared;
|
|
117
|
+
}
|
|
118
|
+
return resolved;
|
|
119
|
+
}
|
|
120
|
+
function resolveGitIsolationSetupFile() {
|
|
121
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
122
|
+
return path.join(path.dirname(thisFile), `vitest-git-isolation${path.extname(thisFile)}`);
|
|
123
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for pnpm monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"license": "ISC",
|
|
22
22
|
"author": "William Thorsen <william@thorsen.dev> (https://github.com/williamthorsen)",
|
|
23
23
|
"sideEffects": [
|
|
24
|
-
"./dist/esm/cli*.js"
|
|
24
|
+
"./dist/esm/cli*.js",
|
|
25
|
+
"./dist/esm/vitest-git-isolation.js"
|
|
25
26
|
],
|
|
26
27
|
"type": "module",
|
|
27
28
|
"exports": {
|
|
@@ -68,21 +69,22 @@
|
|
|
68
69
|
"dist"
|
|
69
70
|
],
|
|
70
71
|
"dependencies": {
|
|
71
|
-
"@williamthorsen/toolbelt.errors": "0.
|
|
72
|
+
"@williamthorsen/toolbelt.errors": "0.6.1",
|
|
72
73
|
"glob": "13.0.6",
|
|
73
74
|
"prettier-plugin-sh": "0.19.0",
|
|
74
|
-
"taze": "
|
|
75
|
+
"taze": "21.1.0",
|
|
75
76
|
"yaml": "2.9.0",
|
|
76
|
-
"@williamthorsen/nmr-core": "0.10.
|
|
77
|
+
"@williamthorsen/nmr-core": "0.10.3"
|
|
77
78
|
},
|
|
78
79
|
"devDependencies": {
|
|
79
|
-
"@williamthorsen/toolbelt.filesystem": "0.
|
|
80
|
-
"@williamthorsen/toolbelt.testing": "0.
|
|
81
|
-
"@williamthorsen/toolbelt.vitest": "0.
|
|
80
|
+
"@williamthorsen/toolbelt.filesystem": "0.8.0",
|
|
81
|
+
"@williamthorsen/toolbelt.testing": "0.4.0",
|
|
82
|
+
"@williamthorsen/toolbelt.vitest": "0.6.0",
|
|
82
83
|
"prettier": "3.9.6",
|
|
83
|
-
"readyup": "0.
|
|
84
|
+
"readyup": "0.32.0",
|
|
84
85
|
"tinyglobby": "0.2.17",
|
|
85
86
|
"typescript": "6.0.3",
|
|
87
|
+
"vite": "8.2.1",
|
|
86
88
|
"vitest": "4.1.10"
|
|
87
89
|
},
|
|
88
90
|
"peerDependencies": {
|