# Prefetch official Node, Corepack pnpm, and lockfile packages into in-repo caches. # Run on the same OS/CPU as the later install, while nodejs.org and the npm registry # are reachable. Node/pnpm cannot use socks5h:// proxies; pass an HTTP proxy via # DSH_NPM_HTTP_PROXY (example: http://127.0.0.1:3067). That overlay is applied only # around Corepack/pnpm and is restored before the script returns, so a caller SOCKS # proxy stays in the current PowerShell session. curl keeps the inherited proxy # (SOCKS included). Optional DSH_NODE_VERSION (no leading v) pins the Node distro; # otherwise the running Node is used, or the newest v24.x from nodejs.org/dist/index.json. # # Usage (repo root): powershell -File scripts/offline/download-deps.ps1 $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest $Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path Set-Location $Root $ProxyVarNames = @('HTTPS_PROXY', 'HTTP_PROXY', 'ALL_PROXY', 'https_proxy', 'http_proxy', 'all_proxy') function Get-ProcessVar([string]$Name) { return [Environment]::GetEnvironmentVariable($Name, 'Process') } function Get-InheritedProxy { foreach ($name in $ProxyVarNames) { $value = Get-ProcessVar $name if ($value) { return $value } } return $null } function Resolve-NodeHttpProxy { $override = Get-ProcessVar 'DSH_NPM_HTTP_PROXY' if ($override) { if ($override -match '^socks5') { throw "DSH_NPM_HTTP_PROXY=$override. Node and pnpm cannot fetch through SOCKS. Use an HTTP proxy (for example http://127.0.0.1:3067)." } return $override } $socks = $null foreach ($name in $ProxyVarNames) { $value = Get-ProcessVar $name if (-not $value) { continue } if ($value -match '^socks5') { $socks = "$name=$value" continue } return $value } if ($socks) { throw "$socks. Node and pnpm cannot fetch through SOCKS. Set DSH_NPM_HTTP_PROXY to an HTTP proxy (for example http://127.0.0.1:3067)." } return $null } function Invoke-WithNodeHttpProxy([scriptblock]$Action) { $http = Resolve-NodeHttpProxy if (-not $http) { & $Action return } $saved = @{} foreach ($name in $ProxyVarNames) { $saved[$name] = Get-ProcessVar $name } try { foreach ($name in $ProxyVarNames) { [Environment]::SetEnvironmentVariable($name, $http, 'Process') } & $Action } finally { foreach ($name in $ProxyVarNames) { [Environment]::SetEnvironmentVariable($name, $saved[$name], 'Process') } } } function Get-CurlProxyArgs { $proxy = Get-InheritedProxy if (-not $proxy) { $proxy = Get-ProcessVar 'DSH_NPM_HTTP_PROXY' } if ($proxy) { @('--proxy', $proxy) } else { @() } } function Save-Url([string]$Url, [string]$Destination) { $dir = Split-Path -Parent $Destination New-Item -ItemType Directory -Force -Path $dir | Out-Null $curl = Get-Command curl.exe -ErrorAction SilentlyContinue if ($null -eq $curl) { throw 'curl.exe is required to download Node (bundled with Windows 10+).' } & $curl.Source -fsSL --retry 3 @(Get-CurlProxyArgs) -o $Destination $Url if ($LASTEXITCODE -ne 0) { throw "Download failed ($LASTEXITCODE): $Url" } } function Get-NodePlatform { $arch = $env:PROCESSOR_ARCHITECTURE if ($arch -eq 'ARM64') { return 'win-arm64' } if ($arch -eq 'AMD64') { return 'win-x64' } throw "Unsupported Windows architecture: $arch" } function Get-NodeArchiveName([string]$Version, [string]$Platform) { return "node-v$Version-$Platform.zip" } function Resolve-NodeVersion([string]$Platform) { if ($env:DSH_NODE_VERSION) { return $env:DSH_NODE_VERSION.TrimStart('v') } $node = Get-Command node -ErrorAction SilentlyContinue if ($null -ne $node) { $running = (& $node.Source --version).Trim().TrimStart('v') $parts = $running.Split('.') $major = [int]$parts[0] $minor = [int]$parts[1] if (-not (($major -eq 22 -and $minor -ge 19) -or ($major -ge 24))) { throw "Running Node v$running is outside engines.node (^22.19.0 || >=24.0.0). Set DSH_NODE_VERSION." } return $running } $indexPath = Join-Path $env:TEMP 'dsh-node-index.json' $distBase = if ($env:DSH_NODE_DIST_BASE) { $env:DSH_NODE_DIST_BASE.TrimEnd('/') } else { 'https://nodejs.org/dist' } Save-Url "$distBase/index.json" $indexPath $releases = Get-Content $indexPath -Raw | ConvertFrom-Json $wantedFile = if ($Platform -eq 'win-arm64') { 'win-arm64-zip' } else { 'win-x64-zip' } foreach ($release in $releases) { $ver = [string]$release.version if ($ver -notmatch '^v24\.') { continue } if (@($release.files) -notcontains $wantedFile) { continue } return $ver.TrimStart('v') } throw "No Node v24.x zip published for $Platform at $distBase." } function Assert-NodeChecksum([string]$SumsPath, [string]$ArchivePath, [string]$ArchiveName) { $expected = $null foreach ($line in Get-Content $SumsPath) { if ($line -match ('^[0-9a-fA-F]{64} ' + [regex]::Escape($ArchiveName) + '$')) { $expected = $line.Substring(0, 64) break } } if (-not $expected) { throw "SHASUMS256.txt has no entry for $ArchiveName" } $actual = (Get-FileHash -Algorithm SHA256 -Path $ArchivePath).Hash if ($actual.ToLowerInvariant() -ne $expected.ToLowerInvariant()) { throw "SHA256 mismatch for $ArchiveName" } } $manifest = Get-Content (Join-Path $Root 'package.json') -Raw | ConvertFrom-Json $packageManager = [string]$manifest.packageManager if ($packageManager -notmatch '^pnpm@') { throw "package.json packageManager must be pnpm@, got $packageManager" } $distBase = if ($env:DSH_NODE_DIST_BASE) { $env:DSH_NODE_DIST_BASE.TrimEnd('/') } else { 'https://nodejs.org/dist' } $platform = Get-NodePlatform $nodeVersion = Resolve-NodeVersion $platform $archiveName = Get-NodeArchiveName $nodeVersion $platform $nodeCache = Join-Path $Root '.offline-cache\node' $archivePath = Join-Path $nodeCache $archiveName $sumsPath = Join-Path $nodeCache "SHASUMS256-v$nodeVersion.txt" $runtimeDir = Join-Path $nodeCache 'runtime' $nodeHome = Join-Path $runtimeDir "node-v$nodeVersion-$platform" Write-Host "Prefetch Node v$nodeVersion ($platform)" Save-Url "$distBase/v$nodeVersion/SHASUMS256.txt" $sumsPath Save-Url "$distBase/v$nodeVersion/$archiveName" $archivePath Assert-NodeChecksum $sumsPath $archivePath $archiveName if (-not (Test-Path (Join-Path $nodeHome 'node.exe'))) { if (Test-Path $runtimeDir) { Remove-Item -Recurse -Force $runtimeDir } New-Item -ItemType Directory -Force -Path $runtimeDir | Out-Null Expand-Archive -LiteralPath $archivePath -DestinationPath $runtimeDir } if (-not (Test-Path (Join-Path $nodeHome 'node.exe'))) { throw "Unpacked Node is missing: $nodeHome\node.exe" } $json = @{ version = $nodeVersion archive = $archiveName platform = $platform } | ConvertTo-Json [System.IO.File]::WriteAllText((Join-Path $nodeCache 'runtime.json'), $json) $storeDir = Join-Path $Root '.offline-store' $corepackHome = Join-Path $Root '.offline-cache\corepack' New-Item -ItemType Directory -Force -Path $storeDir, $corepackHome | Out-Null $env:COREPACK_HOME = $corepackHome $env:PATH = "$nodeHome;$env:PATH" Write-Host "Node $((& (Join-Path $nodeHome 'node.exe') --version)); prefetch $packageManager into $storeDir" Invoke-WithNodeHttpProxy { & (Join-Path $nodeHome 'corepack.cmd') prepare $packageManager --activate if ($LASTEXITCODE -ne 0) { throw "corepack prepare failed ($LASTEXITCODE)" } & (Join-Path $nodeHome 'corepack.cmd') pnpm fetch --frozen-lockfile --store-dir $storeDir if ($LASTEXITCODE -ne 0) { throw "pnpm fetch failed ($LASTEXITCODE)" } } Write-Host @" Prefetch complete (Node v$nodeVersion zip + pnpm store). Copy this checkout (including .offline-store and .offline-cache) to the offline machine. Then run: powershell -File scripts/offline/install-deps.ps1 "@