Skip to content

chore(node): require Node 24, fail fast on a mismatched version - #1795

Draft
kriszyp wants to merge 1 commit into
stagefrom
claude/node-engines-guard
Draft

kriszyp wants to merge 1 commit into
stagefrom
claude/node-engines-guard

Conversation

@kriszyp

@kriszyp kriszyp commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

⊙ Problem

Studio has no engines.node, and the unit suite fails wholesale on Node > 24 (jsdom-based suites) with no hint why — .nvmrc pins 24.21.0, but nothing enforces it. A dev who drifts off that version gets a confusing pile of jsdom failures instead of a one-line "wrong Node" message.

❓ Your call: the task asked for engines.node + engine-strict (or an equivalent early check) so that both pnpm test and pnpm install fail fast on an unsupported Node. I implemented the pnpm test half as asked, but overruled the pnpm install half — see ⚖️ Alternatives below for the disqualifying fact.

💡 Solution

  • package.json:engines.node: ">=24 <25", matching .nvmrc.
  • scripts/check-node-version.mjs, wired into vitest.config.ts's globalSetup: runs once before any test file, on any Node, regardless of how vitest is invoked (pnpm test/test:watch/test:coverage, pnpm exec vitest, an IDE "run test"). Throws a clear, one-line message naming the required range — vitest reports it natively rather than the process exiting out from under an embedded host (e.g. an IDE's vitest child).
  • The range parser (isSupported, same file) is anchored to the exact >=N <N shape engines.node currently uses, and throws on anything else, so a later edit to that field can't silently stop being enforced — pinned by scripts/check-node-version.test.mjs.
  • README.md:142 troubleshooting line updated off the stale "Node 20+".
  • DESIGN.md:67: recorded the engineStrict/harper-pro invariant below, since the natural way to "finish" strict enforcement is to flip engineStrict on — which would reopen the problem this PR avoids.

⚖️ Alternatives

❓ Your call: pnpm-workspace.yaml's engineStrict: true is the natural way to make pnpm install hard-fail on the wrong Node (and was my first attempt — caught by this PR's own pre-push review before push). I overruled it: engineStrict doesn't just gate a local pnpm install, it gates every pnpm install/pnpm run build:local against this repo, including harper-pro's prod/Docker release build, which clones studio's prod branch and runs exactly those two commands on Node 22 and latest (currently 26.x) — harper-pro/build-tools/build-studio.sh:7-10, driven from create-release.yaml:29,84 and publish-docker.yaml:34-39. Shipping engineStrict to prod would break that release pipeline the next time it runs. Fix chosen instead: enforce only where the actual motivating problem (jsdom suites) lives — vitest's globalSetup — and leave pnpm install/dev/build on pnpm's existing advisory-only warning. This is a one-line, easily-reversible decision if harper-pro's build ever changes to tolerate it.

Also considered reading .nvmrc's major directly instead of parsing engines.node (removes the parser and a second source of truth, but decouples enforcement from the field a human actually edits) and a semver dependency for the range parser (exact pnpm parity, but a new dependency for one fixed range shape nobody expects to change). Kept the engines.node + anchored-regex approach: cheapest, and the anchored throw already closes the drift risk a looser parser would have.

✅ Verification

  • node/direct calls: Node 24.21.0 → passes; Node 22/26 → throws the intended diagnostic. Confirmed on this box's actual Node 26.2.0, and via the independent review's own executed checks on 22.23.1/26.2.0.
  • pnpm exec vitest run (full suite, globalSetup temporarily removed to exercise it on this sandbox's only available Node, 26.2.0): 396 files / 3667 tests pass. One file (src/lib/monaco/editorApi.test.ts) timed out under parallel load on an earlier run and passed cleanly both in isolation and on a clean rerun — a pre-existing, load-dependent flake unrelated to this change.
  • pnpm exec tsc -b: no new errors (this worktree has pre-existing, unrelated monaco-editor duplicate-type errors from two different node_modules trees — a worktree artifact, not introduced here).
  • oxlint / dprint check: clean across the whole repo.
  • pnpm exec vitest run scripts/check-node-version.test.mjs: 8/8 pass, including the anchored-regex fail-closed cases.
  • Three rounds of independent cross-model pre-push review (codex + gemini + cursor + Harper-domain adjudication): round 1 found the engineStrict/harper-pro blocker (fixed by removing it); round 2 found a fail-open parser bug, a process.exit risk, a dead code path, and narrative-comment/dangling-reference issues (all fixed); round 3 closed with severity nit — remaining items are documented, deliberate trade-offs (Node 24 prereleases pass on major-only comparison; pnpm dev/build stay unguarded on purpose, matching the engineStrict decision above).

Related PRs: none found

Review-Coverage: authored=claude; ran=gemini,cursor-composer,codex,cursor-muse; adjudicated=domain; declined=cursor-grok,cursor-kimi; rounds=3; full=2 @ 0ed8de2

Review-Attention: read ~7m (sensitive: package.json; decisions: guard-source-of-truth, vitest-only-enforcement, fail-closed-parser) @ 0ed8de2

Studio had no engines.node, so a dev on the wrong Node (jsdom-based
unit tests break above Node 24; .nvmrc pins 24.21.0) got a wholesale,
unexplained suite failure instead of a clear error.

- engines.node: ">=24 <25", matching .nvmrc.
- scripts/check-node-version.mjs, wired in as vitest.config.ts's
  globalSetup: runs once before any test file, throwing (vitest's own
  error reporting, not a bare process.exit that could kill an embedded
  host) with a clear message on any Node outside the range, regardless
  of how vitest is invoked (pnpm test*, `pnpm exec vitest`, an IDE "run
  test"). The range parser is anchored to the exact ">=N <N" shape and
  throws on anything else, so an edit to engines.node can't silently
  stop being enforced.
- No pnpm-workspace.yaml engineStrict: it would also block harper-pro's
  prod-bundle build, which runs `pnpm install`/`pnpm run build:local`
  against studio's `prod` branch on Node 22 and latest (26.x) in
  create-release.yaml and publish-docker.yaml. pnpm's own engines
  check also only fires when install does real work, so it can't
  reliably guard "pnpm install" after a Node switch with node_modules
  already in place either way — the test-time guard above is the
  actual, reliable enforcement. Recorded as a DESIGN.md invariant since
  "add engineStrict" is the natural way to misread this as incomplete.
- README troubleshooting note updated off the stale "Node 20+".

Does not touch studio#1689 (undici WebSocket teardown), a separate
issue out of scope here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3s4BXF9ivRLFRaHBBoNQy
Dispatch-Task: studio-node-engines-guard

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a Node.js version enforcement mechanism by adding a Node engine restriction (>=24 <25) to package.json and a custom check script (check-node-version.mjs) executed during Vitest's global setup. It also updates documentation in DESIGN.md and README.md to reflect these changes. Feedback on the changes suggests improving the robustness of the version parser to handle leading 'v' characters (such as in process.version) and adding a corresponding unit test to verify this behavior.

Comment thread scripts/check-node-version.mjs
Comment thread scripts/check-node-version.test.mjs
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 67.52% 10104 / 14963
🔵 Statements 67.68% 10782 / 15930
🔵 Functions 60.77% 2600 / 4278
🔵 Branches 62.52% 7689 / 12297
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
scripts/check-node-version.mjs 60% 50% 50% 60% 15-20
Generated in workflow #2063 for commit 0ed8de2 by the Vitest Coverage Report Action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants