<# .SYNOPSIS Switching the machine's Node version, shared by start-local.ps1 and run-tests.ps1. .DESCRIPTION Both scripts need Node 20 or newer for the same reason: node-pg-migrate pulls in an lru-cache that calls diagnostics_channel.tracingChannel(), which does not exist before Node 19.9. On Node 18 the migration dies inside minified library code with "(0 , U.tracingChannel) is not a function", a message that says nothing about versions. ts-jest and Playwright are subject to the same floor. Kept in one file because two copies of a version switch would drift, and the half that drifts is the half nobody runs. nvm-windows rewrites a machine-global symlink (NVM_SYMLINK, typically C:\nvm4w\nodejs) rather than changing one shell, so switching here changes the Node version for every terminal on the machine. That is intended — the point is to work in whatever shell is already open — but it is announced rather than done quietly, and it is put back afterwards. #> function Get-NodeVersionString { return (node --version 2>$null) } function Get-NodeMajor { $raw = Get-NodeVersionString if (-not $raw) { return 0 } return [int](($raw -replace '^v', '') -split '\.')[0] } <# Switches, then checks what is actually running. nvm-windows exits 0 for switches that did not take: a version it cannot find, a symlink it cannot rewrite without elevation, and — seen on this machine — a rewrite immediately after another one, where the directory symlink is briefly still the old target. So the result is verified rather than trusted, and retried once, because reporting a version that is not the one running is worse than not switching at all. nvm's own output is captured rather than discarded. Suppressing it hid the only message that explained a failed switch. #> function Use-Node { param( [Parameter(Mandatory)][string]$Version, [Parameter(Mandatory)][string]$Why, [scriptblock]$Step, [scriptblock]$Note ) if ($Step) { & $Step "Switching Node to $Version ($Why)" } if ($Note) { & $Note 'nvm changes the version for every shell on this machine, not just this one' } $output = $null foreach ($attempt in 1..2) { $output = (nvm use $Version 2>&1 | Out-String).Trim() $raw = Get-NodeVersionString if ($raw) { # `latest` is whatever nvm decided, so there is nothing to compare a # version string against — the caller checks the major instead. if ($Version -eq 'latest' -or $raw.TrimStart('v') -eq $Version.TrimStart('v')) { if ($Note) { & $Note "node $raw" } return $raw } } if ($attempt -eq 1) { if ($Note) { & $Note 'the switch has not taken yet; retrying' } Start-Sleep -Milliseconds 750 } } $running = Get-NodeVersionString if (-not $running) { throw "Node is not on PATH after 'nvm use $Version'. Check that nvm-windows is installed.`n`nnvm said:`n$output" } throw @" Asked nvm for Node $Version, but $running is still what runs. nvm said: $output nvm-windows rewrites a symlink at $env:NVM_SYMLINK, and can report success without having rewritten it. Check the version is installed, and that this shell can write that link: nvm list "@ } <# Switches to the newest installed version and insists it is new enough. `nvm use latest` picks the highest version nvm has installed locally, which can still be older than this project needs. #> function Use-NodeLatest { param([scriptblock]$Step, [scriptblock]$Note) Use-Node -Version 'latest' -Why 'this project needs Node 20 or newer' -Step $Step -Note $Note | Out-Null $major = Get-NodeMajor if ($major -lt 20) { throw @" Node is still v$major after 'nvm use latest'. This project needs Node 20 or newer. The newest version nvm has installed is too old. Install a newer one: nvm install 24.13.1 Installed versions: nvm list "@ } } <# Best effort by design. A failure to switch back must not mask the error that got us here, nor turn a passing run into a failing one — but it must still say so, because leaving the machine on the wrong version silently is how the next confusing failure starts. #> function Restore-Node { param( [Parameter(Mandatory)][string]$Version, [scriptblock]$Step, [scriptblock]$Note ) try { Use-Node -Version $Version -Why 'restoring the machine default' -Step $Step -Note $Note | Out-Null } catch { if ($Note) { & $Note "could not restore Node ${Version}: $($_.Exception.Message)" } } }