@williamthorsen/nmr 0.45.0 → 0.46.1
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/.meta/changelog.json +44 -0
- package/.readyup/kits/default.js +1 -1
- package/.readyup/manifest.json +3 -3
- package/CHANGELOG.md +14 -0
- package/README.md +3 -13
- package/agents/guidance/rulebooks/nmr.md +1 -1
- package/dist/esm/bin-path.d.ts +12 -0
- package/dist/esm/bin-path.js +21 -0
- package/dist/esm/cli-taze.js +1 -1
- package/dist/esm/commands/load-package-maturity-period.d.ts +8 -0
- package/dist/esm/commands/load-package-maturity-period.js +15 -0
- package/dist/esm/commands/taze-args.d.ts +17 -0
- package/dist/esm/commands/taze-args.js +25 -0
- package/dist/esm/commands/taze-passes.d.ts +18 -0
- package/dist/esm/commands/taze-passes.js +59 -0
- package/dist/esm/commands/taze.d.ts +15 -9
- package/dist/esm/commands/taze.js +31 -16
- package/dist/esm/runCli.js +7 -1
- package/dist/esm/taze.d.ts +7 -0
- package/dist/esm/tiers.d.ts +6 -5
- package/docs/scripts.md +5 -1
- package/docs/upgrades.md +29 -1
- package/docs/vitest.md +12 -10
- package/package.json +4 -3
package/.meta/changelog.json
CHANGED
|
@@ -1,4 +1,48 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.46.1",
|
|
4
|
+
"date": "2026-10-13",
|
|
5
|
+
"sections": [
|
|
6
|
+
{
|
|
7
|
+
"title": "🐛 Bug fixes",
|
|
8
|
+
"audience": "all",
|
|
9
|
+
"items": [
|
|
10
|
+
{
|
|
11
|
+
"description": "Fixes the issue that a script run by bare `nmr` inside a package failed with `command not found` on a binary installed only in that package, and found the root's binaries, such as `eslint` and `tsgo`, only when the shell's `PATH` already contained the root's `node_modules/.bin`. (#1039)",
|
|
12
|
+
"hash": "64cde10b969329630577e2f9011d0a6cd480e009",
|
|
13
|
+
"entry": 1
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"version": "0.46.0",
|
|
21
|
+
"date": "2026-10-13",
|
|
22
|
+
"sections": [
|
|
23
|
+
{
|
|
24
|
+
"title": "🎉 Features",
|
|
25
|
+
"audience": "all",
|
|
26
|
+
"items": [
|
|
27
|
+
{
|
|
28
|
+
"description": "Adds a `packageMaturityPeriod` option to the config built with `defineConfig` from `@williamthorsen/nmr/taze`, which sets, for packages whose names match a `*` pattern, how many days after a release `nmr upgrade` waits before proposing it, in place of the shared `maturityPeriod`. (#1036)",
|
|
29
|
+
"hash": "ed3d16ce494c8162e0598ec9ed1004f34a158efd",
|
|
30
|
+
"entry": 1
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"description": "Defines an embedded test, which runs an engine installed by the package, such as PGlite (Postgres compiled to WebAssembly), inside the test process, and places it in the `tool` tier rather than `unit` in `docs/vitest.md` and the `nmr` rulebook. (#1037)",
|
|
34
|
+
"hash": "15c5d922fb5a05aa2d57d18a46a957909ad0f006",
|
|
35
|
+
"entry": 1
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"description": "Redefines a package's `peerDependencies` in `docs/vitest.md` as candidate tools rather than as the `tool` boundary, so a test is `tool` only when it makes a peer do work, and a test that imports a peer only for its constants, such as `AST_NODE_TYPES`, stays `unit`. (#1037)",
|
|
39
|
+
"hash": "15c5d922fb5a05aa2d57d18a46a957909ad0f006",
|
|
40
|
+
"entry": 2
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
2
46
|
{
|
|
3
47
|
"version": "0.45.0",
|
|
4
48
|
"date": "2026-10-12",
|
package/.readyup/kits/default.js
CHANGED
|
@@ -8939,7 +8939,7 @@ function formatPaths(paths) {
|
|
|
8939
8939
|
${paths.map((path7) => ` ${path7}`).join("\n")}`;
|
|
8940
8940
|
}
|
|
8941
8941
|
function getMinVersion() {
|
|
8942
|
-
const pickedFields = { "version": "0.
|
|
8942
|
+
const pickedFields = { "version": "0.46.1" };
|
|
8943
8943
|
if (typeof pickedFields["version"] !== "string") {
|
|
8944
8944
|
throw new TypeError("nmr/package.json: 'version' must be a string");
|
|
8945
8945
|
}
|
package/.readyup/manifest.json
CHANGED
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"path": "kits/default.ts"
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
|
-
"hash": "
|
|
89
|
+
"hash": "e03b7529",
|
|
90
90
|
"kind": "inline",
|
|
91
91
|
"path": "../package.json",
|
|
92
92
|
"paths": [
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
"path": "../src/steps.ts"
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
|
-
"hash": "
|
|
162
|
+
"hash": "d8368f1a",
|
|
163
163
|
"kind": "module",
|
|
164
164
|
"path": "../src/tiers.ts"
|
|
165
165
|
},
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"readyupVersion": "0.40.0",
|
|
190
190
|
"source": "kits/default.ts",
|
|
191
191
|
"sourceHash": "aaeb1a84",
|
|
192
|
-
"targetHash": "
|
|
192
|
+
"targetHash": "dbfec446"
|
|
193
193
|
}
|
|
194
194
|
]
|
|
195
195
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.46.1 — 2026-10-13
|
|
6
|
+
|
|
7
|
+
### 🐛 Bug fixes
|
|
8
|
+
|
|
9
|
+
- Fixes the issue that a script run by bare `nmr` inside a package failed with `command not found` on a binary installed only in that package, and found the root's binaries, such as `eslint` and `tsgo`, only when the shell's `PATH` already contained the root's `node_modules/.bin`. (#1039)
|
|
10
|
+
|
|
11
|
+
## 0.46.0 — 2026-10-13
|
|
12
|
+
|
|
13
|
+
### 🎉 Features
|
|
14
|
+
|
|
15
|
+
- Adds a `packageMaturityPeriod` option to the config built with `defineConfig` from `@williamthorsen/nmr/taze`, which sets, for packages whose names match a `*` pattern, how many days after a release `nmr upgrade` waits before proposing it, in place of the shared `maturityPeriod`. (#1036)
|
|
16
|
+
- Defines an embedded test, which runs an engine installed by the package, such as PGlite (Postgres compiled to WebAssembly), inside the test process, and places it in the `tool` tier rather than `unit` in `docs/vitest.md` and the `nmr` rulebook. (#1037)
|
|
17
|
+
- Redefines a package's `peerDependencies` in `docs/vitest.md` as candidate tools rather than as the `tool` boundary, so a test is `tool` only when it makes a peer do work, and a test that imports a peer only for its constants, such as `AST_NODE_TYPES`, stays `unit`. (#1037)
|
|
18
|
+
|
|
5
19
|
## 0.45.0 — 2026-10-12
|
|
6
20
|
|
|
7
21
|
### 🎉 Features
|
package/README.md
CHANGED
|
@@ -5,21 +5,11 @@
|
|
|
5
5
|
Context-aware script runner for pnpm monorepos. Includes an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
|
|
6
6
|
|
|
7
7
|
<!-- section:release-notes -->
|
|
8
|
-
## Release notes — v0.
|
|
9
|
-
|
|
10
|
-
### 🎉 Features
|
|
11
|
-
|
|
12
|
-
- Adds `maintenance.auto=false` to the git config that the Vitest git-isolation setup file injects, so that a git command run by a test does not start a detached `git maintenance` process that can still be writing under `.git` when the test removes the repository. (#1033)
|
|
8
|
+
## Release notes — v0.46.1 (2026-10-13)
|
|
13
9
|
|
|
14
10
|
### 🐛 Bug fixes
|
|
15
11
|
|
|
16
|
-
- Fixes the issue that `nmr
|
|
17
|
-
- Fixes the issue that a `rootScripts` or `workspaceScripts` entry in `.config/nmr.config.ts` that names its own key, as a shell segment or as a step-list element, re-entered nmr without bound, by rejecting the entry when the config loads with an error that names the file, the field, the key, and the edit that resolves it. (#1011)
|
|
18
|
-
- Fixes the issue that a package's own `package.json` entry running `nmr -R <same command>` re-entered that package without bound instead of being discarded or rejected as a self-reference. (#1011)
|
|
19
|
-
- Fixes the issue that nmr treated a root entry such as `pnpm --recursive exec nmr build` as a re-invocation of its own command, because it ignored the `-r`, `--filter`, and `-w` flags that pnpm applies to the nmr that it launches. (#1011)
|
|
20
|
-
- Fixes the issue that `nmr` hung when a script entry re-entered its own command in the same directory without nmr recognizing the self-reference, as `nmr -F own-pkg build` in `own-pkg`'s own `build` or a cycle through two entries does, by failing the run with an error that names the command and the directory and lists the commands that the run had entered, outermost first. (#1012)
|
|
21
|
-
- Fixes the issue that the test-tier definitions in `vitest.md` gave a test that starts its own loopback server three defensible tiers: `tool` is now a process that the test starts or a program that the environment supplies, `localhost` a service already running on this machine, and an in-process server from the package's own dependency closure `unit`. (#1023)
|
|
22
|
-
- Fixes the issue that `nmr`'s warning about a `package.json` script that runs `nmr` through a shell sent an entry whose whole value is `nmr <name>` to `.config/nmr.config.ts`, when the accurate edit is to replace that value with the command of the `<name>` script in the same file. (#1030)
|
|
12
|
+
- Fixes the issue that a script run by bare `nmr` inside a package failed with `command not found` on a binary installed only in that package, and found the root's binaries, such as `eslint` and `tsgo`, only when the shell's `PATH` already contained the root's `node_modules/.bin`. (#1039)
|
|
23
13
|
<!-- /section:release-notes -->
|
|
24
14
|
|
|
25
15
|
## Installation
|
|
@@ -220,7 +210,7 @@ export default definePrettierConfig();
|
|
|
220
210
|
|
|
221
211
|
### Dependency upgrades
|
|
222
212
|
|
|
223
|
-
`nmr upgrade` reports available dependency upgrades. A `taze.config.ts` at the monorepo root, built with the `defineConfig` exported by `@williamthorsen/nmr/taze`, declares the upgrade policy. [Dependency upgrades](docs/upgrades.md) covers what each invocation reaches, the policy and its ceilings, and the request timeout.
|
|
213
|
+
`nmr upgrade` reports available dependency upgrades. A `taze.config.ts` at the monorepo root, built with the `defineConfig` exported by `@williamthorsen/nmr/taze`, declares the upgrade policy. [Dependency upgrades](docs/upgrades.md) covers what each invocation reaches, the policy and its ceilings, per-package maturity periods, and the request timeout.
|
|
224
214
|
|
|
225
215
|
### Workspace introspection
|
|
226
216
|
|
|
@@ -13,5 +13,5 @@ delivery: ambient
|
|
|
13
13
|
- A check that already passed on the current working tree skips, printing `SKIP <scope>: <command>: passed …` and, when the recalled run left one, `— replayed: <excerpt>` of what that run concluded. Any edit, addition, or deletion re-runs it. Read the replayed excerpt rather than running the check again: Never pass `--no-cache` to get a "real" run, which takes 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`. A composite replays its constituents' excerpts, each attributed as `<scope>: <command>: <excerpt>` and cut at the line's ceiling when there are too many to fit. A skip without an excerpt is one whose run wrote to a terminal, or one whose constituents recorded none.
|
|
14
14
|
- `nmr --log <command>` prints the whole run behind that excerpt -- up to 256 KiB, led by when it was recorded, how long it ran, and the command string that produced it. Output that you need in full is a recording to read rather than a command to run again. It runs nothing, and it is held to the tree rather than to the terminal: A recording made by a piped run prints for a reader at one. A composite prints its constituents' excerpts, not their transcripts, so the transcript of one of them is `nmr -F <pkg> --log <command>` or that constituent's own name. When there is nothing to show, `--log` says so on one line, names which case it is, and exits non-zero.
|
|
15
15
|
- Every command that nmr runs reports one line -- `PASS <scope>: <command>: passed in 12.4s`, `FAIL … failed in 1.2s (exit 1)`, `SKIP … passed 4m ago on this tree`, or `NOOP … skipped, the override is empty` -- and a composite reports alongside every command into which it expands. Those are the words that a pipe and a CI log get; a terminal gets ✅, ❌, ⏩, and ⚪ in their place, and `--output-style rich` or `NMR_OUTPUT_STYLE=rich` forces the emoji on any stream. The lines print in every verbosity, so `nmr -q <command>` is the low-noise way to run rather than a silent one: It withholds the output of the commands that nmr runs and keeps nmr's own verdicts, which is how a pass, a cached skip, and a `""`/`":"` override stay distinguishable. nmr reads its own flags only before the command name and passes everything after the name to the commands that run: `nmr ci -q` hands `-q` to `nmr-fmt`, which rejects it. Expect a run to be quiet already without the flag: nmr recognizes a known agent harness in the environment and takes quiet from that, as it does from `NMR_COMMAND_VERBOSITY` or the repo's `output.commandVerbosity`. A command whose output you need in full is one to run with `NMR_COMMAND_VERBOSITY=full`, which outranks both.
|
|
16
|
-
- Every test file lives under a `__tests__` directory and names its isolation tier last: `<subject>[.<aspect>].<tier>.test.ts`, where any earlier segment is free-form documentation. A tier is the furthest thing that the test reaches -- `tool` (a process that the test starts, or a program that the
|
|
16
|
+
- Every test file lives under a `__tests__` directory and names its isolation tier last: `<subject>[.<aspect>].<tier>.test.ts`, where any earlier segment is free-form documentation. A tier is the furthest thing that the test reaches -- `tool` (a process that the test starts, or a program other than the package's own code that the test makes do its work, including an embedded engine, which the package installs and the test runs in-process, such as PGlite), `localhost` (a service already running on this machine), `remote` (a machine that isn't this one), or `unit`, the residual claiming everything else. A file outside `__tests__`, or one whose tier segment is missing or misspelt, fails the test run in a repo that declares nmr's exported conventions check, which reports both. In a repo that has not, neither fails anything -- the first is collected by nothing and the second runs under `unit` -- and `rdy run --sources` reports both and warns that the check is missing.
|
|
17
17
|
- The rest is in `node_modules/@williamthorsen/nmr/docs/`: `scripts.md` for pre/post hooks (every `nmr X` auto-wraps as `nmr X:pre && nmr X && nmr X:post`) and script overrides and their `""`/`":"` skip values; `upgrades.md` for `nmr upgrade` ceilings (report-only until `nmr upgrade --write`); `utilities.md` for the `nmr-compile` build (the repo does not define a build script of its own); `vitest.md` for the test tiers' timeout budgets, the `tiers` seam that raises one, and which script reaches which tier; and `check-cache.md` for the check-result cache's key, the retention key to which a replayed excerpt is additionally held, the retention ceiling within which `--log` prints, its `checkCache` config, and every condition that disables it.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the environment in which a scope's commands run: Its `PATH` leads with the anchor's `node_modules/.bin`
|
|
3
|
+
* and then the monorepo root's, as under `pnpm run`, followed by the inherited `PATH`.
|
|
4
|
+
*
|
|
5
|
+
* A directory that the inherited `PATH` already contains is left where it is, so that an nmr run nested in another
|
|
6
|
+
* does not lengthen `PATH`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function prependBinDirs(options: {
|
|
9
|
+
anchorDir: string;
|
|
10
|
+
env: NodeJS.ProcessEnv;
|
|
11
|
+
monorepoRoot: string;
|
|
12
|
+
}): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export function prependBinDirs(options) {
|
|
3
|
+
const { anchorDir, env, monorepoRoot } = options;
|
|
4
|
+
const inherited = env['PATH'] ?? '';
|
|
5
|
+
const seen = new Set(inherited
|
|
6
|
+
.split(path.delimiter)
|
|
7
|
+
.filter((entry) => entry !== '')
|
|
8
|
+
.map((entry) => path.resolve(entry)));
|
|
9
|
+
const additions = [];
|
|
10
|
+
for (const dir of [anchorDir, monorepoRoot]) {
|
|
11
|
+
const binDir = path.resolve(dir, 'node_modules', '.bin');
|
|
12
|
+
if (!seen.has(binDir)) {
|
|
13
|
+
seen.add(binDir);
|
|
14
|
+
additions.push(binDir);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (additions.length === 0) {
|
|
18
|
+
return env;
|
|
19
|
+
}
|
|
20
|
+
return { ...env, PATH: [...additions, ...(inherited === '' ? [] : [inherited])].join(path.delimiter) };
|
|
21
|
+
}
|
package/dist/esm/cli-taze.js
CHANGED
|
@@ -3,7 +3,7 @@ import { reportError } from '@williamthorsen/nmr-core';
|
|
|
3
3
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
4
|
import { runTaze } from "./commands/taze.js";
|
|
5
5
|
try {
|
|
6
|
-
process.exitCode = runTaze(process.argv.slice(2));
|
|
6
|
+
process.exitCode = await runTaze(process.argv.slice(2));
|
|
7
7
|
}
|
|
8
8
|
catch (error) {
|
|
9
9
|
reportError(describeError(error));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the `packageMaturityPeriod` that the taze config governing `cwd` declares, unvalidated, or `undefined`
|
|
3
|
+
* when no config file is found.
|
|
4
|
+
*
|
|
5
|
+
* The sources, the upward search, and the `default` unwrapping mirror `resolveConfig` and `normalizeConfig` in
|
|
6
|
+
* taze 21.3.0, so that this reads the same file that taze reads. Throws when the file fails to load.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadPackageMaturityPeriod(cwd: string): Promise<unknown>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createConfigLoader } from 'unconfig';
|
|
2
|
+
export async function loadPackageMaturityPeriod(cwd) {
|
|
3
|
+
const { config, sources } = await createConfigLoader({
|
|
4
|
+
sources: [{ files: ['taze.config'] }, { files: ['.tazerc'], extensions: ['json', ''] }],
|
|
5
|
+
cwd,
|
|
6
|
+
merge: false,
|
|
7
|
+
}).load();
|
|
8
|
+
if (sources.length === 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
const options = isRecord(config) && 'default' in config ? config['default'] : config;
|
|
11
|
+
return isRecord(options) ? options['packageMaturityPeriod'] : undefined;
|
|
12
|
+
}
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === 'object' && value !== null;
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Ends the arguments that taze reads: Everything past it is collected for a downstream tool. */
|
|
2
|
+
export declare const ARGUMENT_TERMINATOR = "--";
|
|
3
|
+
/** A flag found among the arguments that taze reads, with the value given to it, if any. */
|
|
4
|
+
export interface FlagReading {
|
|
5
|
+
value: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Inserts `inserted` after the arguments that taze reads, ahead of a bare `--` so that taze still reads them.
|
|
9
|
+
*/
|
|
10
|
+
export declare function insertBeforeTerminator(args: readonly string[], inserted: readonly string[]): string[];
|
|
11
|
+
/** Reports whether the invocation silences taze's output, in any spelling that taze's CLI accepts. */
|
|
12
|
+
export declare function isSilent(args: readonly string[]): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Finds the first of `spellings` among the arguments that taze reads, returning the value given in its `=value`
|
|
15
|
+
* form or as the next argument. A next argument that starts with `-` is another flag, not a value.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readFlag(args: readonly string[], spellings: readonly string[]): FlagReading | undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const ARGUMENT_TERMINATOR = '--';
|
|
2
|
+
export function insertBeforeTerminator(args, inserted) {
|
|
3
|
+
const terminatorIndex = args.indexOf(ARGUMENT_TERMINATOR);
|
|
4
|
+
if (terminatorIndex === -1)
|
|
5
|
+
return [...args, ...inserted];
|
|
6
|
+
return [...args.slice(0, terminatorIndex), ...inserted, ...args.slice(terminatorIndex)];
|
|
7
|
+
}
|
|
8
|
+
export function isSilent(args) {
|
|
9
|
+
return readFlag(args, ['--silent', '-s']) !== undefined || readFlag(args, ['--loglevel'])?.value === 'silent';
|
|
10
|
+
}
|
|
11
|
+
export function readFlag(args, spellings) {
|
|
12
|
+
for (const [index, arg] of args.entries()) {
|
|
13
|
+
if (arg === ARGUMENT_TERMINATOR)
|
|
14
|
+
return undefined;
|
|
15
|
+
for (const spelling of spellings) {
|
|
16
|
+
if (arg.startsWith(`${spelling}=`))
|
|
17
|
+
return { value: arg.slice(spelling.length + 1) };
|
|
18
|
+
if (arg === spelling) {
|
|
19
|
+
const next = args[index + 1];
|
|
20
|
+
return { value: next === undefined || next.startsWith('-') ? undefined : next };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** One run of taze: the arguments that it receives and, in a split run, the heading printed before it. */
|
|
2
|
+
export interface TazePass {
|
|
3
|
+
args: string[];
|
|
4
|
+
heading?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Splits a taze invocation into one pass per `packageMaturityPeriod` entry and one for every other dependency.
|
|
8
|
+
*
|
|
9
|
+
* Each pass is confined by `--exclude` alone: taze subtracts every `--exclude` value from the dependencies that it
|
|
10
|
+
* checks, so the invocation's own `--include` and `--exclude` narrow each pass as they narrow a single run. An
|
|
11
|
+
* entry pass excludes every package that its key does not match or that an earlier key matches, so that the first
|
|
12
|
+
* matching key decides a package's period. A maturity period given on the command line applies to every
|
|
13
|
+
* dependency, so it returns the invocation as a single pass.
|
|
14
|
+
*
|
|
15
|
+
* Throws when `packageMaturityPeriod` is not a record of glob keys to non-negative periods. A key cannot be a regex,
|
|
16
|
+
* contain a comma, or name a version (`name@range`), each of which taze reads as something other than a name glob.
|
|
17
|
+
*/
|
|
18
|
+
export declare function planTazePasses(args: readonly string[], packageMaturityPeriod: unknown): TazePass[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { insertBeforeTerminator, readFlag } from "./taze-args.js";
|
|
2
|
+
const MATURITY_PERIOD_FLAGS = ['--maturity-period', '--maturityPeriod'];
|
|
3
|
+
export function planTazePasses(args, packageMaturityPeriod) {
|
|
4
|
+
const entries = validatePackageMaturityPeriod(packageMaturityPeriod);
|
|
5
|
+
if (entries.length === 0 || readFlag(args, MATURITY_PERIOD_FLAGS) !== undefined)
|
|
6
|
+
return [{ args: [...args] }];
|
|
7
|
+
const keys = entries.map(([key]) => key);
|
|
8
|
+
const entryPasses = entries.map(([key, days], index) => ({
|
|
9
|
+
args: insertBeforeTerminator(args, [
|
|
10
|
+
'--maturity-period',
|
|
11
|
+
String(days),
|
|
12
|
+
'--exclude',
|
|
13
|
+
composeComplementFilter(key, keys.slice(0, index)),
|
|
14
|
+
]),
|
|
15
|
+
heading: `Packages matching ${key}: ${days}-day maturity period`,
|
|
16
|
+
}));
|
|
17
|
+
const remainderPass = {
|
|
18
|
+
args: insertBeforeTerminator(args, ['--exclude', keys.join(',')]),
|
|
19
|
+
heading: 'All other packages: configured maturity period',
|
|
20
|
+
};
|
|
21
|
+
return [...entryPasses, remainderPass];
|
|
22
|
+
}
|
|
23
|
+
function composeComplementFilter(key, earlierKeys) {
|
|
24
|
+
const outsideKey = `^(?!${convertGlobToRegexBody(key)}$)`;
|
|
25
|
+
if (earlierKeys.length === 0)
|
|
26
|
+
return `/${outsideKey}/`;
|
|
27
|
+
return `/${outsideKey}|^(?:${earlierKeys.map(convertGlobToRegexBody).join('|')})$/`;
|
|
28
|
+
}
|
|
29
|
+
function convertGlobToRegexBody(glob) {
|
|
30
|
+
return glob.replaceAll(/[.+?^${}()|[\]\\]/g, String.raw `\$&`).replaceAll(/\*+/g, '.*?');
|
|
31
|
+
}
|
|
32
|
+
function validatePackageMaturityPeriod(value) {
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
return [];
|
|
35
|
+
if (!isPlainObject(value)) {
|
|
36
|
+
throw new Error('packageMaturityPeriod must be an object mapping package patterns to periods in days');
|
|
37
|
+
}
|
|
38
|
+
return Object.entries(value).map(([key, days]) => {
|
|
39
|
+
if (key.startsWith('/')) {
|
|
40
|
+
throw new Error(`packageMaturityPeriod entry '${key}': keys are package names with * wildcards, not regexes`);
|
|
41
|
+
}
|
|
42
|
+
if (key.includes(',')) {
|
|
43
|
+
throw new Error(`packageMaturityPeriod entry '${key}': a key cannot contain a comma`);
|
|
44
|
+
}
|
|
45
|
+
if (key.lastIndexOf('@') > 0) {
|
|
46
|
+
throw new Error(`packageMaturityPeriod entry '${key}': a key names packages, not a version selector`);
|
|
47
|
+
}
|
|
48
|
+
if (typeof days !== 'number' || !Number.isFinite(days) || days < 0) {
|
|
49
|
+
throw new Error(`packageMaturityPeriod entry '${key}': the period must be a non-negative number of days`);
|
|
50
|
+
}
|
|
51
|
+
return [key, days];
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function isPlainObject(value) {
|
|
55
|
+
if (typeof value !== 'object' || value === null)
|
|
56
|
+
return false;
|
|
57
|
+
const prototype = Object.getPrototypeOf(value);
|
|
58
|
+
return prototype === Object.prototype || prototype === null;
|
|
59
|
+
}
|
|
@@ -4,28 +4,34 @@ export interface SpawnOutcome {
|
|
|
4
4
|
error?: Error | undefined;
|
|
5
5
|
}
|
|
6
6
|
export interface RunTazeOptions {
|
|
7
|
+
/** Returns the `packageMaturityPeriod` that the taze config governing a directory declares. Injected by tests. */
|
|
8
|
+
loadConfig?: (cwd: string) => Promise<unknown>;
|
|
7
9
|
/** Resolves the absolute path of taze's CLI entry. Injected by tests. */
|
|
8
10
|
resolveCliPath?: () => string;
|
|
9
11
|
/** Runs the CLI and reports how it exited. Injected by tests. */
|
|
10
12
|
spawn?: (nodePath: string, argv: string[]) => SpawnOutcome;
|
|
11
|
-
/** Stream that error output
|
|
13
|
+
/** Stream that error output and pass headings are written to. Defaults to `process.stderr`. */
|
|
12
14
|
stderr?: Writable;
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* Runs taze from nmr's own dependency tree, forwarding `args`, and returns its exit code.
|
|
16
18
|
*
|
|
19
|
+
* When the repo's taze config declares `packageMaturityPeriod`, runs taze once per entry and once for every other
|
|
20
|
+
* dependency, each with `args` and the filters and period that `planTazePasses` adds, and returns the first non-zero
|
|
21
|
+
* exit code among them. Every pass runs; a heading on `stderr` names each pass unless `args` silence taze.
|
|
22
|
+
*
|
|
17
23
|
* Consumers depend on nmr, not on taze, so taze is transitive and its bin is absent from the consumer's
|
|
18
24
|
* root `node_modules/.bin`. This launcher runs taze without that bin: pnpm links nmr's own bins into the
|
|
19
25
|
* consumer root, and nmr resolves taze from the tree that it does control.
|
|
20
26
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
27
|
+
* Every pass also receives a request timeout, unless `args` sets one. Neither other site can declare it: Because
|
|
28
|
+
* an invocation's trailing arguments are appended to the resolved command string, the same flag in the `upgrade`
|
|
29
|
+
* script would be doubled by `nmr upgrade --request-timeout ...`, and taze's parser collects the pair into an
|
|
30
|
+
* array that it reads as a near-zero deadline; a `taze.config.ts` setting goes into the object that taze's own CLI
|
|
31
|
+
* defaults overwrite. Because every argument of the invocation reaches taze, invocation policy (`--recursive`)
|
|
32
|
+
* stays in the script registry, visible in `nmr` help output and overridable per repo, and upgrade policy stays in
|
|
33
|
+
* `taze.ts`.
|
|
28
34
|
*/
|
|
29
|
-
export declare function runTaze(args: string[], options?: RunTazeOptions): number
|
|
35
|
+
export declare function runTaze(args: string[], options?: RunTazeOptions): Promise<number>;
|
|
30
36
|
/** Resolves the absolute path of taze's CLI entry from nmr's own dependency tree. */
|
|
31
37
|
export declare function resolveTazeCliPath(): string;
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import process from 'node:process';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
5
6
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
7
|
+
import { loadPackageMaturityPeriod } from "./load-package-maturity-period.js";
|
|
8
|
+
import { isSilent, readFlag } from "./taze-args.js";
|
|
9
|
+
import { planTazePasses } from "./taze-passes.js";
|
|
6
10
|
const TAZE_CLI_SPECIFIER = 'taze/cli';
|
|
7
11
|
const REQUEST_TIMEOUT_FLAG = '--request-timeout';
|
|
8
12
|
const REQUEST_TIMEOUT_FLAGS = [REQUEST_TIMEOUT_FLAG, '--requestTimeout'];
|
|
9
13
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
10
|
-
|
|
11
|
-
export function runTaze(args, options = {}) {
|
|
14
|
+
export async function runTaze(args, options = {}) {
|
|
12
15
|
const stderr = options.stderr ?? process.stderr;
|
|
13
16
|
const resolveCliPath = options.resolveCliPath ?? resolveTazeCliPath;
|
|
14
17
|
const spawn = options.spawn ?? spawnNode;
|
|
18
|
+
const loadConfig = options.loadConfig ?? loadPackageMaturityPeriod;
|
|
15
19
|
let cliPath;
|
|
16
20
|
try {
|
|
17
21
|
cliPath = resolveCliPath();
|
|
@@ -21,27 +25,38 @@ export function runTaze(args, options = {}) {
|
|
|
21
25
|
reportError(`Could not resolve '${TAZE_CLI_SPECIFIER}' from nmr's dependencies. Reinstall the workspace to restore it. (${detail})`, stderr);
|
|
22
26
|
return 1;
|
|
23
27
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
let passes;
|
|
29
|
+
try {
|
|
30
|
+
passes = planTazePasses(args, await loadConfig(resolveConfigDir(args)));
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
reportError(`Could not read the taze config: ${describeError(error)}`, stderr);
|
|
27
34
|
return 1;
|
|
28
35
|
}
|
|
29
|
-
|
|
36
|
+
const showHeadings = !isSilent(args);
|
|
37
|
+
let exitCode = 0;
|
|
38
|
+
for (const pass of passes) {
|
|
39
|
+
if (pass.heading !== undefined && showHeadings)
|
|
40
|
+
stderr.write(`${pass.heading}\n`);
|
|
41
|
+
const outcome = spawn(process.execPath, [cliPath, ...composeRequestTimeoutArgs(pass.args), ...pass.args]);
|
|
42
|
+
if (outcome.error) {
|
|
43
|
+
reportError(`Failed to run taze: ${outcome.error.message}`, stderr);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
const status = outcome.status ?? 1;
|
|
47
|
+
if (exitCode === 0)
|
|
48
|
+
exitCode = status;
|
|
49
|
+
}
|
|
50
|
+
return exitCode;
|
|
30
51
|
}
|
|
31
52
|
export function resolveTazeCliPath() {
|
|
32
53
|
return fileURLToPath(import.meta.resolve(TAZE_CLI_SPECIFIER));
|
|
33
54
|
}
|
|
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
55
|
function composeRequestTimeoutArgs(args) {
|
|
44
|
-
return
|
|
56
|
+
return readFlag(args, REQUEST_TIMEOUT_FLAGS) === undefined ? [REQUEST_TIMEOUT_FLAG, String(REQUEST_TIMEOUT_MS)] : [];
|
|
57
|
+
}
|
|
58
|
+
function resolveConfigDir(args) {
|
|
59
|
+
return path.resolve(readFlag(args, ['--cwd', '-C'])?.value ?? '.');
|
|
45
60
|
}
|
|
46
61
|
function spawnNode(nodePath, argv) {
|
|
47
62
|
const result = spawnSync(nodePath, argv, { stdio: 'inherit' });
|
package/dist/esm/runCli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { describeInvalidOutputStyle, formatGlyphLine, reportError, resolveSelfVersion, STATUS_GLYPHS, } from '@williamthorsen/nmr-core';
|
|
3
3
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
|
+
import { prependBinDirs } from "./bin-path.js";
|
|
4
5
|
import { certifyRetention, computeCacheKey, computeRetentionKey, CURRENT_RUNTIME, encodeTreeSnapshot, findStaleBuildOutput, formatMisplacedNoCacheWarning, isCacheableCommand, NO_CACHE_ENV_VAR, readBuildOutputState, readCheckCacheEntry, recordTranscript, resolveRunId, resolveTreeSnapshot, RUN_ID_ENV_VAR, TREE_SNAPSHOT_ENV_VAR, writeCheckCacheEntry, writeDebugNote, } from "./check-cache.js";
|
|
5
6
|
import { resolveContext } from "./context.js";
|
|
6
7
|
import { enterChain } from "./entry-chain.js";
|
|
@@ -163,7 +164,12 @@ async function planRun(invocation) {
|
|
|
163
164
|
? runOptions
|
|
164
165
|
: {
|
|
165
166
|
...runOptions,
|
|
166
|
-
env: enterChain({
|
|
167
|
+
env: enterChain({
|
|
168
|
+
anchorDir,
|
|
169
|
+
command,
|
|
170
|
+
env: prependBinDirs({ anchorDir, env: childEnv, monorepoRoot: context.monorepoRoot }),
|
|
171
|
+
monorepoRoot: context.monorepoRoot,
|
|
172
|
+
}),
|
|
167
173
|
};
|
|
168
174
|
const chain = assembleChain({ invocation, registry, resolvedScript, snapshot });
|
|
169
175
|
if (chain.settled) {
|
package/dist/esm/taze.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ import type { CheckOptions } from 'taze';
|
|
|
8
8
|
*/
|
|
9
9
|
export type TazeConfig = {
|
|
10
10
|
[K in keyof CheckOptions]?: CheckOptions[K] | undefined;
|
|
11
|
+
} & {
|
|
12
|
+
/**
|
|
13
|
+
* Maturity period in days for each package pattern, read by `nmr-taze` rather than by taze. A key is a package
|
|
14
|
+
* name in which `*` matches any run of characters, and the first key that matches a package decides its period.
|
|
15
|
+
* Every other dependency keeps `maturityPeriod`.
|
|
16
|
+
*/
|
|
17
|
+
packageMaturityPeriod?: Record<string, number> | undefined;
|
|
11
18
|
};
|
|
12
19
|
/**
|
|
13
20
|
* Builds a taze configuration from nmr's shared upgrade policy and a repo's own settings.
|
package/dist/esm/tiers.d.ts
CHANGED
|
@@ -56,11 +56,12 @@ export interface TestFileScanOptions {
|
|
|
56
56
|
excludedBasenames?: readonly string[];
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* The isolation ladder, ordered by the furthest thing that a test reaches
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* `tool`
|
|
59
|
+
* The isolation ladder, ordered by the furthest thing that a test reaches: `unit` runs only the package's own code
|
|
60
|
+
* and the libraries that it calls, and `tool` starts a process or makes another program do its work. A tier names
|
|
61
|
+
* what a test reaches, never how it invokes it: A test driving a compiler through its JavaScript API is `tool`,
|
|
62
|
+
* exactly as one spawning `tsc` would be, and so is an embedded test, which runs in-process an engine that the
|
|
63
|
+
* package installs, such as PGlite. Each named tier's name is also its filename infix, so `parse.tool.test.ts` is
|
|
64
|
+
* collected by the `tool` project.
|
|
64
65
|
*/
|
|
65
66
|
export declare const TIER_NAMES: readonly ["unit", "tool", "localhost", "remote"];
|
|
66
67
|
/** One of the four isolation tiers. */
|
package/docs/scripts.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Scripts and configuration
|
|
2
2
|
|
|
3
|
-
How nmr resolves a command through its three tiers, how script values compose, how hooks wrap a command, how to configure scripts and builds, and what the default commands do beyond what `nmr --help` prints.
|
|
3
|
+
How nmr resolves a command through its three tiers, how script values compose, which binaries a command finds on `PATH`, how hooks wrap a command, how to configure scripts and builds, and what the default commands do beyond what `nmr --help` prints.
|
|
4
4
|
|
|
5
5
|
## Three-tier override system
|
|
6
6
|
|
|
@@ -83,6 +83,10 @@ nmr recognizes `nmr` in command position: at the start of a step, after an unquo
|
|
|
83
83
|
|
|
84
84
|
A [self-referential entry](#three-tier-override-system), in `package.json` or in `.config/nmr.config.ts`, is the shape that nmr cannot run as written, and it is rejected rather than reported. The same recognition finds it, so the same residuals apply: What goes unrecognized there re-enters nmr once, and the run then fails with an error naming the command and the directory that it re-entered.
|
|
85
85
|
|
|
86
|
+
### Binaries on `PATH`
|
|
87
|
+
|
|
88
|
+
Every command that nmr runs, whatever its tier, finds the binaries that `pnpm run` would find. Inside a package, its `PATH` leads with the package's `node_modules/.bin` and then the root's, followed by the `PATH` from which nmr was started; at the root, it leads with the root's `node_modules/.bin`. A script can therefore invoke a binary installed only in its own package by bare name, however nmr was started: bare `nmr` through direnv, `pnpm exec nmr`, or `nmr -F <pkg>` from the root. A directory that `PATH` already contains is not added again, so a nested nmr run does not lengthen it.
|
|
89
|
+
|
|
86
90
|
## Pre and post hooks
|
|
87
91
|
|
|
88
92
|
The wrap rule and a repo-wide example are in the [README](../README.md#pre-and-post-hooks).
|
package/docs/upgrades.md
CHANGED
|
@@ -51,6 +51,34 @@ Two consequences of the `minor` range mode declared by the policy:
|
|
|
51
51
|
- 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 a developer runs `nmr upgrade patch`, and a `minor` ceiling hides it from `nmr upgrade major`.
|
|
52
52
|
- 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'`.
|
|
53
53
|
|
|
54
|
+
### Per-package maturity periods
|
|
55
|
+
|
|
56
|
+
`packageMaturityPeriod` gives the packages that match a pattern a quarantine of their own, in days. Every other dependency keeps `maturityPeriod`. taze applies one maturity period to a whole run, so `nmr upgrade` runs it once for each entry and once more for every other dependency, each run headed on stderr by the patterns and the period that it covers. The command exits with the first non-zero exit code among the runs.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { defineConfig } from '@williamthorsen/nmr/taze';
|
|
60
|
+
|
|
61
|
+
export default defineConfig({
|
|
62
|
+
// Propose first-party releases after one day, and third-party ones after the policy's seven.
|
|
63
|
+
packageMaturityPeriod: {
|
|
64
|
+
'@williamthorsen/*': 1,
|
|
65
|
+
readyup: 1,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A key is a package name in which `*` matches any run of characters; a regex key, a key containing a comma, or a version selector such as `typescript@7` fails the command before taze runs. When several keys match a package, the first one decides its period. A period of `0` removes the quarantine for those packages.
|
|
71
|
+
|
|
72
|
+
A package listed in `maturityPeriodExclude`, or in the `minimumReleaseAgeExclude` of `pnpm-workspace.yaml`, which taze inherits into it, skips the quarantine in every run. Remove it from that list before giving it a period here.
|
|
73
|
+
|
|
74
|
+
`--maturity-period <days>` on the command line applies one period to every dependency for that call, in a single run:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
nmr upgrade --maturity-period 0
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Every other argument reaches every run, and `--include` and `--exclude` narrow each run as they narrow a single one. Because each run reports separately, `--json` prints one document per run, `--interactive` opens one selection per run, and `--install` installs after each run.
|
|
81
|
+
|
|
54
82
|
## Request timeout
|
|
55
83
|
|
|
56
84
|
Every `upgrade` script calls the upgrade tool with a 30-second request timeout, in place of the tool's own 5 seconds.
|
|
@@ -101,7 +129,7 @@ nmr report-overrides
|
|
|
101
129
|
|
|
102
130
|
## `nmr-taze`
|
|
103
131
|
|
|
104
|
-
Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it
|
|
132
|
+
Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it and adding a 30-second request timeout, unless the invocation already includes one. When the taze config declares [per-package maturity periods](#per-package-maturity-periods), it runs taze once per period, adding to each run the filters and the period that confine it. Every `upgrade` chain ends with it, `root:upgrade` included; see [dependency upgrades](#dependency-upgrades) for the workflow, and [request timeout](#request-timeout) for why the timeout is set here.
|
|
105
133
|
|
|
106
134
|
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 that nmr controls.
|
|
107
135
|
|
package/docs/vitest.md
CHANGED
|
@@ -10,22 +10,24 @@ The [README](../README.md#shared-vitest-config) shows the one-line config file.
|
|
|
10
10
|
|
|
11
11
|
The config declares four projects, an ordered ladder named for the furthest thing that a test reaches:
|
|
12
12
|
|
|
13
|
-
| Project | Matches | Reaches
|
|
14
|
-
| ----------- | ------------------------------------------- |
|
|
15
|
-
| `unit` | every test file that the others don't claim |
|
|
16
|
-
| `tool` | `*.tool.test.{ts,tsx}` | a process that the test starts, or a program that the
|
|
17
|
-
| `localhost` | `*.localhost.test.{ts,tsx}` | a service already running on this machine
|
|
18
|
-
| `remote` | `*.remote.test.{ts,tsx}` | a machine that isn't this one
|
|
13
|
+
| Project | Matches | Reaches |
|
|
14
|
+
| ----------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
15
|
+
| `unit` | every test file that the others don't claim | only the package's own code and the libraries that it calls |
|
|
16
|
+
| `tool` | `*.tool.test.{ts,tsx}` | a process that the test starts, or a program other than the package's own code that the test makes do its work |
|
|
17
|
+
| `localhost` | `*.localhost.test.{ts,tsx}` | a service already running on this machine |
|
|
18
|
+
| `remote` | `*.remote.test.{ts,tsx}` | a machine that isn't this one |
|
|
19
19
|
|
|
20
20
|
All four match only under a `__tests__` directory.
|
|
21
21
|
|
|
22
|
-
**A tier names
|
|
22
|
+
**A tier names the furthest program that a test makes do its work.** A test is `unit` while everything that it runs is the package's own code or a library that the code calls for a result. It is `tool` when it starts a process, or when it makes a program other than the package's own code do its work, whether the environment supplies that program or the package installs it. The machine sets how long such a program takes to start and to run, which is why a spawned process counts even when it runs the package's own code.
|
|
23
23
|
|
|
24
|
-
**A tier names what a test reaches, never how it invokes it.** A test driving a compiler through its JavaScript API is `tool`, exactly as one spawning the compiler binary is; whether a program ships as a library or an executable is an accident of packaging, not a property of the test.
|
|
24
|
+
**A tier names what a test reaches, never how it invokes it.** A test driving a compiler through its JavaScript API is `tool`, exactly as one spawning the compiler binary is; whether a program ships as a library or an executable is an accident of packaging, not a property of the test. The same holds for an **embedded** test, which runs, inside the test process, an engine that the package installs as a dependency and that is not the package's own code. An engine does a job of its own, such as a database, a compiler, or a bundler; a library is a set of functions that the code calls for a result. Embedded tests are `tool`: A test that starts PGlite (Postgres compiled to WebAssembly) is `tool`, and a test that calls a parsing library is `unit`.
|
|
25
25
|
|
|
26
|
-
**A service that the test starts itself takes the tier of the code that runs it.** A test that calls `net.Server.listen(0)` on a server
|
|
26
|
+
**A service that the test starts itself takes the tier of the code that runs it.** A test that calls `net.Server.listen(0)` on a server implemented by the package's own code and reaches it with `fetch` is `unit`: Both ends of the loopback connection are inside the test process, and the connection is how the test invokes that code. The same server started as a child process is `tool`, as is a service whose program the environment supplies, such as a `postgres` binary, or one that the package installs, such as PGlite. `localhost` is for a service that is already running when the test starts, such as a separately started backend or a seeded database.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
**`peerDependencies` name candidate tools; they do not classify a test.** A package's peers, with the binaries that it spawns and the engines that it installs, are the programs that its tests might make do their work, and a test is `tool` only when it does. A test that imports `AST_NODE_TYPES` from `@typescript-eslint/utils` for its constants, or calls a helper that ships beside a tool and does none of the tool's work, stays `unit`; a test that runs ESLint over a fixture is `tool`. That the peer must be installed for the import to succeed is a precondition, not a tier argument, as the next paragraph states.
|
|
29
|
+
|
|
30
|
+
**A tier is not a statement about preconditions.** A `unit` test may still need something set up before it runs: a build, a generated fixture, a seeded file, an installed peer. It is `unit` because it does not make any program beyond its own code do work _while running_. The filesystem belongs to `unit` for the same reason: Reaching it does not run another program, and it is always available.
|
|
29
31
|
|
|
30
32
|
**Every test file names its tier**, in the form `<subject>[.<aspect>].<tier>.test.ts`. Only the segment immediately before `.test.` selects a project, so an earlier one is free for documentation: `resolveConfig.packaged.unit.test.ts` reads as a companion to `resolveConfig.unit.test.ts` and still names `unit`.
|
|
31
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for pnpm monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -83,7 +83,8 @@
|
|
|
83
83
|
"glob": "13.0.6",
|
|
84
84
|
"prettier": "3.9.9",
|
|
85
85
|
"prettier-plugin-sh": "0.20.2",
|
|
86
|
-
"taze": "21.3.0"
|
|
86
|
+
"taze": "21.3.0",
|
|
87
|
+
"unconfig": "7.5.0"
|
|
87
88
|
},
|
|
88
89
|
"devDependencies": {
|
|
89
90
|
"@williamthorsen/toolbelt.testing": "0.6.3",
|
|
@@ -92,7 +93,7 @@
|
|
|
92
93
|
"readyup": "0.40.0",
|
|
93
94
|
"tinyglobby": "0.2.17",
|
|
94
95
|
"typescript": "6.0.3",
|
|
95
|
-
"vite": "8.3.
|
|
96
|
+
"vite": "8.3.3",
|
|
96
97
|
"vitest": "4.1.11",
|
|
97
98
|
"yaml": "2.9.1"
|
|
98
99
|
},
|