@williamthorsen/nmr 0.44.0 → 0.46.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/.meta/changelog.json +107 -0
- package/.readyup/kits/default.js +419 -129
- package/.readyup/manifest.json +13 -17
- package/CHANGELOG.md +32 -0
- package/README.md +6 -21
- package/agents/guidance/rulebooks/nmr.md +1 -1
- package/dist/esm/cli-taze.js +1 -1
- package/dist/esm/commands/fmt.d.ts +4 -2
- package/dist/esm/commands/fmt.js +11 -3
- 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/config.js +46 -3
- package/dist/esm/entry-chain.d.ts +24 -0
- package/dist/esm/entry-chain.js +57 -0
- package/dist/esm/glyphs.d.ts +1 -1
- package/dist/esm/helpers/escapeControlCharacters.d.ts +10 -0
- package/dist/esm/helpers/escapeControlCharacters.js +12 -0
- package/dist/esm/resolver.js +5 -2
- package/dist/esm/runCli.js +141 -98
- package/dist/esm/steps.d.ts +38 -13
- package/dist/esm/steps.js +77 -10
- package/dist/esm/taze.d.ts +7 -0
- package/dist/esm/tiers.d.ts +6 -4
- package/dist/esm/vitest-git-isolation.js +3 -1
- package/docs/reporting.md +2 -0
- package/docs/scripts.md +10 -2
- package/docs/upgrades.md +29 -1
- package/docs/utilities.md +2 -2
- package/docs/vitest.md +15 -9
- package/package.json +5 -4
package/.readyup/manifest.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"bundledDependencies": {
|
|
6
6
|
"@williamthorsen/toolbelt.errors": "0.8.3",
|
|
7
7
|
"@williamthorsen/toolbelt.filesystem": "0.9.3",
|
|
8
|
+
"@williamthorsen/toolbelt.packaging": "0.6.3",
|
|
8
9
|
"@williamthorsen/toolbelt.terminal": "0.2.1",
|
|
9
10
|
"ansi-regex": "6.4.0",
|
|
10
11
|
"ansi-styles": "6.2.3",
|
|
@@ -29,11 +30,6 @@
|
|
|
29
30
|
"kind": "module",
|
|
30
31
|
"path": "../../nmr-core/dist/esm/cache-store.js"
|
|
31
32
|
},
|
|
32
|
-
{
|
|
33
|
-
"hash": "a5f44396",
|
|
34
|
-
"kind": "module",
|
|
35
|
-
"path": "../../nmr-core/dist/esm/findPackageRoot.js"
|
|
36
|
-
},
|
|
37
33
|
{
|
|
38
34
|
"hash": "54384dca",
|
|
39
35
|
"kind": "module",
|
|
@@ -55,7 +51,7 @@
|
|
|
55
51
|
"path": "../../nmr-core/dist/esm/helpers/workspace-patterns.js"
|
|
56
52
|
},
|
|
57
53
|
{
|
|
58
|
-
"hash": "
|
|
54
|
+
"hash": "191f7c24",
|
|
59
55
|
"kind": "module",
|
|
60
56
|
"path": "../../nmr-core/dist/esm/index.js"
|
|
61
57
|
},
|
|
@@ -64,11 +60,6 @@
|
|
|
64
60
|
"kind": "module",
|
|
65
61
|
"path": "../../nmr-core/dist/esm/parseArgs.js"
|
|
66
62
|
},
|
|
67
|
-
{
|
|
68
|
-
"hash": "a7efc38f",
|
|
69
|
-
"kind": "module",
|
|
70
|
-
"path": "../../nmr-core/dist/esm/readPackageVersion.js"
|
|
71
|
-
},
|
|
72
63
|
{
|
|
73
64
|
"hash": "14547f78",
|
|
74
65
|
"kind": "module",
|
|
@@ -95,7 +86,7 @@
|
|
|
95
86
|
"path": "kits/default.ts"
|
|
96
87
|
},
|
|
97
88
|
{
|
|
98
|
-
"hash": "
|
|
89
|
+
"hash": "106526b9",
|
|
99
90
|
"kind": "inline",
|
|
100
91
|
"path": "../package.json",
|
|
101
92
|
"paths": [
|
|
@@ -103,7 +94,7 @@
|
|
|
103
94
|
]
|
|
104
95
|
},
|
|
105
96
|
{
|
|
106
|
-
"hash": "
|
|
97
|
+
"hash": "7e066d18",
|
|
107
98
|
"kind": "module",
|
|
108
99
|
"path": "../src/config.ts"
|
|
109
100
|
},
|
|
@@ -122,6 +113,11 @@
|
|
|
122
113
|
"kind": "module",
|
|
123
114
|
"path": "../src/helpers/config-path.ts"
|
|
124
115
|
},
|
|
116
|
+
{
|
|
117
|
+
"hash": "a663548a",
|
|
118
|
+
"kind": "module",
|
|
119
|
+
"path": "../src/helpers/escapeControlCharacters.ts"
|
|
120
|
+
},
|
|
125
121
|
{
|
|
126
122
|
"hash": "47249ca4",
|
|
127
123
|
"kind": "module",
|
|
@@ -153,17 +149,17 @@
|
|
|
153
149
|
"path": "../src/resolve-scripts.ts"
|
|
154
150
|
},
|
|
155
151
|
{
|
|
156
|
-
"hash": "
|
|
152
|
+
"hash": "15247f0e",
|
|
157
153
|
"kind": "module",
|
|
158
154
|
"path": "../src/resolver.ts"
|
|
159
155
|
},
|
|
160
156
|
{
|
|
161
|
-
"hash": "
|
|
157
|
+
"hash": "ed585ab6",
|
|
162
158
|
"kind": "module",
|
|
163
159
|
"path": "../src/steps.ts"
|
|
164
160
|
},
|
|
165
161
|
{
|
|
166
|
-
"hash": "
|
|
162
|
+
"hash": "d8368f1a",
|
|
167
163
|
"kind": "module",
|
|
168
164
|
"path": "../src/tiers.ts"
|
|
169
165
|
},
|
|
@@ -193,7 +189,7 @@
|
|
|
193
189
|
"readyupVersion": "0.40.0",
|
|
194
190
|
"source": "kits/default.ts",
|
|
195
191
|
"sourceHash": "aaeb1a84",
|
|
196
|
-
"targetHash": "
|
|
192
|
+
"targetHash": "92d23b08"
|
|
197
193
|
}
|
|
198
194
|
]
|
|
199
195
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.46.0 — 2026-10-13
|
|
6
|
+
|
|
7
|
+
### 🎉 Features
|
|
8
|
+
|
|
9
|
+
- 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)
|
|
10
|
+
- 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)
|
|
11
|
+
- 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)
|
|
12
|
+
|
|
13
|
+
## 0.45.0 — 2026-10-12
|
|
14
|
+
|
|
15
|
+
### 🎉 Features
|
|
16
|
+
|
|
17
|
+
- 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)
|
|
18
|
+
|
|
19
|
+
### 🐛 Bug fixes
|
|
20
|
+
|
|
21
|
+
- Fixes the issue that `nmr-fmt` failed a run in which every path matched by the pathspecs was one that it skips, such as a symlink, a submodule gitlink, or an unreadable file, and makes such a run exit 0 and name the skipped paths on stderr. (#1010)
|
|
22
|
+
- 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)
|
|
23
|
+
- 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)
|
|
24
|
+
- 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)
|
|
25
|
+
- 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)
|
|
26
|
+
- 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)
|
|
27
|
+
- 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)
|
|
28
|
+
|
|
29
|
+
### ♻️ Refactoring
|
|
30
|
+
|
|
31
|
+
- Moves the work of `runInvocation` in `nmr` into the phase functions `establishInvocation`, `planRun`, and `executePlan`, which `runCli` calls in order, and changes `runGated` and `reportRecording` to take a named `RunPlan` in place of an options object that repeated its fields. (#1014)
|
|
32
|
+
|
|
33
|
+
### 🧪 Tests
|
|
34
|
+
|
|
35
|
+
- Replaces the `GIT_CEILING_DIRECTORIES` stub with a `GIT_DIR` stub that points to a missing path in the no-repository test of `check-cache-gate.tool.test.ts`, and sets `maintenance.auto` to `false` in the file's fixture repository, so that git neither finds a leftover of the removed `repo/.git` nor starts a detached maintenance process that writes under `.git`. (#1032)
|
|
36
|
+
|
|
5
37
|
## 0.44.0 — 2026-10-11
|
|
6
38
|
|
|
7
39
|
### 🎉 Features
|
package/README.md
CHANGED
|
@@ -5,28 +5,13 @@
|
|
|
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.
|
|
8
|
+
## Release notes — v0.46.0 (2026-10-13)
|
|
9
9
|
|
|
10
10
|
### 🎉 Features
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- Adds the root-only config key `selfHostedRunner.exempt`, a non-empty reason that stops the kit from recommending `CI_RUNS_ON` for a repo that qualifies for the self-hosted runners but stays off them. (#1006)
|
|
16
|
-
|
|
17
|
-
### 🪦 Removed
|
|
18
|
-
|
|
19
|
-
- 🚨 **Breaking:** Removes the zero-argument form of `getDefaultRootScripts` from the `@williamthorsen/nmr/scripts` export, which now requires the monorepo root so that it can read the declared workspaces. (#992)
|
|
20
|
-
|
|
21
|
-
### 🐛 Bug fixes
|
|
22
|
-
|
|
23
|
-
- Fixes the issue that `root:lint`, `root:lint:check`, and `root:lint:strict` linted every workspace outside `packages/` under the root ESLint config, by excluding each workspace package that `pnpm-workspace.yaml` declares, with its `!` exclusions honored, in place of the fixed `packages/**`. (#992)
|
|
24
|
-
- Fixes the issue that `defineConfig({ mode: undefined })` emptied the `nmr upgrade` report, by translating a cleared `mode` into the `'default'` mode of taze (the upgrade tool that `nmr upgrade` runs), which follows the range that each dependency declares. (#1008)
|
|
25
|
-
|
|
26
|
-
### ⚡ Performance
|
|
27
|
-
|
|
28
|
-
- Sets `maxWorkers: 3` on every project built by `defineVitestConfig` and `defineRootVitestConfig` when the machine has 2 or fewer cores, such as GitHub's standard runner for a private repo, on which Vitest's default ran every test file serially on one worker. (#994)
|
|
29
|
-
- Changes the default `check` and `check:strict` to start lint first and run it in the background, so that a local check, `nmr ci`, and `nmr prepush` no longer wait for type-aware lint to finish before typecheck, the format check, and the tests run. (#1003)
|
|
12
|
+
- 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)
|
|
13
|
+
- 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)
|
|
14
|
+
- 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)
|
|
30
15
|
<!-- /section:release-notes -->
|
|
31
16
|
|
|
32
17
|
## Installation
|
|
@@ -124,7 +109,7 @@ Scripts resolve through three tiers. Higher tiers override lower ones:
|
|
|
124
109
|
2. **Repo-wide config**: Additions and overrides in `.config/nmr.config.ts`
|
|
125
110
|
3. **Per-package overrides**: Scripts in a package's `package.json`
|
|
126
111
|
|
|
127
|
-
[Scripts and configuration](docs/scripts.md#three-tier-override-system) shows a worked example, how to skip a script for one package, and what nmr does with a `package.json` entry that re-invokes its own command.
|
|
112
|
+
[Scripts and configuration](docs/scripts.md#three-tier-override-system) shows a worked example, how to skip a script for one package, and what nmr does with a `package.json` or `.config/nmr.config.ts` entry that re-invokes its own command.
|
|
128
113
|
|
|
129
114
|
## Pre and post hooks
|
|
130
115
|
|
|
@@ -227,7 +212,7 @@ export default definePrettierConfig();
|
|
|
227
212
|
|
|
228
213
|
### Dependency upgrades
|
|
229
214
|
|
|
230
|
-
`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.
|
|
215
|
+
`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.
|
|
231
216
|
|
|
232
217
|
### Workspace introspection
|
|
233
218
|
|
|
@@ -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 program
|
|
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.
|
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));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type FormatMode = 'check' | 'write';
|
|
2
2
|
export interface FormatTargets {
|
|
3
|
+
/** Listed paths that Prettier cannot be handed, such as symlinks and submodule gitlinks. */
|
|
4
|
+
excludedFiles: string[];
|
|
3
5
|
/** Paths relative to the working directory, as `git ls-files` emits them. */
|
|
4
6
|
files: string[];
|
|
5
7
|
/** Absolute paths to every `.prettierignore` governing the repository, root-most first. */
|
|
@@ -42,8 +44,8 @@ export declare function runFmt(argv: string[], cwd?: string): Promise<number>;
|
|
|
42
44
|
* `pathspecs` narrow the selection and are passed to git verbatim: They have git pathspec semantics rather
|
|
43
45
|
* than shell glob semantics.
|
|
44
46
|
*
|
|
45
|
-
* A listed path that
|
|
46
|
-
* fails the resolution.
|
|
47
|
+
* A listed path that Prettier cannot be handed is set aside in `excludedFiles`, and one that this process is denied
|
|
48
|
+
* access to in `unreadableFiles`; any other stat failure fails the resolution.
|
|
47
49
|
*/
|
|
48
50
|
export declare function resolveFormatTargets(cwd: string, pathspecs?: string[]): ResolveTargetsResult;
|
|
49
51
|
/** @internal */
|
package/dist/esm/commands/fmt.js
CHANGED
|
@@ -33,15 +33,20 @@ export async function runFmt(argv, cwd = process.cwd()) {
|
|
|
33
33
|
reportError(`nmr-fmt: ${resolvedTargets.error}`);
|
|
34
34
|
return 1;
|
|
35
35
|
}
|
|
36
|
-
const { files, ignorePaths, repositoryRoot, unreadableFiles } = resolvedTargets.targets;
|
|
36
|
+
const { excludedFiles, files, ignorePaths, repositoryRoot, unreadableFiles } = resolvedTargets.targets;
|
|
37
37
|
for (const { file, code } of unreadableFiles) {
|
|
38
38
|
process.stderr.write(`nmr-fmt: skipped ${file}, which cannot be read (${code})\n`);
|
|
39
39
|
}
|
|
40
40
|
if (files.length === 0) {
|
|
41
|
-
if (parsedArgs.pathspecs.length
|
|
42
|
-
|
|
41
|
+
if (parsedArgs.pathspecs.length === 0)
|
|
42
|
+
return 0;
|
|
43
|
+
if (excludedFiles.length === 0 && unreadableFiles.length === 0) {
|
|
44
|
+
reportError(`nmr-fmt: the pathspecs did not match any file that git lists: ${parsedArgs.pathspecs.join(', ')}`);
|
|
43
45
|
return 1;
|
|
44
46
|
}
|
|
47
|
+
if (excludedFiles.length > 0) {
|
|
48
|
+
process.stderr.write(`nmr-fmt: nothing to format; skipped paths that are not regular files: ${excludedFiles.join(', ')}\n`);
|
|
49
|
+
}
|
|
45
50
|
return 0;
|
|
46
51
|
}
|
|
47
52
|
const cli = resolvePrettierCli(cwd);
|
|
@@ -84,6 +89,7 @@ export function resolveFormatTargets(cwd, pathspecs = []) {
|
|
|
84
89
|
const ignoreFiles = runGit([...LIST_FILES_ARGS, '--', IGNORE_FILE_PATHSPEC], repositoryRoot);
|
|
85
90
|
if (!ignoreFiles.ok)
|
|
86
91
|
return ignoreFiles;
|
|
92
|
+
const excludedFiles = [];
|
|
87
93
|
const files = [];
|
|
88
94
|
const unreadableFiles = [];
|
|
89
95
|
const listedPaths = dedupe(splitNulSeparated(listedFiles.stdout));
|
|
@@ -91,6 +97,7 @@ export function resolveFormatTargets(cwd, pathspecs = []) {
|
|
|
91
97
|
const status = classifyListedFile(cwd, file);
|
|
92
98
|
switch (status.kind) {
|
|
93
99
|
case 'excluded':
|
|
100
|
+
excludedFiles.push(file);
|
|
94
101
|
break;
|
|
95
102
|
case 'failed':
|
|
96
103
|
return { ok: false, error: status.error };
|
|
@@ -108,6 +115,7 @@ export function resolveFormatTargets(cwd, pathspecs = []) {
|
|
|
108
115
|
return {
|
|
109
116
|
ok: true,
|
|
110
117
|
targets: {
|
|
118
|
+
excludedFiles: excludedFiles.toSorted(),
|
|
111
119
|
files: files.toSorted(),
|
|
112
120
|
ignorePaths: dedupe([path.join(repositoryRoot, IGNORE_FILENAME), ...discoveredIgnorePaths]),
|
|
113
121
|
repositoryRoot,
|
|
@@ -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' });
|