diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 518402303711f..1a0f100075b95 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -8,7 +8,7 @@ "type": "module", "main": "dist/electron-main.mjs", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=22.22.0" }, "scripts": { "clean": "npm run clean:e2e && npm run clean:renderer && npm run clean:electron", diff --git a/package-lock.json b/package-lock.json index 9cbd2126a79e7..d7cd4adf58606 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "typescript-eslint": "8.64.0" }, "engines": { - "node": ">=20.0.0", + "node": ">=22.22.0", "npm": "<11.10.0 || >=11.17.0" } }, @@ -168,7 +168,7 @@ "wait-on": "9.0.10" }, "engines": { - "node": ">=26.0.0" + "node": ">=22.22.0" } }, "apps/desktop/node_modules/ignore": { diff --git a/package.json b/package.json index 8da1e0f176958..81a56b1b83905 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "brace-expansion": "5.0.8" }, "engines": { - "node": ">=20.0.0", + "node": ">=22.22.0", "npm": "<11.10.0 || >=11.17.0" }, "allowScripts": { diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 09310dffae7ca..fdfdd7aa07cf1 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1190,11 +1190,11 @@ function Set-GitBashEnvVar { Write-Info "If needed, set HERMES_GIT_BASH_PATH manually to your bash.exe path." } -# The desktop build runs Vite ^8, which refuses to start on Node outside -# `^20.19 || >=22.12`. That toolchain floor is the real constraint; do NOT -# raise it past what a dependency actually demands, or every user on a working -# Node gets their toolchain replaced for nothing. Returns $true when a -# `node --version` string clears that floor. +# The dependency tree's real Node floor is >=22.22.0, set by react-router 8.3.0 +# (`engines.node`). Keep this in sync with the root package.json: looser lets an +# install reach a `npm ci` that dies with EBADENGINE, stricter replaces a working +# user toolchain for nothing. Returns $true when a `node --version` string +# clears that floor. function Test-NodeVersionOk { param([string]$Version) try { @@ -1202,8 +1202,7 @@ function Test-NodeVersionOk { } catch { return $false } - if ($v.Major -eq 20) { return ($v.Minor -ge 19) } - if ($v.Major -eq 22) { return ($v.Minor -ge 12) } + if ($v.Major -eq 22) { return ($v.Minor -ge 22) } return ($v.Major -gt 22) } diff --git a/scripts/install.sh b/scripts/install.sh index 6f3d777ba4f30..23bb54ca6d973 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -780,22 +780,20 @@ check_git() { exit 1 } -# The desktop build runs Vite ^8, which refuses to start on Node outside -# `^20.19 || >=22.12` — older Node lacks `node:util.styleText`, so `vite build` -# crashes with a SyntaxError that surfaces only as the opaque "Build desktop -# app … exit code 1" install failure. That toolchain floor is the real -# constraint; do NOT raise it past what a dependency actually demands, or every -# user on a working Node gets their toolchain replaced for nothing. Returns 0 -# when the given `node --version` string clears the floor; anything below it is -# replaced with the Hermes-managed Node $NODE_VERSION. +# The dependency tree's real Node floor is >=22.22.0, set by react-router 8.3.0 +# (`engines.node`), with Vite ^8 next at `^20.19 || >=22.12`. Keep this in sync +# with the root package.json — a gate looser than the manifest lets an install +# proceed to a `npm ci` that then dies with EBADENGINE, and a gate stricter than +# the manifest replaces a working user toolchain for nothing. Returns 0 when the +# given `node --version` string clears the floor; anything below it is replaced +# with the Hermes-managed Node $NODE_VERSION. node_satisfies_build() { local ver="${1#v}" local major="${ver%%.*}" local minor="${ver#*.}"; minor="${minor%%.*}" case "$major" in ''|*[!0-9]*) return 1 ;; esac case "$minor" in ''|*[!0-9]*) minor=0 ;; esac - if [ "$major" -eq 20 ] && [ "$minor" -ge 19 ]; then return 0; fi - if [ "$major" -ge 22 ] && { [ "$major" -gt 22 ] || [ "$minor" -ge 12 ]; }; then return 0; fi + if [ "$major" -ge 22 ] && { [ "$major" -gt 22 ] || [ "$minor" -ge 22 ]; }; then return 0; fi return 1 } diff --git a/tests/test_engines_satisfiable.py b/tests/test_engines_satisfiable.py index 117764f894ab6..dcffbd8c96e5c 100644 --- a/tests/test_engines_satisfiable.py +++ b/tests/test_engines_satisfiable.py @@ -19,6 +19,7 @@ declare and the toolchain that has to satisfy it. from __future__ import annotations import json +import re from pathlib import Path import pytest @@ -116,9 +117,17 @@ class TestEnginesAreSatisfiable: else: # pragma: no cover - install.sh always defines it pytest.fail("install.sh does not define NODE_VERSION") - assert _satisfies_range(f"{managed_major}.0.0", node_range), ( + # install.sh fetches latest-v{major}.x, not {major}.0.0, so compare on + # the major: the newest release of that line must be able to clear the + # floor. A floor in a HIGHER major than we provision can never be met. + floor_majors = [ + int(m.group(1)) + for m in re.finditer(r">=\s*v?(\d+)", node_range) + ] + assert floor_majors, f"cannot read a floor out of {node_range!r}" + assert managed_major >= min(floor_majors), ( f"engines.node is {node_range!r} but install.sh provisions Node " - f"{managed_major}. The runtime we ship must satisfy the floor we " + f"{managed_major}.x. The runtime we ship must satisfy the floor we " "declare, or the install we just performed cannot install deps." ) @@ -131,9 +140,10 @@ class TestEnginesAreSatisfiable: """ desktop = json.loads((REPO_ROOT / "apps" / "desktop" / "package.json").read_text()) node_range = desktop["engines"]["node"] - # Vite 8's own floor. If this ever legitimately rises, the assertion + # The tightest floor any dependency actually declares (react-router + # 8.3.0 -> >=22.22.0). If this legitimately rises, the assertion # documents the reason for the bump rather than blocking it. - assert _satisfies_range("22.12.0", node_range), ( + assert _satisfies_range("22.22.0", node_range), ( f"apps/desktop engines.node is {node_range!r}, which rejects Node " "22.12 — stricter than Vite requires. A desktop floor above the " "build toolchain's own floor replaces working user toolchains for "