fix(install): align npm gate with manifest range

This commit is contained in:
Gille
2026-08-20 16:11:27 -06:00
committed by Teknium
parent 4572dbf090
commit 902cd05147
2 changed files with 74 additions and 75 deletions
+21 -24
View File
@@ -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")) {
@@ -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
}