fix(install): align npm gate with manifest range
This commit is contained in:
+21
-24
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user