@williamthorsen/nmr 0.42.0 → 0.44.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 +105 -0
- package/.readyup/kits/default.js +1042 -137
- package/.readyup/manifest.json +151 -8
- package/CHANGELOG.md +33 -0
- package/README.md +20 -11
- package/dist/esm/commands/build-output.js +1 -1
- package/dist/esm/commands/build.js +3 -2
- package/dist/esm/commands/fmt-cache.d.ts +26 -0
- package/dist/esm/commands/fmt-cache.js +71 -0
- package/dist/esm/commands/fmt.d.ts +6 -1
- package/dist/esm/commands/fmt.js +24 -4
- package/dist/esm/config.js +28 -6
- package/dist/esm/default-scripts.d.ts +10 -1
- package/dist/esm/default-scripts.js +45 -39
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/help.d.ts +1 -1
- package/dist/esm/help.js +3 -3
- package/dist/esm/helpers/createOutputRecorder.d.ts +17 -0
- package/dist/esm/helpers/createOutputRecorder.js +22 -0
- package/dist/esm/resolve-scripts.d.ts +5 -3
- package/dist/esm/resolve-scripts.js +7 -3
- package/dist/esm/resolver.d.ts +5 -4
- package/dist/esm/resolver.js +11 -4
- package/dist/esm/runCli.js +5 -3
- package/dist/esm/runner.d.ts +7 -0
- package/dist/esm/runner.js +105 -15
- package/dist/esm/steps.d.ts +12 -5
- package/dist/esm/steps.js +5 -3
- package/dist/esm/taze.d.ts +5 -2
- package/dist/esm/taze.js +4 -1
- package/dist/esm/types.d.ts +10 -0
- package/dist/esm/vitest.js +9 -5
- package/dist/esm/workspace-globs.d.ts +5 -0
- package/dist/esm/workspace-globs.js +8 -0
- package/docs/check-cache.md +2 -0
- package/docs/scripts.md +26 -8
- package/docs/upgrades.md +1 -1
- package/docs/utilities.md +17 -3
- package/docs/vitest.md +8 -0
- package/package.json +6 -6
package/.readyup/manifest.json
CHANGED
|
@@ -3,21 +3,99 @@
|
|
|
3
3
|
"kits": [
|
|
4
4
|
{
|
|
5
5
|
"bundledDependencies": {
|
|
6
|
-
"@williamthorsen/toolbelt.errors": "0.8.
|
|
6
|
+
"@williamthorsen/toolbelt.errors": "0.8.3",
|
|
7
|
+
"@williamthorsen/toolbelt.filesystem": "0.9.3",
|
|
8
|
+
"@williamthorsen/toolbelt.terminal": "0.2.1",
|
|
9
|
+
"ansi-regex": "6.4.0",
|
|
10
|
+
"ansi-styles": "6.2.3",
|
|
11
|
+
"cli-truncate": "6.1.1",
|
|
12
|
+
"diff": "9.0.0",
|
|
13
|
+
"get-east-asian-width": "1.7.0",
|
|
14
|
+
"is-fullwidth-code-point": "5.1.0",
|
|
15
|
+
"slice-ansi": "9.0.1",
|
|
16
|
+
"string-width": "8.3.0",
|
|
17
|
+
"strip-ansi": "7.2.0",
|
|
18
|
+
"wrap-ansi": "10.0.2",
|
|
7
19
|
"yaml": "2.9.1"
|
|
8
20
|
},
|
|
9
21
|
"checklists": [
|
|
10
|
-
"nmr"
|
|
22
|
+
"nmr",
|
|
23
|
+
"self-hosted runner"
|
|
11
24
|
],
|
|
12
25
|
"esbuildVersion": "0.28.2",
|
|
13
26
|
"inputs": [
|
|
14
27
|
{
|
|
15
|
-
"hash": "
|
|
28
|
+
"hash": "aed52073",
|
|
29
|
+
"kind": "module",
|
|
30
|
+
"path": "../../nmr-core/dist/esm/cache-store.js"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"hash": "a5f44396",
|
|
34
|
+
"kind": "module",
|
|
35
|
+
"path": "../../nmr-core/dist/esm/findPackageRoot.js"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"hash": "54384dca",
|
|
39
|
+
"kind": "module",
|
|
40
|
+
"path": "../../nmr-core/dist/esm/gitOutputLimit.js"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"hash": "6b7d0243",
|
|
44
|
+
"kind": "module",
|
|
45
|
+
"path": "../../nmr-core/dist/esm/hasErrnoCode.js"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"hash": "211453c6",
|
|
49
|
+
"kind": "module",
|
|
50
|
+
"path": "../../nmr-core/dist/esm/hashWorkingTree.js"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"hash": "41232d10",
|
|
54
|
+
"kind": "module",
|
|
55
|
+
"path": "../../nmr-core/dist/esm/helpers/workspace-patterns.js"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"hash": "1467293d",
|
|
59
|
+
"kind": "module",
|
|
60
|
+
"path": "../../nmr-core/dist/esm/index.js"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"hash": "2507870d",
|
|
64
|
+
"kind": "module",
|
|
65
|
+
"path": "../../nmr-core/dist/esm/parseArgs.js"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"hash": "a7efc38f",
|
|
69
|
+
"kind": "module",
|
|
70
|
+
"path": "../../nmr-core/dist/esm/readPackageVersion.js"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"hash": "14547f78",
|
|
74
|
+
"kind": "module",
|
|
75
|
+
"path": "../../nmr-core/dist/esm/terminal.js"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"hash": "3c395f31",
|
|
79
|
+
"kind": "module",
|
|
80
|
+
"path": "../../nmr-core/dist/esm/updateManagedFile.js"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"hash": "2224ec3a",
|
|
84
|
+
"kind": "module",
|
|
85
|
+
"path": "../../nmr-core/dist/esm/workspace.js"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"hash": "24bde686",
|
|
89
|
+
"kind": "module",
|
|
90
|
+
"path": "../../nmr-core/dist/esm/writeFileWithCheck.js"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"hash": "aaeb1a84",
|
|
16
94
|
"kind": "module",
|
|
17
95
|
"path": "kits/default.ts"
|
|
18
96
|
},
|
|
19
97
|
{
|
|
20
|
-
"hash": "
|
|
98
|
+
"hash": "747d941f",
|
|
21
99
|
"kind": "inline",
|
|
22
100
|
"path": "../package.json",
|
|
23
101
|
"paths": [
|
|
@@ -25,7 +103,12 @@
|
|
|
25
103
|
]
|
|
26
104
|
},
|
|
27
105
|
{
|
|
28
|
-
"hash": "
|
|
106
|
+
"hash": "b23f5a52",
|
|
107
|
+
"kind": "module",
|
|
108
|
+
"path": "../src/config.ts"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"hash": "0fb883a1",
|
|
29
112
|
"kind": "module",
|
|
30
113
|
"path": "../src/default-scripts.ts"
|
|
31
114
|
},
|
|
@@ -35,22 +118,82 @@
|
|
|
35
118
|
"path": "../src/git-ignored-paths.ts"
|
|
36
119
|
},
|
|
37
120
|
{
|
|
38
|
-
"hash": "
|
|
121
|
+
"hash": "ae257d50",
|
|
122
|
+
"kind": "module",
|
|
123
|
+
"path": "../src/helpers/config-path.ts"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"hash": "47249ca4",
|
|
127
|
+
"kind": "module",
|
|
128
|
+
"path": "../src/helpers/findClosestName.ts"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"hash": "3efe4131",
|
|
132
|
+
"kind": "module",
|
|
133
|
+
"path": "../src/helpers/hook-name.ts"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"hash": "8850834f",
|
|
137
|
+
"kind": "module",
|
|
138
|
+
"path": "../src/helpers/package-json.ts"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"hash": "61629be6",
|
|
142
|
+
"kind": "module",
|
|
143
|
+
"path": "../src/helpers/readStringValues.ts"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"hash": "56d2bb48",
|
|
147
|
+
"kind": "module",
|
|
148
|
+
"path": "../src/helpers/type-guards.ts"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"hash": "78ca6bd4",
|
|
39
152
|
"kind": "module",
|
|
40
153
|
"path": "../src/resolve-scripts.ts"
|
|
41
154
|
},
|
|
155
|
+
{
|
|
156
|
+
"hash": "6ae507a5",
|
|
157
|
+
"kind": "module",
|
|
158
|
+
"path": "../src/resolver.ts"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"hash": "c4b31275",
|
|
162
|
+
"kind": "module",
|
|
163
|
+
"path": "../src/steps.ts"
|
|
164
|
+
},
|
|
42
165
|
{
|
|
43
166
|
"hash": "19dec003",
|
|
44
167
|
"kind": "module",
|
|
45
168
|
"path": "../src/tiers.ts"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"hash": "faad986a",
|
|
172
|
+
"kind": "module",
|
|
173
|
+
"path": "../src/UserError.ts"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"hash": "857c630c",
|
|
177
|
+
"kind": "module",
|
|
178
|
+
"path": "../src/verbosity.ts"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"hash": "f08b6ff3",
|
|
182
|
+
"kind": "module",
|
|
183
|
+
"path": "../src/workspace-globs.ts"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"hash": "922e7155",
|
|
187
|
+
"kind": "module",
|
|
188
|
+
"path": "../src/workspace.ts"
|
|
46
189
|
}
|
|
47
190
|
],
|
|
48
191
|
"name": "default",
|
|
49
192
|
"path": "kits/default.js",
|
|
50
193
|
"readyupVersion": "0.40.0",
|
|
51
194
|
"source": "kits/default.ts",
|
|
52
|
-
"sourceHash": "
|
|
53
|
-
"targetHash": "
|
|
195
|
+
"sourceHash": "aaeb1a84",
|
|
196
|
+
"targetHash": "25b7edfb"
|
|
54
197
|
}
|
|
55
198
|
]
|
|
56
199
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.44.0 — 2026-10-11
|
|
6
|
+
|
|
7
|
+
### 🎉 Features
|
|
8
|
+
|
|
9
|
+
- Enables Prettier's content-keyed cache in `nmr-fmt` (run by `nmr fmt` and `nmr fmt:check`), which skips each file whose content, Prettier version, resolved options, and lockfile are unchanged since a run that passed it, and adds `--no-cache` and `NMR_NO_CACHE=1` to bypass it. (#993)
|
|
10
|
+
- Adds the `shouldRunInBackground` field to a composite element in `.config/nmr.config.ts`, which starts the step, runs the steps after it without waiting, prints the step's output whole after the step ends, and fails the composite if the step fails. (#1003)
|
|
11
|
+
- Adds a `self-hosted runner` checklist to nmr's readyup kit, which reads the repo's GitHub settings through `gh`, recommends setting `CI_RUNS_ON` when a private repo with an online self-hosted runner and compatible workflows has not set it, and warns about each failing condition once the variable is set. (#1006)
|
|
12
|
+
- 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)
|
|
13
|
+
|
|
14
|
+
### 🪦 Removed
|
|
15
|
+
|
|
16
|
+
- 🚨 **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)
|
|
17
|
+
|
|
18
|
+
### 🐛 Bug fixes
|
|
19
|
+
|
|
20
|
+
- 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)
|
|
21
|
+
- 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)
|
|
22
|
+
|
|
23
|
+
### ⚡ Performance
|
|
24
|
+
|
|
25
|
+
- 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)
|
|
26
|
+
- 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)
|
|
27
|
+
|
|
28
|
+
### 📚 Documentation
|
|
29
|
+
|
|
30
|
+
- Documents in `docs/vitest.md` that Vitest's `--maxWorkers` flag has no effect on a machine with 2 or fewer cores, and that `VITEST_MAX_WORKERS`, `--no-file-parallelism`, or `maxWorkers` in the `project` seam overrides the default there. (#994)
|
|
31
|
+
|
|
32
|
+
## 0.43.0 — 2026-10-09
|
|
33
|
+
|
|
34
|
+
### 🎉 Features
|
|
35
|
+
|
|
36
|
+
- Extends `nmr-compile` to compile `.tsx` sources and to rewrite relative `.tsx` specifiers to `.js`, and leaves the JSX transform to the package's own tsconfig `jsx` option. (#988)
|
|
37
|
+
|
|
5
38
|
## 0.42.0 — 2026-10-08
|
|
6
39
|
|
|
7
40
|
### 🎉 Features
|
package/README.md
CHANGED
|
@@ -5,23 +5,28 @@
|
|
|
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.44.0 (2026-10-11)
|
|
9
9
|
|
|
10
10
|
### 🎉 Features
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
- Adds the
|
|
14
|
-
- Adds a `
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
- Enables Prettier's content-keyed cache in `nmr-fmt` (run by `nmr fmt` and `nmr fmt:check`), which skips each file whose content, Prettier version, resolved options, and lockfile are unchanged since a run that passed it, and adds `--no-cache` and `NMR_NO_CACHE=1` to bypass it. (#993)
|
|
13
|
+
- Adds the `shouldRunInBackground` field to a composite element in `.config/nmr.config.ts`, which starts the step, runs the steps after it without waiting, prints the step's output whole after the step ends, and fails the composite if the step fails. (#1003)
|
|
14
|
+
- Adds a `self-hosted runner` checklist to nmr's readyup kit, which reads the repo's GitHub settings through `gh`, recommends setting `CI_RUNS_ON` when a private repo with an online self-hosted runner and compatible workflows has not set it, and warns about each failing condition once the variable is set. (#1006)
|
|
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)
|
|
18
20
|
|
|
19
21
|
### 🐛 Bug fixes
|
|
20
22
|
|
|
21
|
-
- Fixes the issue that `
|
|
22
|
-
- Fixes the issue that `
|
|
23
|
-
|
|
24
|
-
|
|
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)
|
|
25
30
|
<!-- /section:release-notes -->
|
|
26
31
|
|
|
27
32
|
## Installation
|
|
@@ -125,6 +130,8 @@ Scripts resolve through three tiers. Higher tiers override lower ones:
|
|
|
125
130
|
|
|
126
131
|
Every `nmr X` invocation auto-wraps as the equivalent of `nmr X:pre && nmr X && nmr X:post`. Hooks are first-class scripts that resolve through the same three tiers as any other script (built-in defaults, then `.config/nmr.config.ts`, then per-package `package.json`). Wrapping is uniform: Nested invocations from composite expansion get their own hook treatment. A failing hook ends the sequence, and its exit code propagates.
|
|
127
132
|
|
|
133
|
+
A composite runs its steps in series, except a step declared with `shouldRunInBackground`, which runs alongside the steps after it. The default `check` and `check:strict` run lint that way; [Running a step in the background](docs/scripts.md#running-a-step-in-the-background) covers how its output prints and how it fails.
|
|
134
|
+
|
|
128
135
|
Extend `nmr build` with a pre-build step for every workspace package:
|
|
129
136
|
|
|
130
137
|
```ts
|
|
@@ -267,6 +274,8 @@ Wiring the sync to `postinstall` keeps it current without a hand-run command:
|
|
|
267
274
|
|
|
268
275
|
nmr publishes a `readyup` kit that checks a consuming repo against the current release: the shared Vitest and Prettier configs, the workspace layout, the root script registry, the test-file conventions ([tier names](docs/vitest.md#test-tiers) and `__tests__` placement), that the repo's own suite gates them, that every `bin` target is a committed wrapper rather than build output that pnpm cannot link at install time, that `files` covers the output that each wrapper loads, that the root install scripts do not run `lefthook install` without `lefthook check-install`, and that the `package.json` files in the tree declare neither a `pnpm` field, from which pnpm 11 does not read any key, nor a `workspaces` field, which pnpm ignores. Those settings belong in `pnpm-workspace.yaml`, and `pnpx codemod run pnpm-v10-to-v11` moves the `pnpm` field's. It also checks that every manifest declares a package listed by a `pnpm-workspace.yaml` catalog as `catalog:` rather than as a literal version. A peer range is exempt, and a version that must differ belongs in a named catalog, referenced as `catalog:<name>`. The kit is included in the package, so it checks against the nmr version installed rather than whatever a repository ref happens to point at, and a tier added or renamed in nmr takes effect in the repo when it upgrades nmr. A repo that declares the [conventions check](docs/vitest.md#gating-the-test-file-conventions) gates both conventions in its own test run, and the kit then leaves the test files to that check.
|
|
269
276
|
|
|
277
|
+
A second checklist, `self-hosted runner`, reads the repo's GitHub settings through `gh` and recommends setting `CI_RUNS_ON` to `"self-hosted"` when the repo is private, has an online `self-hosted` runner, does not pin a code-quality workflow older than v8 or override its `runs-on`, and runs Node.js and pnpm only after a step installs them and does not run `gh`, which the image lacks. Once the variable is set, the checklist warns about each of those conditions that fails. It skips when `gh` is not installed or not authenticated. A repo that qualifies but keeps its CI off the runners records why in the root config's [`selfHostedRunner.exempt`](docs/scripts.md#defineconfig-fields).
|
|
278
|
+
|
|
270
279
|
Add `readyup` as a devDependency, then name nmr in its config:
|
|
271
280
|
|
|
272
281
|
```ts
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { findPackageRoot, readCacheEntry, resolveCacheEntryPath } from '@williamthorsen/nmr-core';
|
|
4
4
|
import { glob } from 'glob';
|
|
5
5
|
import { readPackageJson } from "../helpers/package-json.js";
|
|
6
|
-
export const DEFAULT_ENTRY_GLOBS = ['src/**/*.ts'];
|
|
6
|
+
export const DEFAULT_ENTRY_GLOBS = ['src/**/*.{ts,tsx}'];
|
|
7
7
|
export const DEFAULT_IGNORE_PATTERNS = ['**/__fixtures__/**', '**/__mocks__/**', '**/__tests__/**', '**/test-utils/**'];
|
|
8
8
|
export const DEFAULT_OUTDIR = 'dist/esm/';
|
|
9
9
|
const BUILD_CACHE_TOOL = 'nmr-compile';
|
|
@@ -13,7 +13,7 @@ const MINIMUM_TYPESCRIPT_MINOR = 7;
|
|
|
13
13
|
const UNSUPPORTED_TYPESCRIPT_MAJOR = 7;
|
|
14
14
|
const SUPPORTED_TYPESCRIPT_RANGE = `>=${MINIMUM_TYPESCRIPT_MAJOR}.${MINIMUM_TYPESCRIPT_MINOR} <${UNSUPPORTED_TYPESCRIPT_MAJOR}`;
|
|
15
15
|
const INSTALL_TYPESCRIPT_GUIDANCE = "Install a version in that range as the 'typescript' peer dependency.";
|
|
16
|
-
const
|
|
16
|
+
const TS_EXTENSIONS = ['.ts', '.tsx'];
|
|
17
17
|
const JS_EXTENSION = '.js';
|
|
18
18
|
let ts;
|
|
19
19
|
export async function buildPackage(packageDir, options) {
|
|
@@ -401,5 +401,6 @@ function resolveScriptKind(file) {
|
|
|
401
401
|
return file.endsWith('.d.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS;
|
|
402
402
|
}
|
|
403
403
|
function swapTypeScriptExtension(specifier) {
|
|
404
|
-
|
|
404
|
+
const extension = TS_EXTENSIONS.find((candidate) => specifier.endsWith(candidate));
|
|
405
|
+
return extension === undefined ? specifier : `${specifier.slice(0, -extension.length)}${JS_EXTENSION}`;
|
|
405
406
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The directory, relative to the repository root, holding one Prettier cache file per scope. */
|
|
2
|
+
export declare const FMT_CACHE_DIRECTORY: string;
|
|
3
|
+
export interface PrepareFmtCacheOptions {
|
|
4
|
+
cwd: string;
|
|
5
|
+
env: NodeJS.ProcessEnv;
|
|
6
|
+
repositoryRoot: string;
|
|
7
|
+
shouldBypassCache: boolean;
|
|
8
|
+
}
|
|
9
|
+
export type PrepareFmtCacheResult = {
|
|
10
|
+
kind: 'cached';
|
|
11
|
+
location: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'uncached';
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'failed';
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Resolves the Prettier cache file for this scope, after pruning cache files keyed to an outdated lockfile and
|
|
20
|
+
* discarding a corrupt one, or reports why the run goes uncached.
|
|
21
|
+
*
|
|
22
|
+
* The file name combines a hash of the scope with a fingerprint of the repository's lockfiles. Prettier keys
|
|
23
|
+
* entries by path relative to its working directory, so each scope needs a file of its own; Prettier's own key
|
|
24
|
+
* covers its version and the resolved options but not plugin code, which the lockfile fingerprint covers.
|
|
25
|
+
*/
|
|
26
|
+
export declare function prepareFmtCache(options: PrepareFmtCacheOptions): PrepareFmtCacheResult;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
5
|
+
import { NO_CACHE_ENV_VAR } from "../check-cache.js";
|
|
6
|
+
export const FMT_CACHE_DIRECTORY = path.join('node_modules', '.cache', 'prettier', 'nmr-fmt');
|
|
7
|
+
const LOCKFILE_NAMES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'bun.lock', 'bun.lockb'];
|
|
8
|
+
const NO_LOCKFILE_MARKER = 'nolock';
|
|
9
|
+
const HASH_LENGTH = 16;
|
|
10
|
+
const CACHE_FILE_PATTERN = /^([0-9a-f]{16})\.([0-9a-f]{16}|nolock)\.json$/;
|
|
11
|
+
export function prepareFmtCache(options) {
|
|
12
|
+
const { cwd, env, repositoryRoot, shouldBypassCache } = options;
|
|
13
|
+
if (shouldBypassCache || env[NO_CACHE_ENV_VAR] === '1')
|
|
14
|
+
return { kind: 'uncached' };
|
|
15
|
+
try {
|
|
16
|
+
if (statSync(path.join(repositoryRoot, 'node_modules'), { throwIfNoEntry: false })?.isDirectory() !== true) {
|
|
17
|
+
return { kind: 'uncached' };
|
|
18
|
+
}
|
|
19
|
+
const cacheDirectory = path.join(repositoryRoot, FMT_CACHE_DIRECTORY);
|
|
20
|
+
mkdirSync(cacheDirectory, { recursive: true });
|
|
21
|
+
const lockfileHash = hashLockfiles(repositoryRoot);
|
|
22
|
+
pruneOutdatedCacheFiles(cacheDirectory, lockfileHash);
|
|
23
|
+
const scope = path.relative(repositoryRoot, realpathSync(cwd)).split(path.sep).join('/');
|
|
24
|
+
const location = path.join(cacheDirectory, `${hash(scope)}.${lockfileHash}.json`);
|
|
25
|
+
if (isCorruptCacheFile(location))
|
|
26
|
+
rmSync(location, { force: true });
|
|
27
|
+
return { kind: 'cached', location };
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return { kind: 'failed', reason: describeError(error) };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function hash(input) {
|
|
34
|
+
return createHash('sha256').update(input).digest('hex').slice(0, HASH_LENGTH);
|
|
35
|
+
}
|
|
36
|
+
function hashLockfiles(repositoryRoot) {
|
|
37
|
+
const digest = createHash('sha256');
|
|
38
|
+
let hasLockfile = false;
|
|
39
|
+
for (const name of LOCKFILE_NAMES) {
|
|
40
|
+
const lockfilePath = path.join(repositoryRoot, name);
|
|
41
|
+
if (statSync(lockfilePath, { throwIfNoEntry: false })?.isFile() !== true)
|
|
42
|
+
continue;
|
|
43
|
+
digest.update(`${name}\0`).update(readFileSync(lockfilePath)).update('\0');
|
|
44
|
+
hasLockfile = true;
|
|
45
|
+
}
|
|
46
|
+
return hasLockfile ? digest.digest('hex').slice(0, HASH_LENGTH) : NO_LOCKFILE_MARKER;
|
|
47
|
+
}
|
|
48
|
+
function isCorruptCacheFile(location) {
|
|
49
|
+
let content;
|
|
50
|
+
try {
|
|
51
|
+
content = readFileSync(location, 'utf8');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
JSON.parse(content);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function pruneOutdatedCacheFiles(cacheDirectory, lockfileHash) {
|
|
65
|
+
for (const name of readdirSync(cacheDirectory)) {
|
|
66
|
+
const match = CACHE_FILE_PATTERN.exec(name);
|
|
67
|
+
if (match !== null && match[2] !== lockfileHash) {
|
|
68
|
+
rmSync(path.join(cacheDirectory, name), { force: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -4,6 +4,8 @@ export interface FormatTargets {
|
|
|
4
4
|
files: string[];
|
|
5
5
|
/** Absolute paths to every `.prettierignore` governing the repository, root-most first. */
|
|
6
6
|
ignorePaths: string[];
|
|
7
|
+
/** The repository's top-level directory, as git reports it. */
|
|
8
|
+
repositoryRoot: string;
|
|
7
9
|
/** Listed paths that this process is denied access to, each with the errno code that its stat raised. */
|
|
8
10
|
unreadableFiles: UnreadableFile[];
|
|
9
11
|
}
|
|
@@ -20,7 +22,8 @@ export type ResolveTargetsResult = {
|
|
|
20
22
|
};
|
|
21
23
|
/**
|
|
22
24
|
* Formats or checks the files that git reports for `cwd`, and returns the exit code to report. A repository
|
|
23
|
-
* without a Prettier config of its own is formatted with the house config.
|
|
25
|
+
* without a Prettier config of its own is formatted with the house config. The run uses Prettier's content-keyed
|
|
26
|
+
* cache unless `--no-cache` or `NMR_NO_CACHE=1` bypasses it.
|
|
24
27
|
*
|
|
25
28
|
* Trailing arguments are git pathspecs, not Prettier flags; an unrecognized option is rejected rather
|
|
26
29
|
* than passed along, since git would read it as a pathspec matching nothing and the run would report
|
|
@@ -46,6 +49,8 @@ export declare function resolveFormatTargets(cwd: string, pathspecs?: string[]):
|
|
|
46
49
|
/** @internal */
|
|
47
50
|
export interface RunPrettierOptions {
|
|
48
51
|
cliPath: string;
|
|
52
|
+
/** Absolute path of the cache file to pass to Prettier; the run is uncached when it is absent. */
|
|
53
|
+
cacheLocation?: string | undefined;
|
|
49
54
|
/** Passed as `--config`, which replaces Prettier's own config discovery. */
|
|
50
55
|
configPath?: string | undefined;
|
|
51
56
|
mode: FormatMode;
|
package/dist/esm/commands/fmt.js
CHANGED
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { hasErrnoCode, reportError } from '@williamthorsen/nmr-core';
|
|
9
9
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
10
10
|
import { isObject } from "../helpers/type-guards.js";
|
|
11
|
+
import { prepareFmtCache } from "./fmt-cache.js";
|
|
11
12
|
const IGNORE_FILENAME = '.prettierignore';
|
|
12
13
|
const LIST_FILES_ARGS = ['ls-files', '-z', '--cached', '--others', '--exclude-standard'];
|
|
13
14
|
const IGNORE_FILE_PATHSPEC = `*${IGNORE_FILENAME}`;
|
|
@@ -15,7 +16,7 @@ const UNREADABLE_CODES = ['EACCES', 'EPERM'];
|
|
|
15
16
|
const PRETTIER_PACKAGE = 'prettier';
|
|
16
17
|
const DEFAULT_CONFIG_PATH = fileURLToPath(new URL(`../prettier-default-config${path.extname(fileURLToPath(import.meta.url))}`, import.meta.url));
|
|
17
18
|
const ARGUMENT_BUDGET_BYTES = 100_000;
|
|
18
|
-
const USAGE = 'Usage: nmr-fmt (--check | --write) [pathspec...]';
|
|
19
|
+
const USAGE = 'Usage: nmr-fmt (--check | --write) [--no-cache] [pathspec...]';
|
|
19
20
|
const MODE_ARGS = {
|
|
20
21
|
check: ['--check'],
|
|
21
22
|
write: ['--list-different', '--write'],
|
|
@@ -32,7 +33,7 @@ export async function runFmt(argv, cwd = process.cwd()) {
|
|
|
32
33
|
reportError(`nmr-fmt: ${resolvedTargets.error}`);
|
|
33
34
|
return 1;
|
|
34
35
|
}
|
|
35
|
-
const { files, ignorePaths, unreadableFiles } = resolvedTargets.targets;
|
|
36
|
+
const { files, ignorePaths, repositoryRoot, unreadableFiles } = resolvedTargets.targets;
|
|
36
37
|
for (const { file, code } of unreadableFiles) {
|
|
37
38
|
process.stderr.write(`nmr-fmt: skipped ${file}, which cannot be read (${code})\n`);
|
|
38
39
|
}
|
|
@@ -53,7 +54,17 @@ export async function runFmt(argv, cwd = process.cwd()) {
|
|
|
53
54
|
reportError(`nmr-fmt: ${repositoryConfig.error}`);
|
|
54
55
|
return 1;
|
|
55
56
|
}
|
|
57
|
+
const cache = prepareFmtCache({
|
|
58
|
+
cwd,
|
|
59
|
+
env: process.env,
|
|
60
|
+
repositoryRoot,
|
|
61
|
+
shouldBypassCache: parsedArgs.shouldBypassCache,
|
|
62
|
+
});
|
|
63
|
+
if (cache.kind === 'failed') {
|
|
64
|
+
process.stderr.write(`nmr-fmt: running without the Prettier cache: ${cache.reason}\n`);
|
|
65
|
+
}
|
|
56
66
|
return runPrettier({
|
|
67
|
+
cacheLocation: cache.kind === 'cached' ? cache.location : undefined,
|
|
57
68
|
cliPath: cli.cliPath,
|
|
58
69
|
configPath: repositoryConfig.found ? undefined : DEFAULT_CONFIG_PATH,
|
|
59
70
|
mode: parsedArgs.mode,
|
|
@@ -99,6 +110,7 @@ export function resolveFormatTargets(cwd, pathspecs = []) {
|
|
|
99
110
|
targets: {
|
|
100
111
|
files: files.toSorted(),
|
|
101
112
|
ignorePaths: dedupe([path.join(repositoryRoot, IGNORE_FILENAME), ...discoveredIgnorePaths]),
|
|
113
|
+
repositoryRoot,
|
|
102
114
|
unreadableFiles,
|
|
103
115
|
},
|
|
104
116
|
};
|
|
@@ -117,8 +129,13 @@ function classifyListedFile(cwd, file) {
|
|
|
117
129
|
}
|
|
118
130
|
function parseFmtArgs(argv) {
|
|
119
131
|
let mode;
|
|
132
|
+
let shouldBypassCache = false;
|
|
120
133
|
const pathspecs = [];
|
|
121
134
|
for (const arg of argv) {
|
|
135
|
+
if (arg === '--no-cache') {
|
|
136
|
+
shouldBypassCache = true;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
122
139
|
if (arg === '--check' || arg === '--write') {
|
|
123
140
|
const requestedMode = arg === '--check' ? 'check' : 'write';
|
|
124
141
|
if (mode !== undefined && mode !== requestedMode) {
|
|
@@ -135,7 +152,7 @@ function parseFmtArgs(argv) {
|
|
|
135
152
|
if (mode === undefined) {
|
|
136
153
|
return { ok: false, error: 'Pass --check or --write.' };
|
|
137
154
|
}
|
|
138
|
-
return { ok: true, mode, pathspecs };
|
|
155
|
+
return { ok: true, mode, pathspecs, shouldBypassCache };
|
|
139
156
|
}
|
|
140
157
|
function resolvePrettierCli(cwd) {
|
|
141
158
|
const missingResult = {
|
|
@@ -172,12 +189,15 @@ async function findRepositoryConfig(manifestPath, cwd) {
|
|
|
172
189
|
}
|
|
173
190
|
}
|
|
174
191
|
export function runPrettier(options) {
|
|
175
|
-
const { cliPath, configPath, mode, files, ignorePaths, cwd, budgetBytes = ARGUMENT_BUDGET_BYTES } = options;
|
|
192
|
+
const { cacheLocation, cliPath, configPath, mode, files, ignorePaths, cwd, budgetBytes = ARGUMENT_BUDGET_BYTES, } = options;
|
|
176
193
|
const prettierArgs = [
|
|
177
194
|
cliPath,
|
|
178
195
|
...(configPath === undefined ? [] : ['--config', configPath]),
|
|
179
196
|
'--ignore-unknown',
|
|
180
197
|
...ignorePaths.flatMap((ignorePath) => ['--ignore-path', ignorePath]),
|
|
198
|
+
...(cacheLocation === undefined
|
|
199
|
+
? []
|
|
200
|
+
: ['--cache', '--cache-strategy', 'content', '--cache-location', cacheLocation]),
|
|
181
201
|
...MODE_ARGS[mode],
|
|
182
202
|
];
|
|
183
203
|
let firstFailureStatus = 0;
|
package/dist/esm/config.js
CHANGED
|
@@ -20,7 +20,7 @@ import { getWorkspacePackageDirs, isMonorepoRoot } from "./workspace.js";
|
|
|
20
20
|
const CONFIG_TIERS = {
|
|
21
21
|
root: {
|
|
22
22
|
label: 'a monorepo-root config',
|
|
23
|
-
honoredKeys: ['checkCache', 'devBin', 'output', 'rootScripts', 'workspaceScripts'],
|
|
23
|
+
honoredKeys: ['checkCache', 'devBin', 'output', 'rootScripts', 'selfHostedRunner', 'workspaceScripts'],
|
|
24
24
|
elsewhere: "the package's own config",
|
|
25
25
|
},
|
|
26
26
|
workspace: {
|
|
@@ -33,7 +33,8 @@ const RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.works
|
|
|
33
33
|
const RECOGNIZED_BUILD_KEYS = ['extraIgnorePatterns'];
|
|
34
34
|
const RECOGNIZED_CHECK_CACHE_KEYS = ['enabled', 'excludeCommands', 'extraCommands'];
|
|
35
35
|
const RECOGNIZED_OUTPUT_KEYS = ['commandVerbosity', 'extraAgentEnvVars'];
|
|
36
|
-
const
|
|
36
|
+
const RECOGNIZED_SELF_HOSTED_RUNNER_KEYS = ['exempt'];
|
|
37
|
+
const RECOGNIZED_STEP_KEYS = ['run', 'shouldDeclineArguments', 'shouldRunInBackground'];
|
|
37
38
|
const RETIRED_STEP_KEYS = new Map([['declinesArgs', 'shouldDeclineArguments']]);
|
|
38
39
|
function isScriptRecord(value) {
|
|
39
40
|
if (!isObject(value))
|
|
@@ -51,8 +52,7 @@ function isScriptElement(value) {
|
|
|
51
52
|
return true;
|
|
52
53
|
if (!isObject(value) || typeof value['run'] !== 'string')
|
|
53
54
|
return false;
|
|
54
|
-
|
|
55
|
-
return shouldDeclineArguments === undefined || typeof shouldDeclineArguments === 'boolean';
|
|
55
|
+
return isOptionalBoolean(value['shouldDeclineArguments']) && isOptionalBoolean(value['shouldRunInBackground']);
|
|
56
56
|
}
|
|
57
57
|
function validateScriptField(value, fieldName, configPath) {
|
|
58
58
|
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
@@ -61,7 +61,8 @@ function validateScriptField(value, fieldName, configPath) {
|
|
|
61
61
|
const scripts = value[fieldName];
|
|
62
62
|
if (!isScriptRecord(scripts)) {
|
|
63
63
|
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | element[]>, ` +
|
|
64
|
-
'where an element is a command string or
|
|
64
|
+
'where an element is a command string or ' +
|
|
65
|
+
'`{ run: string, shouldDeclineArguments?: boolean, shouldRunInBackground?: boolean }`');
|
|
65
66
|
}
|
|
66
67
|
assertValidElements(scripts, fieldName, configPath);
|
|
67
68
|
return scripts;
|
|
@@ -85,6 +86,9 @@ function assertValidElements(scripts, fieldName, configPath) {
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
}
|
|
89
|
+
function isOptionalBoolean(value) {
|
|
90
|
+
return value === undefined || typeof value === 'boolean';
|
|
91
|
+
}
|
|
88
92
|
function isStringArray(value) {
|
|
89
93
|
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
90
94
|
}
|
|
@@ -132,6 +136,21 @@ function validateCheckCacheField(value, configPath) {
|
|
|
132
136
|
}
|
|
133
137
|
return config;
|
|
134
138
|
}
|
|
139
|
+
function validateSelfHostedRunnerField(value, configPath) {
|
|
140
|
+
const selfHostedRunner = value['selfHostedRunner'];
|
|
141
|
+
if (selfHostedRunner === undefined) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
if (!isObject(selfHostedRunner)) {
|
|
145
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`selfHostedRunner\` must be an object`);
|
|
146
|
+
}
|
|
147
|
+
assertRecognizedKeys(selfHostedRunner, RECOGNIZED_SELF_HOSTED_RUNNER_KEYS, configPath, 'selfHostedRunner.');
|
|
148
|
+
const exempt = selfHostedRunner['exempt'];
|
|
149
|
+
if (typeof exempt !== 'string' || exempt.trim() === '') {
|
|
150
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`selfHostedRunner.exempt\` must be a non-empty string giving the reason`);
|
|
151
|
+
}
|
|
152
|
+
return { exempt };
|
|
153
|
+
}
|
|
135
154
|
function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
|
|
136
155
|
const { checkCache } = config;
|
|
137
156
|
if (checkCache === undefined || !isMonorepoRoot(baseDir)) {
|
|
@@ -145,7 +164,7 @@ function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
|
|
|
145
164
|
'which is never gated on its own: It runs as part of the chain of the command that it wraps.');
|
|
146
165
|
}
|
|
147
166
|
const registeredNames = new Set([
|
|
148
|
-
...Object.keys(buildRootRegistry(config)),
|
|
167
|
+
...Object.keys(buildRootRegistry(config, baseDir)),
|
|
149
168
|
...Object.keys(buildWorkspaceRegistry(config)),
|
|
150
169
|
]);
|
|
151
170
|
const missingEntries = entries.filter(({ command }) => !registeredNames.has(command));
|
|
@@ -250,6 +269,9 @@ function validateConfig(value, configPath, baseDir) {
|
|
|
250
269
|
const rootScripts = validateScriptField(value, 'rootScripts', configPath);
|
|
251
270
|
if (rootScripts)
|
|
252
271
|
config.rootScripts = rootScripts;
|
|
272
|
+
const selfHostedRunner = validateSelfHostedRunnerField(value, configPath);
|
|
273
|
+
if (selfHostedRunner)
|
|
274
|
+
config.selfHostedRunner = selfHostedRunner;
|
|
253
275
|
assertResolvableCheckCacheCommands(config, configPath, baseDir);
|
|
254
276
|
return config;
|
|
255
277
|
}
|