Repository navigation
fix(cli): plugin scaffolds declare vitest ~4.0.0 so npm install works on npm 10 - #22920
Conversation
SCAFFOLD_VITEST_RANGE moves from ^4.0.0 to ~4.0.0. With ^4.0.0, npm 10 (Node 22's bundled major) crashes in arborist #loadPeerSet with "Cannot read properties of null (reading 'edgesOut')": vitest 4.1.x admits vite 8, whose optional @vitejs/devtools peer leads to a vitest@* peer that resolves to vitest's latest and contradicts the root vitest. vitest 4.0.x admits only vite 6 or 7, which declare no such peer. The comment beside the constant records the measured cause, why ^5.0.0 is not the fix, and the re-check that lets it move back. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 34dd3b9e177fddede9bb5c2ac5efebe0113d83e8 && git checkout 34dd3b9e177fddede9bb5c2ac5efebe0113d83e8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 018b077fbf8876c0fd5f46e66dcc551a4e74c895 cd7e34ee272da80a2d43504408a166b6c3996a11 && git checkout -B drift-repro 018b077fbf8876c0fd5f46e66dcc551a4e74c895 && git merge --no-ff cd7e34ee272da80a2d43504408a166b6c3996a11
node scripts/docs-audit/affected-docs.mjs --json 018b077fbf8876c0fd5f46e66dcc551a4e74c895
|
Fixes #22912
Clause-②: no
What changes
SCAFFOLD_VITEST_RANGE(packages/cli/src/commands/init.ts) moves from^4.0.0to~4.0.0. It is the one range both CLI scaffolders emit (objectstack init -t pluginandobjectstack create plugin), and neither holds a copy. A comment beside the constant records the upstream cause, why^5.0.0was not chosen, and the re-check that lets the range move back. Onepatchchangeset for@objectstack/cli.There is no
.npmrc, nolegacy-peer-deps, no per-scaffolder range and nopackages/create-objectstackchange.SCAFFOLD_VITEST_RANGEis not one of thePOLICY_STAMPSthatscripts/sync-scaffold-emission-policy.mjswrites into that package's template, and its blank template declares no vitest.Install matrix (measured 2026-10-12)
Every row ran on Node v22.22.0, in an empty directory outside the repo, with a fresh
--cachedirectory per row, asnpm install --no-audit --no-fund. npm 10.9.4 is this container's global npm, the major that Node 22 bundles. npm 10.9.9 (newest 10.x) and npm 11.21.0 (newest 11.x) were installed into scratch prefixes and run by path. The global npm was not changed.The card's one-line manifest,
{"devDependencies":{"vitest":RANGE}}^4.0.0npm error Cannot read properties of null (reading 'edgesOut')^4.0.0^4.0.0~4.1.0~4.1.0~4.0.0~4.0.0~4.0.0^5.0.0^5.0.05.0.0(exact)5.0.2(exact)The CLI-emitted manifests (the card's "not measured" item)
These were emitted by driving the real commands from this worktree (
tsx packages/cli/bin/run-dev.js init probe-plugin -t plugin --no-install, andcreate plugin probe-create). Each emitted project directory was then installed as-is. Their@objectstack/*ranges are^17.7.0, which is the publishedlatest.os init -t plugin^4.0.0(before)edgesOut, as aboveos create plugin^4.0.0(before)edgesOut, as aboveos init -t plugin~4.0.0os create plugin~4.0.0os init -t plugin~4.0.0os create plugin~4.0.0os init -t plugin~4.0.0os create plugin~4.0.0In the npm 10.9.4
os init -t plugininstall, the only vite is 7.3.7, the only vitest is 4.0.18, and no@vitejs/devtools*package is present. In the npm 10.9.4os create plugininstall,vitest runof a one-test file passes (1 passed, exit 0).Not measured:
latest. It refuses Node 22.22.0 (its engines are^22.22.2 || ^24.15.0 || >=26.0.0), and no other Node is on the box.~4.1.0on npm 11.Why
~4.0.0: the mechanism, read off the registry and npm's own log@vitejs/devtools@^0.7.1,devtools-oxc,devtools-rolldown,devtools-vite,@vitejs/devtools-vitest@^0.7.6,vitest@*,@vitest/browser-playwright@5.0.3andvitest@4.1.11. Then it throwsTypeError: Cannot read properties of null (reading 'edgesOut')at#loadPeerSet(build-ideal-tree.js:1289).vite@8.3.4declares the optional peer@vitejs/devtools: ^0.7.1, and 33 vite 8.x releases declare some devtools peer. The union of peer keys over all 80 stable vite 6.x and 7.x releases has no devtools key.@vitejs/devtools-vitest0.7.x peersvitest: *. vitest'slatestis 5.0.3, and@vitest/browser-playwright@5.0.3peersvitest: 5.0.3exactly.^6.0.0 || ^7.0.0. vitest 4.1.0 adds^8.0.0-0, and 4.1.1 to 4.1.11 add^8.0.0. So every 4.1.x resolves vite 8.3.4, and 4.0.x resolves vite 7.3.7.So npm 10 crashes when two things are both true: vite 8 is in the tree, and the root vitest is not the version that
latestnames. The exact5.0.0and5.0.2rows show the second half. They crash the same way although they are vitest 5.Why not
^5.0.0, though it is newer and installs today? It installs only because it resolves to exactly the version thatlatestnames. It would crash again on npm 10, with no change in this repo, the daylatestmoves past 5.x. It is also a major bump of the scaffold's test runner. By contrast, every 4.0.x is already inside today's^4.0.0, so~4.0.0admits no version the scaffold did not already admit.~4.0.0keeps vite on 7.x, where thevitest@*peer never enters the graph, so its install does not depend on thelatesttag.The cost: a scaffold installs vitest 4.0.18 (published 2026-01-22) instead of 4.1.11. Engines do not move. vitest 4.0.18 and 4.1.11 both declare node
^20.0.0 || ^22.0.0 || >=24.0.0, and vite 7.3.7 and 8.3.4 both declare^20.19.0 || >=22.12.0.The one-range pin already exists, and it can fail
packages/cli/test/scaffold-emission-policy.e2e.test.tsalready makes two assertions across all four CLI emissions: each third-party dependency has exactly one range, and the vitest range ISSCAFFOLD_VITEST_RANGE. Its expectation is the imported constant, not a literal, so the file is unchanged here and no new test was added. It is a nightly-tier file, selected by its.e2ename.Ablation (one-shot,
scripts/ablation-replace.mjsin wrap mode, atcd7e34ee27): increate.ts,vitest: SCAFFOLD_VITEST_RANGE,was replaced with the copied literalvitest: '^4.0.0',.d2c94f6a66f5to98d8393f80b4.OS_TEST_TIERS=nightlythe file went red, with 3 failed and 7 passed: "declares exactly one range per third-party dependency", "emits the exported constant rather than a literal, for every policy range" and "declares exactly one range per third-party dependency, across ALL THREE scaffolders".git diff HEADis empty.Verification (all at
cd7e34ee27, the PR head)pnpm --workspace-concurrency=2 --filter '@objectstack/cli^...' build: VERDICT command-exit 0.pnpm --filter @objectstack/cli typecheck: VERDICT command-exit 0 (tsc --noEmit, andcheck:test-typecheckOK).pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 282 files and 4262 tests passed, VERDICT command-exit 0. Theintegrationlayer is left to CI, because the diff touches no integration-layer file and no spawn entry.OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/scaffold-emission-policy.e2e.test.ts: 10 passed, VERDICT command-exit 0.node scripts/pm/dispatch-gates.mjs --commands, with no paths, derived 59 commands from the merge base. All 59 ran and each exited 0. Reconciled with--ran: "59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero, all 59 exit-coded).check:dual-build-cjs-loadsandcheck:i18n-coveragefirst exited 3 (PREREQUISITE NOT MET), because ten packages outside the cli closure had nodist/. After those ten were built under the lock, both exited 0.check:scaffold-emission-policy: "2 carrier(s) emit the shared policy".pnpm exec eslint --no-inline-config --format json packages/cli/src/commands/init.tsexited 0, with 1 file, 0 errors and 0 warnings.--print-configresolves 5 rules for the file, so it is not ignored.parserOptions.projectand noprojectService, as its own comment states and--print-configconfirms). So this diff cannot move any untouched file's verdict..changesetfile is outside eslint's population. Repo-widepnpm lintis left to CI.Acceptance notes
plugin-spec.mdxstops promising the non-existent@objectstack/testingand points at@objectstack/verify; thecreate-objectstackblank template ships a test story (epic hotcrm#1579, step 5b) #15952 remains open and is not touched here. Its held scaffold half adds vitest tocreate-objectstack's blank template, which cannot import this constant.SCAFFOLD_VITEST_RANGEis not aPOLICY_STAMPSrow inscripts/sync-scaffold-emission-policy.mjs, so a vitest line in that template would need a new stamp row to stay tied to this value.origin/main(eab7236ff7). That commit touchespackages/cli/src/commands/verify.tsand other files outside this diff, so it was not merged in.Generated by Claude Code