@kitschpatrol/shared-config 8.12.1 → 8.13.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/bin/cli.js +1 -1
- package/package.json +15 -14
- package/readme.md +8 -2
- package/skills/ksc-update/SKILL.md +419 -693
- package/skills/ksc-update/references/configuration.md +373 -0
- package/skills/ksc-update/references/verification.md +256 -134
- package/skills/ksc-update/scripts/compare-outputs.mjs +144 -0
- package/skills/ksc-update/scripts/consumer-ranges.mjs +127 -0
- package/skills/ksc-update/scripts/lib.mjs +307 -0
- package/skills/ksc-update/scripts/packed-fixture.mjs +247 -0
- package/skills/ksc-update/scripts/production-engines.mjs +119 -0
- package/skills/ksc-update/scripts/release-age.mjs +174 -0
- package/skills/ksc-update/scripts/run-at-node.mjs +108 -0
|
@@ -1,9 +1,44 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Verification recipes
|
|
2
2
|
|
|
3
3
|
Use these recipes during Setup and step 5. Save commands, versions, raw logs,
|
|
4
|
-
and results
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
and results in the run directory, `$KSC_UPDATE_RUN_DIR`, created once using the
|
|
5
|
+
[Environment instructions](../SKILL.md#environment),
|
|
6
|
+
under `baseline/` and `updated/`. Capture output without losing exit codes and
|
|
7
|
+
use the runner's normal permissions.
|
|
8
|
+
|
|
9
|
+
## Baselines
|
|
10
|
+
|
|
11
|
+
Keep these states distinct:
|
|
12
|
+
|
|
13
|
+
- **Original checkout:** the starting manifests, lockfile, and policy settings
|
|
14
|
+
copied before any Setup edit. Identify the original locked tree without
|
|
15
|
+
claiming it passed checks that only ran later.
|
|
16
|
+
- **Setup baseline:** every install attempt and build, test, lint, inventory,
|
|
17
|
+
and advisory output under `baseline/`, with the actual pnpm version, policy
|
|
18
|
+
changes, commands, environment, and lockfile diff. Label results after a
|
|
19
|
+
lockfile repair as repaired. A failure here predates the dependency updates
|
|
20
|
+
but may have been introduced by Setup; call it pre-existing only when evidence
|
|
21
|
+
shows it.
|
|
22
|
+
- **Updated result:** compared with the Setup baseline, with every change
|
|
23
|
+
explained.
|
|
24
|
+
- **Published release:** a fresh install resolving the currently allowed
|
|
25
|
+
versions is not the historical dependency tree; that needs a known lockfile or
|
|
26
|
+
preserved installation. A registry tarball alone does not recover it, so
|
|
27
|
+
report missing historical evidence rather than relabeling a fresh install.
|
|
28
|
+
|
|
29
|
+
For build-producing repositories, save the baseline artifacts such as `dist/`,
|
|
30
|
+
disable nondeterministic steps such as encryption or timestamps through
|
|
31
|
+
repository switches, and record the matching commands and environment for later
|
|
32
|
+
comparison.
|
|
33
|
+
|
|
34
|
+
For each published package, record declared `engines.node`, the effective
|
|
35
|
+
consumer minimum and full supported range, and separate development
|
|
36
|
+
requirements. Inspect published dependency trees and artifacts, not just the
|
|
37
|
+
root or the current CI runtime. When support is undeclared, derive the existing
|
|
38
|
+
requirement from published code and dependencies rather than assuming the
|
|
39
|
+
developer runtime is the consumer floor. Capture the production inventory,
|
|
40
|
+
advisory report, isolated consumer installs, and representative fixture results
|
|
41
|
+
below before updates, and repeat them for the updated result.
|
|
7
42
|
|
|
8
43
|
## Production-tree engines
|
|
9
44
|
|
|
@@ -13,156 +48,243 @@ For each published package, run in its directory before and after updates:
|
|
|
13
48
|
pnpm list --prod --depth Infinity --json
|
|
14
49
|
```
|
|
15
50
|
|
|
16
|
-
Save stdout as `production-tree.json` in the
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
not only a private root with no production dependencies.
|
|
51
|
+
Save stdout as `production-tree.json` in the matching results folder. If stdout
|
|
52
|
+
contains lines beginning with `[WARN]`, keep those warnings separately and strip
|
|
53
|
+
only those lines before parsing. Require a successful command and valid JSON;
|
|
54
|
+
never extract a JSON-looking substring from failed output. In a monorepo include
|
|
55
|
+
every published package, not only a private root.
|
|
22
56
|
|
|
23
|
-
The list supplies installed paths
|
|
24
|
-
actual engine declaration.
|
|
25
|
-
|
|
57
|
+
The list supplies installed paths, and each installed `package.json` holds the
|
|
58
|
+
actual engine declaration. The skill's `scripts/production-engines.mjs` walks
|
|
59
|
+
the tree and prints one sorted row of name, version, and declared `engines.node`
|
|
60
|
+
per installed package:
|
|
26
61
|
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
|
|
62
|
+
```bash
|
|
63
|
+
node "<skill-directory>/scripts/production-engines.mjs" production-tree.json > production-engines.txt
|
|
64
|
+
```
|
|
30
65
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
66
|
+
Save the inventory and diff it after resolution and deduplication. A row with
|
|
67
|
+
`installed: false` is a dependency this host did not install, usually a
|
|
68
|
+
platform-specific optional dependency; review its engines from registry
|
|
69
|
+
metadata, because one host's installed tree cannot prove it. An omitted `node`
|
|
70
|
+
field means undeclared support, not compatibility. Check full semver ranges
|
|
71
|
+
against every supported consumer Node.js line, and check shipped or bundled code
|
|
72
|
+
separately. This inventory does not replace the newest-in-range registry review.
|
|
35
73
|
|
|
36
|
-
|
|
37
|
-
const rows = new Set()
|
|
74
|
+
## Security advisories
|
|
38
75
|
|
|
39
|
-
|
|
40
|
-
if (!pkg.path) {
|
|
41
|
-
throw new Error(`Missing installed path: ${pkg.name ?? pkg.version}`)
|
|
42
|
-
}
|
|
76
|
+
Before and after updates, in each workspace root:
|
|
43
77
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
78
|
+
```bash
|
|
79
|
+
pnpm audit --prod --json > audit.json
|
|
80
|
+
echo $? > audit.exit
|
|
81
|
+
```
|
|
47
82
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}),
|
|
55
|
-
)
|
|
56
|
-
for (const group of ['dependencies', 'optionalDependencies']) {
|
|
57
|
-
const children = Object.values(pkg[group] ?? {})
|
|
58
|
-
for (const child of children) {
|
|
59
|
-
visit(child)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
83
|
+
Diff the two reports and list advisories that are new, resolved, or still open,
|
|
84
|
+
with their advisory identifiers and dependency paths. Never run `pnpm audit
|
|
85
|
+
--fix`: it writes overrides and release-age exclusions outside the override and
|
|
86
|
+
exemption policies. Resolve an open advisory through an accepted update or a
|
|
87
|
+
reviewed override under the configuration policy, and otherwise report it as a
|
|
88
|
+
gap.
|
|
63
89
|
|
|
64
|
-
|
|
65
|
-
visit(root)
|
|
66
|
-
}
|
|
90
|
+
## Packed builds
|
|
67
91
|
|
|
68
|
-
|
|
69
|
-
```
|
|
92
|
+
Use this repository's tests and CLI fixtures for baseline comparisons.
|
|
70
93
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
`AGENTS.md`. A useful `Known consumers` entry contains the consumer's name and
|
|
83
|
-
path, which package/imports/CLI it uses, its test command, and representative
|
|
84
|
-
CLI commands and fixture inputs. Preserve existing instructions; add only facts
|
|
85
|
-
confirmed by the user or repository evidence.
|
|
86
|
-
|
|
87
|
-
1. Create a temporary directory under `${TMPDIR:-/tmp}`. Clone each local
|
|
88
|
-
consumer into it with `git clone --no-hardlinks <consumer-path> <temp-copy>`.
|
|
89
|
-
Record the commit. A clone excludes uncommitted changes; if those matter,
|
|
90
|
-
explicitly copy the needed working-tree files and record that baseline.
|
|
91
|
-
Do not modify the consumer's original checkout.
|
|
92
|
-
2. Install and test the copied consumer with its starting lockfile. Save
|
|
93
|
-
its tests' results and representative CLI stdout, stderr, and exit codes.
|
|
94
|
-
Preserve this copy for baseline comparison.
|
|
95
|
-
3. Build the updated package with its development Node.js version and run
|
|
96
|
-
`pnpm pack` as a bare command in that package. Inspect the archive's contents
|
|
97
|
-
and manifest, including exported entrypoints, engines, and dependency ranges.
|
|
98
|
-
4. In a second consumer copy, replace the consumed dependency with a `file:`
|
|
99
|
-
specifier pointing to that tarball, using a direct manifest edit. Install
|
|
100
|
-
and rerun the same tests and CLI fixtures. If a workspace-local dependency
|
|
101
|
-
or alias is involved, replace the actual consuming edge and verify the
|
|
102
|
-
resolved path/version; do not assume a root-only edit reaches it. A linked
|
|
103
|
-
built package is useful for iteration, but the tarball verifies what ships
|
|
104
|
-
and avoids a link silently borrowing the producer's `node_modules`.
|
|
105
|
-
5. Diff CLI output and fixture results. Inspect semantic differences, including
|
|
106
|
-
behavior JSON may hide, such as object prototypes, missing versus undefined
|
|
107
|
-
values, ordering, and error handling. Explain each change against accepted
|
|
108
|
-
migrations and consumer expectations.
|
|
94
|
+
1. Build the package with its development Node.js version and run `pnpm pack` in
|
|
95
|
+
that package. Inspect the archive's contents and manifest, including exported
|
|
96
|
+
entrypoints, engines, and dependency ranges.
|
|
97
|
+
2. Create an isolated fixture under the run directory with a `file:` dependency
|
|
98
|
+
on that tarball. Install it and run representative API and CLI checks with
|
|
99
|
+
the repository's fixtures. Verify the resolved path and version; the tarball
|
|
100
|
+
checks what ships without borrowing the producer's `node_modules`.
|
|
101
|
+
3. Diff CLI output and fixture results against the Setup baseline. Inspect
|
|
102
|
+
semantic differences that JSON can hide, such as object prototypes, missing
|
|
103
|
+
versus undefined values, ordering, and error handling, and explain each
|
|
104
|
+
change against accepted migrations and consumer expectations.
|
|
109
105
|
|
|
110
106
|
Also test a fresh isolated install of the published release with the newest
|
|
111
|
-
dependencies its ranges allow
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
dependencies its ranges allow, kept distinct from the historical state as the
|
|
108
|
+
Baselines section describes.
|
|
109
|
+
|
|
110
|
+
`scripts/packed-fixture.mjs <state>` performs steps 1 and 2 up to the install:
|
|
111
|
+
it packs every published workspace package, records each tarball's manifest and
|
|
112
|
+
file list in `packed-manifests.json`, and installs a fixture under `<run
|
|
113
|
+
directory>/<state>/fixture`. With `--published name@version` it installs the
|
|
114
|
+
registry release instead, for the fresh-install check. Run the representative
|
|
115
|
+
checks inside the printed fixture directory yourself. Pitfalls it handles, which
|
|
116
|
+
a hand-built fixture must handle too:
|
|
117
|
+
|
|
118
|
+
- Packing rewrites `workspace:` ranges to registry versions, so a fixture that
|
|
119
|
+
depends only on one tarball silently installs its siblings from the registry.
|
|
120
|
+
Override every packed sibling to its tarball.
|
|
121
|
+
- Some consumer setup must exist before installing, for example an init command
|
|
122
|
+
that writes `publicHoistPattern` so bins and plugins resolve. Follow the
|
|
123
|
+
documented consumer installation order; `--before-install` runs such a
|
|
124
|
+
command, with `FIXTURE_TARBALLS` naming a JSON map of package to tarball for
|
|
125
|
+
`dlx` against a packed package.
|
|
126
|
+
- Exported JSON or JSONC files, such as tsconfig presets, cannot be imported as
|
|
127
|
+
ES modules; check them with the consuming tool, for example `tsc
|
|
128
|
+
--showConfig`.
|
|
129
|
+
- Fixers mutate files in the fixture, including untracked probe scripts. Restore
|
|
130
|
+
the tree (`git checkout -- .` after committing the setup, and regenerate
|
|
131
|
+
untracked probes) before every run that should be comparable.
|
|
132
|
+
- pnpm may append `minimumReleaseAgeExclude` entries to the fixture's workspace
|
|
133
|
+
file when it falls back to young versions; that is expected in a fixture and
|
|
134
|
+
shows which young versions consumers resolve.
|
|
116
135
|
|
|
117
136
|
## Exact Node.js floor
|
|
118
137
|
|
|
119
|
-
|
|
120
|
-
developer runtime. For disjoint supported
|
|
121
|
-
minimum. For an approved engine
|
|
122
|
-
with the updated package at its new floor and on an overlapping supported
|
|
123
|
-
runtime where
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
Validate at the exact minimum supported release, not the same major or the
|
|
139
|
+
active developer runtime. For disjoint supported lines check each relevant
|
|
140
|
+
minimum. For an approved engine change compare the old release at its old floor
|
|
141
|
+
with the updated package at its new floor, and on an overlapping supported
|
|
142
|
+
runtime where one exists.
|
|
143
|
+
|
|
144
|
+
Obtain the exact runtime without changing the user's default. In the isolated
|
|
145
|
+
fixture declare the floor version:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"devEngines": {
|
|
150
|
+
"runtime": { "name": "node", "version": "24.16.0", "onFail": "download" }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Run `pnpm install` there and confirm `pnpm exec node --version`; pnpm downloads
|
|
156
|
+
the release, verifies it, and records the checksum in the fixture's lockfile. If
|
|
157
|
+
that is unavailable, download the official archive for the host from
|
|
158
|
+
`https://nodejs.org/dist/v<version>/` together with `SHASUMS256.txt`, verify the
|
|
159
|
+
SHA-256, extract it in the run directory, and use the binary by absolute path.
|
|
160
|
+
Record the binary's path and `--version` output with the results either way.
|
|
161
|
+
|
|
162
|
+
Install and build with the development runtime when the tooling requires it,
|
|
163
|
+
then invoke the packed package's built entrypoint and runtime fixtures with the
|
|
164
|
+
floor binary. For subprocesses that use `env node`, put the floor binary's
|
|
165
|
+
directory first in the test process's `PATH` only, and confirm which binary the
|
|
166
|
+
child used. If the test runner itself needs newer Node.js, run a small fixture
|
|
167
|
+
harness or the built CLI directly at the floor; running only the newer runner is
|
|
168
|
+
not floor validation. Report when installation or execution at the exact runtime
|
|
169
|
+
could not be verified.
|
|
170
|
+
|
|
171
|
+
`scripts/run-at-node.mjs <version> --cwd <fixture> -- <command>` does all of
|
|
172
|
+
this: it downloads and verifies the release through `devEngines.runtime` in the
|
|
173
|
+
run directory, puts a logging `node` shim first in `PATH`, runs the command, and
|
|
174
|
+
reports how many `node` processes started and with which versions. A count that
|
|
175
|
+
is implausibly low for the command means children bypassed `PATH`; treat that as
|
|
176
|
+
unverified rather than as a pass.
|
|
140
177
|
|
|
141
178
|
Record stdout, stderr, exit codes, and relevant API results for identical
|
|
142
179
|
fixtures across runtimes and dependency baselines. Normalize only known
|
|
143
|
-
nondeterministic fields
|
|
144
|
-
Save SHA-256 digests of normalized
|
|
145
|
-
|
|
146
|
-
|
|
180
|
+
nondeterministic fields with the same documented transformation for every run.
|
|
181
|
+
Save SHA-256 digests of normalized output for quick comparison, keep the actual
|
|
182
|
+
output for reviewing differences, and add targeted assertions for behavior that
|
|
183
|
+
serialization omits.
|
|
184
|
+
|
|
185
|
+
`scripts/compare-outputs.mjs <dir-a> <dir-b>` compares two result directories
|
|
186
|
+
with the standard normalization: state names only as path segments (`--token-a`,
|
|
187
|
+
`--token-b`), pnpm store directory names, git blob ids, and content-hashed
|
|
188
|
+
filenames. Never replace a state name as a bare word: `baseline` also occurs in
|
|
189
|
+
CSS properties and rule names. Use `--sort-lines` only for output whose parallel
|
|
190
|
+
tools interleave differently on each run, such as `ksc lint`.
|
|
191
|
+
|
|
192
|
+
## Updated validation
|
|
193
|
+
|
|
194
|
+
After resolution and deduplication, run:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pnpm build
|
|
198
|
+
pnpm fix
|
|
199
|
+
pnpm build # if fix changed source files or build configuration
|
|
200
|
+
pnpm test
|
|
201
|
+
pnpm lint
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Skip a script the repository does not define and say so. Compare every check
|
|
205
|
+
with the recorded baseline, and use these procedures where applicable; later
|
|
206
|
+
cleanup steps reuse them for affected checks:
|
|
207
|
+
|
|
208
|
+
- **Published consumers:** diff the production-engine inventories; inspect
|
|
209
|
+
packed manifests and artifacts; test isolated installations and representative
|
|
210
|
+
fixtures at the exact supported floors above, separately from
|
|
211
|
+
development-runtime checks. Review fixer edits to `engines.node` and
|
|
212
|
+
`devEngines.runtime`. Passing on newer Node.js does not establish consumer
|
|
213
|
+
compatibility. Validate new contracts and record changed baselines for
|
|
214
|
+
authorized breaks.
|
|
215
|
+
- **Advisories:** diff the audit reports as described above.
|
|
216
|
+
- **Warnings:** trace and report new warnings, deprecations, and peer issues
|
|
217
|
+
with the configuration policy's procedure, applying only its permitted
|
|
218
|
+
suppressions or explicitly authorized exceptions.
|
|
219
|
+
- **Build output:** when a baseline exists, rebuild with its command and
|
|
220
|
+
environment and diff the trees with content-hashed filenames normalized
|
|
221
|
+
(`name.[hash].js`). Explain every remaining difference through an upstream
|
|
222
|
+
changelog, issue, or reviewed migration or configuration change, and
|
|
223
|
+
investigate the rest. Record effective targets before and after: unless
|
|
224
|
+
configured explicitly, tsdown derives its target from `engines.node`. Changed
|
|
225
|
+
syntax can be expected, but syntax lowering does not polyfill runtime APIs.
|
|
226
|
+
- **Fixer output:** review the diff and compare a required rebuild with the
|
|
227
|
+
pre-fix build. Formatting changes must produce identical output; handle
|
|
228
|
+
behavioral or browser-compatibility changes under the configuration policy's
|
|
229
|
+
lint-fix rules and recheck the behavior after correction.
|
|
230
|
+
|
|
231
|
+
On failure, make the smallest change the authorization table allows and rerun
|
|
232
|
+
from the failing step, using the baseline logs to separate new failures, subject
|
|
233
|
+
to the baseline qualifications above. `git stash` does not restore
|
|
234
|
+
`node_modules`. To isolate an update, reset half of the accepted specifiers, run
|
|
235
|
+
`pnpm update -r --no-save`, and repeat the failing check; defer the culprit
|
|
236
|
+
unless an authorized migration resolves it. For a runtime release that published
|
|
237
|
+
ranges already allow, follow the consumer-range decision; resetting a specifier
|
|
238
|
+
or the lockfile may not exclude it.
|
|
239
|
+
|
|
240
|
+
## Tool behavior notes
|
|
241
|
+
|
|
242
|
+
Known behaviors that look like failures, or hide them:
|
|
243
|
+
|
|
244
|
+
- `ksc fix` prints nothing when everything was fixed; only remaining problems
|
|
245
|
+
produce output. A silent exit code of 0 is success.
|
|
246
|
+
- `ksc lint` and `ksc fix` use tool caches. Before trusting a pass on files a
|
|
247
|
+
fix or migration touched, rerun ESLint with `--no-cache` and `ksc-prettier
|
|
248
|
+
lint --no-cache`.
|
|
249
|
+
- Output from `ksc lint` interleaves its parallel tools differently on each run;
|
|
250
|
+
compare it with sorted lines.
|
|
251
|
+
- mdat expands rules from the `package.json` nearest to where it runs. Never run
|
|
252
|
+
`mdat expand` on a package readme from a monorepo root; it fills the title,
|
|
253
|
+
description, and other rules from the private root manifest. Regenerate
|
|
254
|
+
through the repository's own script.
|
|
255
|
+
- Use `pnpm -r ls --depth -1 --json` to enumerate workspace packages instead of
|
|
256
|
+
globbing `packages/*`, which also matches stray entries such as `.DS_Store`.
|
|
257
|
+
- Recent pnpm lockfiles contain several YAML documents, the first describing the
|
|
258
|
+
package manager's own dependencies; a lockfile parser must read every
|
|
259
|
+
`packages:` section.
|
|
147
260
|
|
|
148
261
|
## Tool references
|
|
149
262
|
|
|
150
263
|
Check these primary sources or the installed tool's help when behavior differs:
|
|
151
264
|
|
|
152
|
-
- [GitHub Actions
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
265
|
+
- [GitHub Actions
|
|
266
|
+
variables](https://docs.github.com/en/actions/reference/workflows-and-actions/variables):
|
|
267
|
+
`GITHUB_ACTIONS` and `CI` identify unattended execution.
|
|
268
|
+
- [pnpm update](https://pnpm.io/cli/update): `--no-save` and what an update
|
|
269
|
+
writes. The full refresh here uses no package selectors; do not assume
|
|
270
|
+
exclusion syntax is identical in every pnpm version.
|
|
271
|
+
- [pnpm dedupe](https://pnpm.io/cli/dedupe),
|
|
272
|
+
[pnpm list](https://pnpm.io/cli/list), and [pnpm audit](https://pnpm.io/cli/audit).
|
|
273
|
+
- [pnpm settings](https://pnpm.io/settings), including
|
|
274
|
+
[minimumReleaseAge](https://pnpm.io/settings/dependency-resolution#minimumreleaseage),
|
|
275
|
+
[trustPolicy](https://pnpm.io/settings/dependency-resolution#trustpolicy),
|
|
276
|
+
[allowBuilds](https://pnpm.io/settings/build#allowbuilds), and
|
|
277
|
+
[peer rules](https://pnpm.io/settings/peer-dependencies#peerdependencyrulesallowedversions).
|
|
278
|
+
Current pnpm reads only registry and auth settings from `.npmrc`.
|
|
279
|
+
- [devEngines.runtime](https://pnpm.io/package_json#devenginesruntime): exact
|
|
280
|
+
runtime downloads for fixtures.
|
|
281
|
+
- [Taze](https://github.com/antfu-collective/taze): candidate filters, maturity,
|
|
282
|
+
and the pnpm settings it infers.
|
|
283
|
+
- [Actions Up](https://github.com/azat-io/actions-up): SHA pinning, cool-down,
|
|
284
|
+
and `--min-age-exclude`.
|
|
285
|
+
- [Node.js release schedule](https://github.com/nodejs/Release): LTS lines and
|
|
286
|
+
the annual all-LTS cycle from Node 27.
|
|
287
|
+
- [tsdown target](https://tsdown.dev/options/target): engine-derived targets
|
|
288
|
+
affect emitted syntax and do not polyfill APIs.
|
|
289
|
+
- [Node.js release archives](https://nodejs.org/dist/): fallback runtime
|
|
290
|
+
downloads and checksum manifests.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compares two directories of captured outputs, such as baseline and updated
|
|
3
|
+
* fixture results or build trees, after normalizing only known nondeterministic
|
|
4
|
+
* text, and reports each file as identical, different, or present on one side.
|
|
5
|
+
*
|
|
6
|
+
* Normalization:
|
|
7
|
+
*
|
|
8
|
+
* - `--token-a` and `--token-b` replace state names such as `baseline` and
|
|
9
|
+
* `updated` with `<STATE>`, but only where they appear as a path segment
|
|
10
|
+
* (bounded by `/`, `\`, or `+`). Bare-word replacement would also rewrite
|
|
11
|
+
* content such as the CSS property `alignment-baseline`.
|
|
12
|
+
* - Store directory names that pnpm ends with `_` plus 32 hex characters become
|
|
13
|
+
* `<STORE-DIR>`. pnpm truncates long names before the hash, so the whole
|
|
14
|
+
* segment varies with the state name's length, not just the hash.
|
|
15
|
+
* - Git blob ids in captured diffs (`index abc123..def456`) become `index
|
|
16
|
+
* <BLOBS>`.
|
|
17
|
+
* - Content-hashed filenames (`name.<8+ characters>.js`) become `name.[hash].js`.
|
|
18
|
+
* - `--sort-lines` sorts lines first, for output that parallel tools interleave
|
|
19
|
+
* differently on each run. Never use it where order is meaningful.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
*
|
|
23
|
+
* ```
|
|
24
|
+
* node compare-outputs.mjs <dir-a> <dir-b> [--token-a <name>] [--token-b <name>] [--sort-lines]
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Prints a short `git diff --no-index` of each differing file and exits 1 when
|
|
28
|
+
* anything differs.
|
|
29
|
+
*/
|
|
30
|
+
import { spawnSync } from 'node:child_process'
|
|
31
|
+
import { mkdtempSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
32
|
+
import { join, relative } from 'node:path'
|
|
33
|
+
import process from 'node:process'
|
|
34
|
+
import { runDirectory } from './lib.mjs'
|
|
35
|
+
|
|
36
|
+
/** @type {string[]} */
|
|
37
|
+
const positional = []
|
|
38
|
+
/** @type {Record<string, string>} */
|
|
39
|
+
const options = {}
|
|
40
|
+
let sortLines = false
|
|
41
|
+
const argv = process.argv.slice(2)
|
|
42
|
+
for (let index = 0; index < argv.length; index++) {
|
|
43
|
+
const argument = argv[index] ?? ''
|
|
44
|
+
if (argument === '--sort-lines') {
|
|
45
|
+
sortLines = true
|
|
46
|
+
} else if (argument.startsWith('--')) {
|
|
47
|
+
options[argument.slice(2)] = argv[++index] ?? ''
|
|
48
|
+
} else {
|
|
49
|
+
positional.push(argument)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const [directoryA, directoryB] = positional
|
|
54
|
+
if (directoryA === undefined || directoryB === undefined) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'Usage: node compare-outputs.mjs <dir-a> <dir-b> [--token-a x] [--token-b y] [--sort-lines]',
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Lists files under a directory recursively, as paths relative to it.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} root - Directory to walk.
|
|
64
|
+
*
|
|
65
|
+
* @returns {string[]} Relative file paths.
|
|
66
|
+
*/
|
|
67
|
+
function listFiles(root) {
|
|
68
|
+
return readdirSync(root, { recursive: true, withFileTypes: true })
|
|
69
|
+
.filter((entry) => entry.isFile())
|
|
70
|
+
.map((entry) => relative(root, join(entry.parentPath, entry.name)))
|
|
71
|
+
.toSorted()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Normalizes one file's text for comparison.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} text - File contents.
|
|
78
|
+
* @param {string | undefined} token - State name to replace on this side.
|
|
79
|
+
*
|
|
80
|
+
* @returns {string} Normalized text.
|
|
81
|
+
*/
|
|
82
|
+
function normalize(text, token) {
|
|
83
|
+
let result = text
|
|
84
|
+
if (token !== undefined && token !== '') {
|
|
85
|
+
const escaped = token.replaceAll(/[$\(\)*+.?\[\\\]^\{\|\}]/gv, String.raw`\$&`)
|
|
86
|
+
result = result.replaceAll(
|
|
87
|
+
new RegExp(String.raw`(?<=[\/\\+])${escaped}(?=[\/\\+])`, 'gv'),
|
|
88
|
+
'<STATE>',
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
result = result
|
|
93
|
+
.replaceAll(/[^\s"'\/\\]*_[\da-f]{32}/gv, '<STORE-DIR>')
|
|
94
|
+
.replaceAll(/^index [\da-f]+\.\.[\da-f]+/gmv, 'index <BLOBS>')
|
|
95
|
+
.replaceAll(/\.[\w\-]{8,}\.(c?js|mjs|css)\b/gv, '.[hash].$1')
|
|
96
|
+
return sortLines ? result.split('\n').toSorted().join('\n') : result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const filesA = new Set(listFiles(directoryA))
|
|
100
|
+
const filesB = new Set(listFiles(directoryB))
|
|
101
|
+
const scratch = mkdtempSync(join(runDirectory(), 'compare-'))
|
|
102
|
+
let differences = 0
|
|
103
|
+
|
|
104
|
+
for (const file of [...filesA.union(filesB)].toSorted()) {
|
|
105
|
+
if (!filesA.has(file) || !filesB.has(file)) {
|
|
106
|
+
differences++
|
|
107
|
+
process.stdout.write(`only in ${filesA.has(file) ? 'A' : 'B'}: ${file}\n`)
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const pathA = join(directoryA, file)
|
|
112
|
+
const pathB = join(directoryB, file)
|
|
113
|
+
if (statSync(pathA).size === 0 && statSync(pathB).size === 0) {
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const textA = normalize(readFileSync(pathA, 'utf8'), options['token-a'])
|
|
118
|
+
const textB = normalize(readFileSync(pathB, 'utf8'), options['token-b'])
|
|
119
|
+
if (textA === textB) {
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
differences++
|
|
124
|
+
const safeName = file.replaceAll(/[\/\\]/gv, '__')
|
|
125
|
+
writeFileSync(join(scratch, `a-${safeName}`), textA)
|
|
126
|
+
writeFileSync(join(scratch, `b-${safeName}`), textB)
|
|
127
|
+
const diff = spawnSync(
|
|
128
|
+
'git',
|
|
129
|
+
['diff', '--no-index', '--no-color', '-U1', `a-${safeName}`, `b-${safeName}`],
|
|
130
|
+
{ cwd: scratch, encoding: 'utf8' },
|
|
131
|
+
)
|
|
132
|
+
const lines = diff.stdout.split('\n')
|
|
133
|
+
process.stdout.write(`differs: ${file}\n${lines.slice(4, 44).join('\n')}\n`)
|
|
134
|
+
if (lines.length > 44) {
|
|
135
|
+
process.stdout.write(` ... ${lines.length - 44} more diff lines\n`)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
process.stdout.write(
|
|
140
|
+
`${differences === 0 ? 'no differences' : `${differences} file(s) differ`} (normalized copies in ${scratch})\n`,
|
|
141
|
+
)
|
|
142
|
+
if (differences > 0) {
|
|
143
|
+
process.exitCode = 1
|
|
144
|
+
}
|