fix(install): require Node >=20.19/22.12 for the desktop build

The "Build desktop app" install step failed with an opaque "exit code 1"
on machines with an old Node, and nothing in the logs explained it.

Reproduced: on Node 20.5.1, `npm run pack`'s `vite build` crashes with

  You are using Node.js 20.5.1. Vite requires Node.js version 20.19+ or 22.12+.
  SyntaxError: The requested module 'node:util' does not provide an
  export named 'styleText'

Vite 8 (rolldown) imports node:util.styleText, which doesn't exist before
Node 20.12, so the build dies before producing the app. The installer's
check_node / Test-Node accepted ANY pre-existing Node with no version
floor, so a too-old system Node was used for the build instead of the
bundled Node 22.

Add a version floor (^20.19 || >=22.12) to check_node (install.sh) and
Test-Node (install.ps1): a too-old system Node is replaced with the
Hermes-managed Node 22 LTS, and the desktop stage re-resolves Node so the
build always runs on a satisfying version. Declare the same range in
apps/desktop/package.json engines.

Verified: build succeeds on Node 22, fails on 20.5.1 with the error above;
the floor logic matches Vite's range across boundary versions (20.18/20.19,
21.x, 22.11/22.12).
This commit is contained in:
Brooklyn Nicholson
2026-06-03 09:19:04 -05:00
parent 214b7e070f
commit 1dca7c6207
3 changed files with 71 additions and 29 deletions

View File

@ -714,19 +714,39 @@ 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` -- 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. Returns $true when a `node --version`
# string clears that floor.
function Test-NodeVersionOk {
param([string]$Version)
try {
$v = [version]($Version -replace '^v', '' -replace '-.*$', '')
} catch {
return $false
}
if ($v.Major -eq 20 -and $v.Minor -ge 19) { return $true }
if ($v.Major -ge 22 -and ($v.Major -gt 22 -or $v.Minor -ge 12)) { return $true }
return $false
}
function Test-Node {
Write-Info "Checking Node.js (for browser tools)..."
if (Get-Command node -ErrorAction SilentlyContinue) {
$version = node --version
Write-Success "Node.js $version found"
$script:HasNode = $true
return $true
if (Test-NodeVersionOk $version) {
Write-Success "Node.js $version found"
$script:HasNode = $true
return $true
}
Write-Warn "Node.js $version is too old for the desktop build (need ^20.19 or >=22.12)"
}
# Check our own managed install from a previous run
# Prefer a Hermes-managed Node from a previous run over a too-old system one.
$managedNode = "$HermesHome\node\node.exe"
if (Test-Path $managedNode) {
if ((Test-Path $managedNode) -and (Test-NodeVersionOk (& $managedNode --version))) {
$version = & $managedNode --version
$env:Path = "$HermesHome\node;$env:Path"
Write-Success "Node.js $version found (Hermes-managed)"
@ -734,7 +754,7 @@ function Test-Node {
return $true
}
Write-Info "Node.js not found -- installing Node.js $NodeVersion LTS..."
Write-Info "Installing Hermes-managed Node.js $NodeVersion LTS..."
# Try the portable-zip path FIRST -- no UAC, no admin, no winget MSI.
# winget install OpenJS.NodeJS.LTS triggers a system-wide MSI install
@ -1906,16 +1926,17 @@ function Install-Desktop {
# so an "unpacked" build (electron-builder --dir) is enough — we
# don't need to produce an NSIS/MSI artifact here.
if (-not $HasNode) {
# Cross-process driver mode: each `-Stage NAME` invocation runs in a
# fresh PowerShell process, so $script:HasNode set by Stage-Node
# in the previous process isn't visible. Re-detect rather than
# trusting the global.
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
Write-Warn "Skipping desktop build (Node.js / npm not on PATH)"
$script:_StageSkippedReason = "Node.js not available"
return
}
# Always re-resolve Node here. Stages run in separate PowerShell processes,
# so $script:HasNode from Stage-Node isn't visible; more importantly Test-Node
# enforces the build floor (^20.19 || >=22.12) and prepends the Hermes-managed
# Node to PATH, so the build never runs on a too-old system Node -- the cause
# of the opaque "Build desktop app ... exit code 1" failure (Vite crashes on
# old Node).
Test-Node | Out-Null
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
Write-Warn "Skipping desktop build (Node.js / npm not on PATH)"
$script:_StageSkippedReason = "Node.js not available"
return
}
$desktopDir = "$InstallDir\apps\desktop"

View File

@ -702,26 +702,43 @@ 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. Returns 0 when the given `node --version`
# string clears that floor; anything below it is replaced with the Hermes-
# managed Node $NODE_VERSION LTS.
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
return 1
}
check_node() {
log_info "Checking Node.js (for browser tools)..."
if command -v node &> /dev/null; then
local found_ver=$(node --version)
log_success "Node.js $found_ver found"
if command -v node &> /dev/null && node_satisfies_build "$(node --version)"; then
log_success "Node.js $(node --version) found"
HAS_NODE=true
return 0
fi
# Check our own managed install from a previous run
if [ -x "$HERMES_HOME/node/bin/node" ]; then
# Prefer a Hermes-managed Node from a previous run over a too-old system one.
if [ -x "$HERMES_HOME/node/bin/node" ] && node_satisfies_build "$("$HERMES_HOME/node/bin/node" --version)"; then
export PATH="$HERMES_HOME/node/bin:$PATH"
local found_ver=$("$HERMES_HOME/node/bin/node" --version)
log_success "Node.js $found_ver found (Hermes-managed)"
log_success "Node.js $("$HERMES_HOME/node/bin/node" --version) found (Hermes-managed)"
HAS_NODE=true
return 0
fi
if [ "$DISTRO" = "termux" ]; then
if command -v node &> /dev/null; then
log_warn "Node.js $(node --version) is too old for the desktop build (need ^20.19 or >=22.12) — installing Hermes-managed Node $NODE_VERSION LTS..."
elif [ "$DISTRO" = "termux" ]; then
log_info "Node.js not found — installing Node.js via pkg..."
else
log_info "Node.js not found — installing Node.js $NODE_VERSION LTS..."
@ -2235,11 +2252,12 @@ install_desktop() {
# (--include-desktop / 'desktop' stage), so a missing toolchain is a hard
# failure, not a silent skip — a silent skip yields a "complete" install
# with no app and a confusing "couldn't find a built desktop" at launch.
# Try the Hermes-managed Node first (check_node adds $HERMES_HOME/node/bin
# to PATH or installs it) before giving up.
if ! command -v npm >/dev/null 2>&1; then
check_node
fi
# Always re-resolve Node here. Stages run in separate processes, so we can't
# trust an earlier check; more importantly check_node now enforces the build
# floor (^20.19 || >=22.12) and prepends the Hermes-managed Node to PATH, so
# the build never runs on a too-old system Node — the cause of the opaque
# "Build desktop app … exit code 1" failure (Vite crashes on old Node).
check_node
if ! command -v npm >/dev/null 2>&1; then
log_error "Cannot build desktop app: Node.js / npm unavailable"
log_info "Install Node.js and retry: cd $desktop_dir && npm run pack"