#Requires -Version 7 <# .SYNOPSIS Starts the Redefined Designs stack locally for testing and review. .DESCRIPTION Brings up a Postgres container, runs migrations, builds and starts the backend, and starts the Vite dev server. Safe to run repeatedly: an existing container is reused rather than recreated, and processes already listening are left alone. Logs and process ids go to .local/ at the repository root. .PARAMETER Fresh Drop the database container and its data before starting, so migrations run against an empty database. .PARAMETER Stop Stop the backend, the dev server and the database container, then exit. .PARAMETER DbPort Host port for Postgres. Change it if something already holds the default. .EXAMPLE .\scripts\start-local.ps1 .\scripts\start-local.ps1 -Fresh .\scripts\start-local.ps1 -Stop #> [CmdletBinding()] param( # Deliberately not 55432. That port is reserved by Hyper-V on at least one # machine here, and Docker's failure when it cannot bind does not mention # reservations, which has cost time more than once. [int]$DbPort = 55500, [int]$ApiPort = 3000, [int]$WebPort = 5173, [switch]$Fresh, [switch]$Stop ) $ErrorActionPreference = 'Stop' $RepoRoot = Split-Path -Parent $PSScriptRoot $StateDir = Join-Path $RepoRoot '.local' $PidFile = Join-Path $StateDir 'pids.json' $Container = 'redefined-designs-local-db' $DbUser = 'redefined_local' $DbPassword = 'redefined_local' $DbName = 'redefined_local' function Write-Step { param([string]$Message) Write-Host "==> $Message" -ForegroundColor Cyan } function Write-Note { param([string]$Message) Write-Host " $Message" -ForegroundColor DarkGray } function Write-Good { param([string]$Message) Write-Host " $Message" -ForegroundColor Green } # $ErrorActionPreference = 'Stop' does NOT stop the script when a native # executable exits non-zero, only when a cmdlet throws. Everything here is # node, npm or docker, so without this wrapper a failed migration is a line of # red text the script prints and then carries straight past. It did exactly # that once, and reported a healthy stack sitting on an empty database. function Invoke-Checked { param([scriptblock]$Command, [string]$What) & $Command if ($LASTEXITCODE -ne 0) { throw "$What failed (exit code $LASTEXITCODE)." } } function Assert-Docker { docker info *>$null if ($LASTEXITCODE -ne 0) { throw "Docker is not running. Start Docker Desktop and try again." } } # node-pg-migrate pulls in an lru-cache that calls # diagnostics_channel.tracingChannel, which does not exist before Node 20. On # Node 18 the migration dies in minified library code with "(0 , U.tracingChannel) # is not a function", which says nothing about versions. CI runs Node 20. function Assert-NodeVersion { $raw = (node --version) $major = [int](($raw -replace '^v', '') -split '\.')[0] if ($major -lt 20) { throw @" Node $raw is too old. This needs Node 20 or newer. If you use nvm-windows: nvm use 24.13.1 Then run this script again in a new shell. "@ } Write-Note "node $raw" } # Reads back only the ids this script wrote. Killing by port would be shorter # and would also kill whatever else happened to be listening. function Get-TrackedProcesses { if (-not (Test-Path $PidFile)) { return @{} } try { return (Get-Content $PidFile -Raw | ConvertFrom-Json -AsHashtable) } catch { return @{} } } function Stop-Tracked { param([string]$Name) $tracked = Get-TrackedProcesses if (-not $tracked.ContainsKey($Name)) { return } $process = Get-Process -Id $tracked[$Name] -ErrorAction SilentlyContinue if ($process) { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue Write-Good "stopped $Name (pid $($process.Id))" } } function Set-Tracked { param([string]$Name, [int]$ProcessId) $tracked = Get-TrackedProcesses $tracked[$Name] = $ProcessId $tracked | ConvertTo-Json | Set-Content $PidFile } function Test-Listening { param([int]$Port) return [bool](Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) } function Wait-For { param( [scriptblock]$Condition, [string]$What, [int]$TimeoutSeconds = 60 ) for ($i = 1; $i -le $TimeoutSeconds; $i++) { if (& $Condition) { Write-Good "$What ready after ${i}s" return } Start-Sleep -Seconds 1 } throw "$What did not become ready within ${TimeoutSeconds}s." } function Stop-Environment { Write-Step 'Stopping' Stop-Tracked 'backend' Stop-Tracked 'frontend' docker stop $Container *>$null if ($LASTEXITCODE -eq 0) { Write-Good "stopped container $Container" } else { Write-Note "container $Container was not running" } Remove-Item $PidFile -ErrorAction SilentlyContinue Write-Host '' Write-Host 'Stopped.' -ForegroundColor Green } function Start-Database { if ($Fresh) { Write-Step 'Removing the existing database (-Fresh)' docker rm -f $Container *>$null Write-Good 'removed' } $existing = (docker ps -a --filter "name=^/$Container$" --format '{{.Names}}') if ($existing -eq $Container) { Write-Step "Reusing the database container" docker start $Container *>$null } else { Write-Step "Creating the database container on port $DbPort" docker run -d --name $Container ` -e "POSTGRES_USER=$DbUser" ` -e "POSTGRES_PASSWORD=$DbPassword" ` -e "POSTGRES_DB=$DbName" ` -p "${DbPort}:5432" ` postgres:16 *>$null if ($LASTEXITCODE -ne 0) { # The message Docker gives for a reserved port does not say # "reserved", so name the likely cause and the way out of it. throw @" Could not start Postgres on port $DbPort. If the port is in use, or reserved by Hyper-V (which silently claims ranges on Windows), pick another one: .\scripts\start-local.ps1 -DbPort 55600 Reserved ranges: netsh interface ipv4 show excludedportrange protocol=tcp "@ } } Wait-For -What 'Postgres' -Condition { docker exec $Container pg_isready -U $DbUser -d $DbName *>$null $LASTEXITCODE -eq 0 } } function Install-IfMissing { param([string]$Directory) $name = Split-Path -Leaf $Directory if (Test-Path (Join-Path $Directory 'node_modules')) { Write-Note "$name dependencies already installed" return } Write-Step "Installing $name dependencies" Push-Location $Directory try { Invoke-Checked { npm install } "$name npm install" } finally { Pop-Location } } function Start-Backend { $backend = Join-Path $RepoRoot 'backend' # The six the backend refuses to boot without, plus the two that make a # local run behave. Set in this session so the child process inherits them. $env:PGHOST = 'localhost' $env:PGPORT = "$DbPort" $env:PGUSER = $DbUser $env:PGPASSWORD = $DbPassword $env:PGDATABASE = $DbName $env:UPLOADS_DIR = (Join-Path $StateDir 'uploads') $env:PORT = "$ApiPort" # No PayPal credentials locally. DEMO_MODE lets the whole cart and checkout # path run without them and with no way to reach live PayPal. $env:DEMO_MODE = 'true' New-Item -ItemType Directory -Force -Path $env:UPLOADS_DIR *>$null Write-Step 'Running migrations' Push-Location $backend try { Invoke-Checked { node migrate.js up } 'Migrations' Write-Step 'Building the backend' Invoke-Checked { npm run build } 'Backend build' } finally { Pop-Location } if (Test-Listening -Port $ApiPort) { Write-Note "something is already listening on $ApiPort; leaving it alone" return } Write-Step "Starting the backend on $ApiPort" # Separate files: Start-Process cannot redirect both streams to one path. $process = Start-Process -FilePath 'node' -ArgumentList 'dist/server.js' ` -WorkingDirectory $backend ` -RedirectStandardOutput (Join-Path $StateDir 'backend.log') ` -RedirectStandardError (Join-Path $StateDir 'backend.err.log') ` -WindowStyle Hidden -PassThru Set-Tracked 'backend' $process.Id Wait-For -What 'Backend' -Condition { try { $null = Invoke-WebRequest "http://localhost:$ApiPort/api/config" -TimeoutSec 2 -UseBasicParsing $true } catch { $false } } } function Start-Frontend { if (Test-Listening -Port $WebPort) { Write-Note "something is already listening on $WebPort; leaving it alone" return } Write-Step "Starting the dev server on $WebPort" $process = Start-Process -FilePath 'npm.cmd' -ArgumentList 'run', 'dev' ` -WorkingDirectory (Join-Path $RepoRoot 'frontend') ` -RedirectStandardOutput (Join-Path $StateDir 'frontend.log') ` -RedirectStandardError (Join-Path $StateDir 'frontend.err.log') ` -WindowStyle Hidden -PassThru Set-Tracked 'frontend' $process.Id Wait-For -What 'Dev server' -Condition { Test-Listening -Port $WebPort } } New-Item -ItemType Directory -Force -Path $StateDir *>$null if ($Stop) { Stop-Environment return } Assert-NodeVersion Assert-Docker Start-Database Install-IfMissing (Join-Path $RepoRoot 'backend') Install-IfMissing (Join-Path $RepoRoot 'frontend') Start-Backend Start-Frontend Write-Host '' Write-Host 'Running.' -ForegroundColor Green Write-Host " Storefront http://localhost:$WebPort" Write-Host " Admin http://localhost:$WebPort/admin" Write-Host " API http://localhost:$ApiPort/api/config" Write-Host " Postgres localhost:$DbPort ($DbUser / $DbPassword / $DbName)" Write-Host '' Write-Host " Logs $StateDir" Write-Host " Stop .\scripts\start-local.ps1 -Stop" Write-Host ''