Backend CI / type-check, build, tests (push) Successful in 1m0s
Co-authored-by: Cursor <cursoragent@cursor.com>
281 lines
9.2 KiB
PowerShell
281 lines
9.2 KiB
PowerShell
# Starts Child Gold Land backend + frontend for local dev.
|
|
# Manages only processes started by this script. No npm install / rebuild on each run.
|
|
#
|
|
# Usage:
|
|
# pwsh -File .\scripts\dev-local-stack.ps1
|
|
# pwsh -File .\scripts\dev-local-stack.ps1 -UseTestPorts -SmokeTest # alt ports + health/login probe then exit
|
|
#
|
|
# Default ports: backend 3000, frontend 5174. Test ports: 3010 / 5184 (when 3000/5174 are busy).
|
|
|
|
param(
|
|
[switch]$UseTestPorts,
|
|
[switch]$SmokeTest
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$BackendRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
|
$WorkspaceRoot = (Resolve-Path (Join-Path $BackendRoot '..')).Path
|
|
$FrontendRoot = Join-Path $WorkspaceRoot 'frontend'
|
|
|
|
if ($UseTestPorts) {
|
|
$BackendPort = 3010
|
|
$FrontendPort = 5190
|
|
} else {
|
|
$BackendPort = 3000
|
|
$FrontendPort = 5174
|
|
}
|
|
|
|
$HealthUrl = "http://127.0.0.1:${BackendPort}/health"
|
|
$FrontendUrl = "http://localhost:${FrontendPort}/"
|
|
$LoginProxyUrl = "http://localhost:${FrontendPort}/api/auth/login"
|
|
|
|
$script:ManagedProcesses = @()
|
|
$script:TempDbDir = $null
|
|
|
|
function Write-Info([string]$Message) { Write-Host $Message -ForegroundColor Cyan }
|
|
function Write-Warn([string]$Message) { Write-Host $Message -ForegroundColor Yellow }
|
|
function Write-Err([string]$Message) { Write-Host $Message -ForegroundColor Red }
|
|
|
|
function Test-NodeEngines {
|
|
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
|
throw 'Node.js not found in PATH. Install Node ^22.18.0 or >=24.12.0 (see .nvmrc).'
|
|
}
|
|
$raw = (node -v).TrimStart('v')
|
|
$ver = [version]$raw
|
|
$ok = ($ver.Major -eq 22 -and $ver -ge [version]'22.18.0') -or ($ver.Major -ge 24 -and $ver -ge [version]'24.12.0')
|
|
if (-not $ok) {
|
|
throw "Node v$raw is not supported. Required: ^22.18.0 or >=24.12.0. Run: nvm install 22.18.0 && nvm use 22.18.0"
|
|
}
|
|
Write-Info "Node $(node -v) at $((Get-Command node).Source)"
|
|
}
|
|
|
|
function Test-RepoLayout {
|
|
if (-not (Test-Path (Join-Path $BackendRoot 'package.json'))) {
|
|
throw "Backend package.json not found at $BackendRoot"
|
|
}
|
|
if (-not (Test-Path (Join-Path $FrontendRoot 'package.json'))) {
|
|
throw "Frontend not found at `"$FrontendRoot`" (expected sibling of backend-nest)."
|
|
}
|
|
}
|
|
|
|
function Test-DependenciesPresent {
|
|
if (-not (Test-Path (Join-Path $BackendRoot 'node_modules'))) {
|
|
throw "Backend node_modules missing. Run once: cd `"$BackendRoot`"; npm install"
|
|
}
|
|
if (-not (Test-Path (Join-Path $FrontendRoot 'node_modules'))) {
|
|
throw "Frontend node_modules missing. Run once: cd `"$FrontendRoot`"; npm install"
|
|
}
|
|
}
|
|
|
|
function Get-ListenProcessId([int]$Port) {
|
|
$conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($conn) { return [int]$conn.OwningProcess }
|
|
return $null
|
|
}
|
|
|
|
function Assert-PortFree([int]$Port, [string]$Label) {
|
|
$ownerPid = Get-ListenProcessId $Port
|
|
if ($null -ne $ownerPid) {
|
|
$name = (Get-Process -Id $ownerPid -ErrorAction SilentlyContinue).ProcessName
|
|
throw "${Label} port $Port is in use (PID $ownerPid, $name). Use -UseTestPorts or free the port."
|
|
}
|
|
}
|
|
|
|
function New-BackendProcessEnvironment {
|
|
$envMap = @{}
|
|
foreach ($entry in [System.Environment]::GetEnvironmentVariables('Process').GetEnumerator()) {
|
|
$envMap[$entry.Key] = [string]$entry.Value
|
|
}
|
|
$envMap['PORT'] = [string]$BackendPort
|
|
$envMap['HOST'] = '0.0.0.0'
|
|
$envMap['NODE_ENV'] = 'development'
|
|
$envMap['CORS_ORIGIN'] = "http://localhost:${FrontendPort}"
|
|
if ($UseTestPorts) {
|
|
$script:TempDbDir = Join-Path ([System.IO.Path]::GetTempPath()) ("cgl-devstack-" + [guid]::NewGuid().ToString('N'))
|
|
New-Item -ItemType Directory -Path $script:TempDbDir -Force | Out-Null
|
|
$envMap['DB_PATH'] = Join-Path $script:TempDbDir 'app.db'
|
|
Write-Info "Test backend DB: $($envMap['DB_PATH'])"
|
|
}
|
|
return $envMap
|
|
}
|
|
|
|
function Start-ProcessWithEnv {
|
|
param(
|
|
[string]$FilePath,
|
|
[string[]]$ArgumentList,
|
|
[string]$WorkingDirectory,
|
|
[hashtable]$Environment
|
|
)
|
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
$psi.FileName = $FilePath
|
|
$psi.WorkingDirectory = $WorkingDirectory
|
|
$psi.UseShellExecute = $false
|
|
$psi.CreateNoWindow = $true
|
|
if ($ArgumentList -and $ArgumentList.Count -gt 0) {
|
|
$quoted = $ArgumentList | ForEach-Object {
|
|
if ($_ -match '\s') { "`"$_`"" } else { $_ }
|
|
}
|
|
$psi.Arguments = $quoted -join ' '
|
|
}
|
|
foreach ($entry in [System.Environment]::GetEnvironmentVariables('Process').GetEnumerator()) {
|
|
$psi.EnvironmentVariables[[string]$entry.Key] = [string]$entry.Value
|
|
}
|
|
foreach ($key in $Environment.Keys) {
|
|
$psi.EnvironmentVariables[$key] = [string]$Environment[$key]
|
|
}
|
|
$proc = [System.Diagnostics.Process]::Start($psi)
|
|
if (-not $proc) {
|
|
throw "Failed to start process: $FilePath"
|
|
}
|
|
return $proc
|
|
}
|
|
|
|
function Start-Backend {
|
|
$mainJs = Join-Path $BackendRoot 'dist\main.js'
|
|
if (-not (Test-Path $mainJs)) {
|
|
throw "Missing dist/main.js. Run once: cd `"$BackendRoot`"; npm run build"
|
|
}
|
|
$node = (Get-Command node).Source
|
|
$envMap = New-BackendProcessEnvironment
|
|
Write-Info "Starting backend on port $BackendPort ..."
|
|
$p = Start-ProcessWithEnv -FilePath $node -ArgumentList @($mainJs) -WorkingDirectory $BackendRoot -Environment $envMap
|
|
$script:ManagedProcesses += $p
|
|
Start-Sleep -Milliseconds 500
|
|
if ($p.HasExited) {
|
|
throw "Backend exited immediately (exit $($p.ExitCode)). Build with current Node and check dist/main.js."
|
|
}
|
|
return $p
|
|
}
|
|
|
|
function Start-Frontend {
|
|
$npm = (Get-Command npm.cmd -ErrorAction SilentlyContinue).Source
|
|
if (-not $npm) {
|
|
$npm = (Get-Command npm -ErrorAction SilentlyContinue).Source
|
|
}
|
|
if (-not $npm) {
|
|
throw 'npm not found in PATH.'
|
|
}
|
|
$envMap = @{}
|
|
foreach ($entry in [System.Environment]::GetEnvironmentVariables('Process').GetEnumerator()) {
|
|
$envMap[$entry.Key] = [string]$entry.Value
|
|
}
|
|
$envMap['VITE_DEV_PORT'] = [string]$FrontendPort
|
|
$envMap['VITE_DEV_API_TARGET'] = "http://127.0.0.1:${BackendPort}"
|
|
Write-Info "Starting frontend on port $FrontendPort (proxy -> $($envMap['VITE_DEV_API_TARGET'])) ..."
|
|
$p = Start-ProcessWithEnv -FilePath $npm -ArgumentList @('run', 'dev', '--', '--port', [string]$FrontendPort) `
|
|
-WorkingDirectory $FrontendRoot -Environment $envMap
|
|
$script:ManagedProcesses += $p
|
|
return $p
|
|
}
|
|
|
|
function Wait-BackendHealth {
|
|
param([int]$TimeoutSeconds = 90)
|
|
Write-Info "Waiting for backend health (max ${TimeoutSeconds}s) ..."
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
while ((Get-Date) -lt $deadline) {
|
|
foreach ($p in $script:ManagedProcesses) {
|
|
if ($p.HasExited) {
|
|
throw "Managed process exited early (PID $($p.Id), exit $($p.ExitCode))."
|
|
}
|
|
}
|
|
try {
|
|
$r = Invoke-WebRequest -Uri $HealthUrl -UseBasicParsing -TimeoutSec 3
|
|
if ($r.StatusCode -eq 200) {
|
|
Write-Info "Backend health OK: $HealthUrl"
|
|
return
|
|
}
|
|
} catch {
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
}
|
|
throw "Backend health timed out: $HealthUrl"
|
|
}
|
|
|
|
function Wait-FrontendHttp {
|
|
param([int]$TimeoutSeconds = 120)
|
|
Write-Info "Waiting for frontend (max ${TimeoutSeconds}s) ..."
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
while ((Get-Date) -lt $deadline) {
|
|
try {
|
|
$r = Invoke-WebRequest -Uri $FrontendUrl -UseBasicParsing -TimeoutSec 3
|
|
if ($r.StatusCode -eq 200) {
|
|
Write-Info "Frontend OK: $FrontendUrl"
|
|
return
|
|
}
|
|
} catch {
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
}
|
|
throw "Frontend HTTP timed out: $FrontendUrl"
|
|
}
|
|
|
|
function Test-ProxyLogin {
|
|
$body = '{"username":"demo","password":"123456"}'
|
|
$r = Invoke-WebRequest -Uri $LoginProxyUrl -Method POST -Body $body -ContentType 'application/json' -UseBasicParsing
|
|
if ($r.StatusCode -ne 200) {
|
|
throw "Proxy login failed: HTTP $($r.StatusCode)"
|
|
}
|
|
Write-Info "Proxy login OK: $LoginProxyUrl"
|
|
}
|
|
|
|
function Stop-ManagedProcesses {
|
|
foreach ($p in $script:ManagedProcesses) {
|
|
if ($p -and -not $p.HasExited) {
|
|
Write-Info "Stopping PID $($p.Id) ..."
|
|
try {
|
|
& taskkill.exe /PID $p.Id /T /F 2>$null | Out-Null
|
|
} catch {
|
|
Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
$script:ManagedProcesses = @()
|
|
if ($script:TempDbDir -and (Test-Path $script:TempDbDir)) {
|
|
Remove-Item -Recurse -Force $script:TempDbDir -ErrorAction SilentlyContinue
|
|
$script:TempDbDir = $null
|
|
}
|
|
}
|
|
|
|
try {
|
|
Write-Info 'Child Gold Land — local dev stack'
|
|
if ($UseTestPorts) { Write-Warn 'Using test ports 3010 / 5190 (temp SQLite when backend starts fresh DB).' }
|
|
Test-NodeEngines
|
|
Test-RepoLayout
|
|
Test-DependenciesPresent
|
|
Assert-PortFree -Port $BackendPort -Label 'Backend'
|
|
Assert-PortFree -Port $FrontendPort -Label 'Frontend'
|
|
|
|
Start-Backend | Out-Null
|
|
Start-Frontend | Out-Null
|
|
Wait-BackendHealth
|
|
if ($SmokeTest) {
|
|
Wait-FrontendHttp
|
|
Test-ProxyLogin
|
|
Write-Info 'SmokeTest passed.'
|
|
exit 0
|
|
}
|
|
|
|
Write-Host ''
|
|
Write-Info "Frontend: $FrontendUrl"
|
|
Write-Info "Backend health: $HealthUrl"
|
|
Write-Info 'Demo login: demo / 123456'
|
|
Write-Warn 'Press Ctrl+C to stop only processes started by this script.'
|
|
Write-Host ''
|
|
|
|
while ($true) {
|
|
Start-Sleep -Seconds 2
|
|
foreach ($p in $script:ManagedProcesses) {
|
|
if ($p.HasExited) {
|
|
Write-Err "Process PID $($p.Id) exited unexpectedly (code $($p.ExitCode))."
|
|
exit 1
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
Write-Err $_.Exception.Message
|
|
exit 1
|
|
} finally {
|
|
Stop-ManagedProcesses
|
|
}
|