From 902cd05147ddd9291b885dd146a80adabe4d9e0a Mon Sep 17 00:00:00 2001 From: Gille <4317663+helix4u@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:11:27 -0600 Subject: [PATCH] fix(install): align npm gate with manifest range --- scripts/install.ps1 | 45 ++++---- .../test-install-ps1-node-compatibility.ps1 | 104 +++++++++--------- 2 files changed, 74 insertions(+), 75 deletions(-) diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 2347310ec7..0c12bf8a62 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -395,10 +395,9 @@ $NodeVersion = "22" # The npm range the root package.json pins in `engines.npm`. A constant rather # than a manifest read like the POSIX side does: Test-Node runs BEFORE the repo # is cloned, so there is usually no package.json on disk yet (and none at all -# when install.ps1 is piped straight from the web). Get-NpmRange prefers the -# manifest whenever it does exist, so a drifted constant self-corrects on any -# run against an existing checkout. -$NpmRange = ">=12.0.0" +# when install.ps1 is piped straight from the web). Keep this fallback in sync +# with package.json; Get-NpmRange prefers the manifest once a checkout exists. +$NpmRange = "<11.10.0 || >=11.17.0" # Stage-protocol version. Bumped only for genuinely breaking changes to the # manifest schema, stage-name set semantics, or stdout JSON shape. Adding a @@ -1016,14 +1015,11 @@ function Test-NpmVersionOk { return $false } -# Upgrade the Hermes-managed Node tree's bundled npm into $NpmRange. -# -# The nodejs.org zip ships whatever npm that Node major bundles -- Node 26.5.1 -# bundles npm 11.17.0, one minor below the root package.json's own -# `engines.npm` floor of >=12. The repo .npmrc sets `engine-strict=true`, so -# that is fatal rather than a warning and a brand-new install dies at the first -# `npm ci` with EBADENGINE. Provision the right npm here instead of reacting -# to the failure later. +# Upgrade the Hermes-managed Node tree's bundled npm into $NpmRange when +# needed. Managed Node trees survive updates, so their bundled npm can drift +# outside a newer root package.json engine range. The repo .npmrc sets +# `engine-strict=true`, making that mismatch fatal at the first `npm ci`. +# Provision the right npm here instead of reacting to EBADENGINE later. # # Three details are load-bearing, mirroring _nb_ensure_bundled_npm_range in # scripts/lib/node-bootstrap.sh and upgrade_managed_npm in @@ -1045,17 +1041,11 @@ function Update-ManagedNpm { $range = Get-NpmRange # Skip the network round-trip when the bundled npm already satisfies the - # range. Only the ">=N" shape we actually author is parsed; anything more - # exotic falls through to letting npm itself decide. - if ($range -match '^>=(\d+)') { - $want = [int]$Matches[1] - try { - $have = (& $npmCmd --version 2>$null) - if ($have -match '^(\d+)') { - if ([int]$Matches[1] -ge $want) { return $true } - } - } catch { } - } + # same range used by the system-Node acceptance gate. + try { + $have = (& $npmCmd --version 2>$null | Select-Object -First 1) + if ($have -and (Test-NpmVersionOk $have $range)) { return $true } + } catch { } # In-app updates run while the desktop app's Node processes are alive. # The managed npm lives inside the very tree they execute from, so an @@ -4003,7 +3993,7 @@ function Install-Desktop { # 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 (Node >=26) and prepends the Hermes-managed + # enforces the build floor (Node >=22.22.0) 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). @@ -4918,6 +4908,13 @@ function Main { # iex` PowerShell session, and so failures in stage-driver mode produce a # structured JSON error frame instead of a bare exception. +# Dot-sourcing loads the installer's real functions for isolated behavioral +# tests without running an install. Normal script and `irm | iex` entry points +# are unchanged. +if ($MyInvocation.InvocationName -eq ".") { + return +} + try { if ($Ensure -ne "") { if ($PSBoundParameters.ContainsKey("Stage")) { diff --git a/scripts/tests/test-install-ps1-node-compatibility.ps1 b/scripts/tests/test-install-ps1-node-compatibility.ps1 index abdb06a823..ffd7d493e1 100644 --- a/scripts/tests/test-install-ps1-node-compatibility.ps1 +++ b/scripts/tests/test-install-ps1-node-compatibility.ps1 @@ -1,43 +1,19 @@ # Behavioral tests for install.ps1 system Node/npm compatibility selection. # -# The installer itself is not executed. The real shipped functions are lifted -# through the PowerShell AST, then external commands and downloads are replaced -# with deterministic in-process stubs. This exercises the actual range parser -# and Test-Node acceptance gate without changing PATH, installing software, or -# touching the user's Hermes home. - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' +# The installer is dot-sourced without running its entry point, then external +# commands and downloads are replaced with deterministic in-process stubs. +# This exercises the shipped range parser and Test-Node acceptance gate without +# changing PATH, installing software, or touching the user's Hermes home. $repoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)) $installScript = Join-Path $repoRoot 'scripts\install.ps1' -$tokens = $null -$parseErrors = $null -$ast = [System.Management.Automation.Language.Parser]::ParseFile( - $installScript, [ref]$tokens, [ref]$parseErrors -) -if ($parseErrors.Count -gt 0) { - throw "install.ps1 has parse errors: $($parseErrors -join '; ')" -} +$testRoot = Join-Path $env:TEMP ("hermes-node-compatibility-test-" + [Guid]::NewGuid().ToString('N')) +$HermesHome = Join-Path $testRoot 'home' +$InstallDir = Join-Path $testRoot 'missing-checkout' +. $installScript -HermesHome $HermesHome -InstallDir $InstallDir -foreach ($name in @( - 'ConvertTo-NpmVersion', - 'Test-NpmVersionOk', - 'Test-NodeVersionOk', - 'Test-SystemNodeReady', - 'Test-Node' -)) { - $fn = $ast.FindAll( - { - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - $node.Name -eq $name - }, - $true - ) | Select-Object -First 1 - if (-not $fn) { throw "$name not found in install.ps1" } - . ([scriptblock]::Create($fn.Extent.Text)) -} +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' $script:Failures = 0 function Assert-Equal { @@ -53,26 +29,25 @@ function Assert-Equal { } Write-Host '-- npm range evaluation --' -$supportedRange = '<11.10.0 || >=11.17.0' -Assert-Equal $true (Test-NpmVersionOk '11.9.9' $supportedRange) 'lower alternative is accepted' -Assert-Equal $false (Test-NpmVersionOk '11.10.0' $supportedRange) 'excluded band starts at 11.10.0' -Assert-Equal $false (Test-NpmVersionOk '11.16.0' $supportedRange) 'reported npm 11.16.0 is rejected' -Assert-Equal $true (Test-NpmVersionOk '11.17.0' $supportedRange) 'upper alternative starts at 11.17.0' -Assert-Equal $false (Test-NpmVersionOk '11.17.0' '>=12.0.0') 'pre-clone npm floor rejects 11.x' -Assert-Equal $true (Test-NpmVersionOk '12.0.0' '>=12.0.0') 'pre-clone npm floor accepts 12.0.0' -Assert-Equal $false (Test-NpmVersionOk 'not-a-version' $supportedRange) 'malformed version fails closed' +$supportedRange = Get-NpmRange +Assert-Equal '<11.10.0 || >=11.17.0' $supportedRange 'fresh-install fallback matches the supported npm range' +Assert-Equal $true (Test-NpmVersionOk '10.9.8') 'bundled npm 10.9.8 is accepted before clone' +Assert-Equal $true (Test-NpmVersionOk '11.9.9') 'lower alternative is accepted' +Assert-Equal $false (Test-NpmVersionOk '11.10.0') 'excluded band starts at 11.10.0' +Assert-Equal $false (Test-NpmVersionOk '11.16.0') 'reported npm 11.16.0 is rejected' +Assert-Equal $true (Test-NpmVersionOk '11.17.0') 'upper alternative starts at 11.17.0' +Assert-Equal $false (Test-NpmVersionOk 'not-a-version') 'malformed version fails closed' Assert-Equal $false (Test-NpmVersionOk '12.0.0' '^12.0.0') 'unsupported range syntax fails closed' -# Controlled command surface used by the lifted Test-Node function. +# Controlled command surface used by the real Test-Node function. $script:FakeNpmAvailable = $true $script:FakeNpmVersion = '11.16.0' -$script:FakeNpmRange = $supportedRange +$script:FakeNodeVersion = 'v24.18.0' $script:DownloadAttempts = 0 $script:HasNode = $null -$HermesHome = Join-Path $env:TEMP ("hermes-node-compatibility-test-" + [Guid]::NewGuid().ToString('N')) $NodeVersion = '22' -function node { 'v24.18.0' } +function node { $script:FakeNodeVersion } function npm.cmd { $script:FakeNpmVersion } function Get-Command { [CmdletBinding()] @@ -93,7 +68,6 @@ function Get-Command { default { return $null } } } -function Get-NpmRange { $script:FakeNpmRange } function Ensure-NodeExeOnPath { $true } function Get-WindowsArch { 'x64' } function Invoke-WebRequest { @@ -105,8 +79,13 @@ function Write-Warn { param([string]$Message) } function Write-Success { param([string]$Message) } function Invoke-SystemNodeProbe { - param([string]$NpmVersion, [bool]$NpmAvailable = $true) + param( + [string]$NodeVersion, + [string]$NpmVersion, + [bool]$NpmAvailable = $true + ) + $script:FakeNodeVersion = $NodeVersion $script:FakeNpmVersion = $NpmVersion $script:FakeNpmAvailable = $NpmAvailable $script:DownloadAttempts = 0 @@ -120,18 +99,37 @@ function Invoke-SystemNodeProbe { Write-Host '' Write-Host '-- system Node acceptance --' -$result = Invoke-SystemNodeProbe '11.17.0' +$result = Invoke-SystemNodeProbe 'v24.18.0' '11.17.0' Assert-Equal $true $result.HasNode 'compatible system Node/npm is accepted' Assert-Equal 0 $result.DownloadAttempts 'compatible system npm avoids managed download' -$result = Invoke-SystemNodeProbe '11.16.0' +$result = Invoke-SystemNodeProbe 'v22.22.0' '10.9.8' +Assert-Equal $true $result.HasNode 'minimum Node with bundled npm is accepted' +Assert-Equal 0 $result.DownloadAttempts 'bundled npm avoids managed download' + +$result = Invoke-SystemNodeProbe 'v24.18.0' '11.16.0' Assert-Equal $false $result.HasNode 'incompatible system npm is not accepted' Assert-Equal 1 $result.DownloadAttempts 'incompatible system npm falls through to managed Node' -$result = Invoke-SystemNodeProbe '' $false +$result = Invoke-SystemNodeProbe 'v24.18.0' '' $false Assert-Equal $false $result.HasNode 'missing system npm is not accepted' Assert-Equal 1 $result.DownloadAttempts 'missing system npm falls through to managed Node' +Write-Host '' +Write-Host '-- managed npm reuse --' +$managedDir = Join-Path $testRoot 'managed-node' +New-Item -ItemType Directory -Force -Path $managedDir | Out-Null +$managedNpm = Join-Path $managedDir 'npm.cmd' +@' +@echo off +if "%~1"=="--version" ( + echo 10.9.8 + exit /b 0 +) +exit /b 42 +'@ | Set-Content -LiteralPath $managedNpm -Encoding Ascii +Assert-Equal $true (Update-ManagedNpm $managedDir) 'compatible managed npm skips the upgrade command' + if ($script:Failures -gt 0) { Write-Host '' Write-Host "$script:Failures assertion(s) failed" @@ -140,3 +138,7 @@ if ($script:Failures -gt 0) { Write-Host '' Write-Host 'all assertions passed' + +if (Test-Path $testRoot) { + Remove-Item -LiteralPath $testRoot -Recurse -Force +}