# BuildAssets.ps1 # Generates assets.rc + assets.res containing the static web assets # (index.html, js/*, css/*) so they can be linked into PMServer.exe as # Win32 RCDATA resources. # # Workflow: # 1. Edit Z:\password-manager\index.html / js\app.js / css\style.css # 2. Run this script (or set it as pre-build event in Delphi) # 3. Build PMServer.dpr in Delphi # 4. Run - exe is autonomous, no external files needed at runtime # # Resource naming: URL path '/js/app.js' -> resource 'JS_APP_JS' # - strip leading '/' # - replace '/' '\' '.' '-' with '_' # - uppercase [CmdletBinding()] param( [string] $WebRoot = '', [string] $OutRC = '', [string] $OutRES = '', [string] $OutInc = '' ) $ErrorActionPreference = 'Stop' # Resolve $PSScriptRoot — may be empty depending on invocation context. # Fallback to the script's own file path. $ScriptDir = $PSScriptRoot if (-not $ScriptDir) { $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition } if (-not $ScriptDir) { $ScriptDir = (Get-Location).Path } # Apply param defaults now that we have a valid script dir if (-not $WebRoot) { $WebRoot = Join-Path $ScriptDir '..\..' } if (-not $OutRC) { $OutRC = Join-Path $ScriptDir 'assets.rc' } if (-not $OutRES) { $OutRES = Join-Path $ScriptDir 'assets.res' } if (-not $OutInc) { $OutInc = Join-Path $ScriptDir 'assets.inc' } # All output goes to stdout/stderr. When invoked via BuildAssets.cmd the # wrapper captures both streams into build.log; when run interactively # everything shows in the terminal. function Log { param([string] $msg) Write-Host $msg } try { $WebRoot = (Resolve-Path $WebRoot).Path Log "Web root: $WebRoot" # Whitelist patterns (relative to WebRoot). Add more here when needed. # Use exact paths (not wildcards) to avoid embedding *-legacy.* backups. $patterns = @( 'index.html', 'js\argon2.js', 'js\app.crypto.js', 'js\app.js', 'js\app.sync.js', 'css\style.css' ) # Discover files $files = @() foreach ($p in $patterns) { $found = Get-ChildItem -Path (Join-Path $WebRoot $p) -File -ErrorAction SilentlyContinue foreach ($f in $found) { $rel = $f.FullName.Substring($WebRoot.Length).TrimStart('\','/') $files += [pscustomobject]@{ FullPath = $f.FullName Relative = $rel UrlPath = '/' + ($rel -replace '\\','/') ResName = ($rel -replace '[\\/\.\-]','_').ToUpperInvariant() } } } if ($files.Count -eq 0) { throw "No assets found under $WebRoot. Check the patterns." } Log "Embedding $($files.Count) file(s):" $files | ForEach-Object { Log (" " + $_.UrlPath + " -> " + $_.ResName) } # --- JS syntax gate ----------------------------------------------------------- # A syntax error in app.js parses fine here but kills the whole frontend at # runtime (no event handlers → dead UI), and it's only caught after a full # Delphi rebuild. Run `node --check` on every embedded .js so a broken bundle # never makes it into assets.res. Node is optional: if it isn't installed we # warn and continue rather than blocking the build on a machine without it. $node = Get-Command node.exe -ErrorAction SilentlyContinue if (-not $node) { $node = Get-Command node -ErrorAction SilentlyContinue } $jsFiles = $files | Where-Object { $_.Relative -match '\.js$' } if ($jsFiles) { if ($node) { foreach ($jf in $jsFiles) { Log "Syntax check: $($jf.Relative)" # --check prints errors to stderr and returns non-zero on failure. # Pipe $null into node so it can NEVER block waiting on stdin # (some Windows node shims read stdin when launched from a # non-interactive pre-build event, which would hang the build). $out = $null | & $node.Source --check $jf.FullPath 2>&1 if ($LASTEXITCODE -ne 0) { Log "JS SYNTAX ERROR in $($jf.Relative):" Log ($out | Out-String) throw "JS syntax check failed for $($jf.Relative) - aborting asset build." } } Log "JS syntax OK." # --- Unit test gate --------------------------------------------------- # Run the frontend regression suite (crypto round-trip, CSV import, # sync-merge arbitration) before embedding. A broken crypto/merge # invariant now blocks the build the same way a syntax error does, # instead of surfacing only after a full Delphi rebuild + manual test. # ~1.7 s, zero deps (node:test). Skipped automatically if the suite # isn't present. Set PM_SKIP_TESTS=1 to bypass during rapid iteration. if ($env:PM_SKIP_TESTS -eq '1') { Log "PM_SKIP_TESTS=1 - skipping unit tests." } elseif (Test-Path (Join-Path $WebRoot 'js\tests')) { Log "Running frontend unit tests..." Push-Location $WebRoot try { $testOut = $null | & $node.Source --test 'js/tests/**/*.test.js' 2>&1 $testExit = $LASTEXITCODE } finally { Pop-Location } if ($testExit -ne 0) { Log "UNIT TESTS FAILED:" Log ($testOut | Out-String) throw "Frontend unit tests failed - aborting asset build. (Set PM_SKIP_TESTS=1 to bypass.)" } Log "Unit tests OK." } } else { Log "WARNING: node not found - skipping JS syntax check + unit tests. Install Node to enable them." } } # --- Generate assets.rc ------------------------------------------------------- $rc = New-Object System.Text.StringBuilder [void]$rc.AppendLine('// Auto-generated by BuildAssets.ps1 - do not edit by hand.') [void]$rc.AppendLine('#pragma code_page(65001)') [void]$rc.AppendLine('') foreach ($f in $files) { # brcc32 accepts forward slashes; backslashes need to be doubled in C strings $rcPath = $f.FullPath -replace '\\','\\' [void]$rc.AppendLine("$($f.ResName) RCDATA `"$rcPath`"") } [System.IO.File]::WriteAllText($OutRC, $rc.ToString(), [System.Text.Encoding]::ASCII) Log "Wrote $OutRC" # --- Generate assets.inc (Pascal include with manifest) ----------------------- # Compile-time mapping URL -> resource name, consumed by PM.EmbeddedAssets.pas. $nl = [Environment]::NewLine $inc = New-Object System.Text.StringBuilder [void]$inc.Append('// Auto-generated by BuildAssets.ps1 - do not edit by hand.' + $nl) [void]$inc.Append('const' + $nl) [void]$inc.Append(' EMBEDDED_ASSET_COUNT = ' + $files.Count + ';' + $nl) [void]$inc.Append(' EMBEDDED_ASSETS: array[0..EMBEDDED_ASSET_COUNT-1] of TEmbeddedAsset = (' + $nl) for ($i = 0; $i -lt $files.Count; $i++) { $f = $files[$i] if ($i -eq $files.Count - 1) { $sep = '' } else { $sep = ',' } $line = " (UrlPath: '" + $f.UrlPath + "'; ResName: '" + $f.ResName + "')" + $sep + $nl [void]$inc.Append($line) } [void]$inc.Append(' );' + $nl) [System.IO.File]::WriteAllText($OutInc, $inc.ToString(), [System.Text.Encoding]::UTF8) Log "Wrote $OutInc" # --- Compile to .res via brcc32 ---------------------------------------------- $brcc = $null if ($env:BDS) { $candidate = Join-Path $env:BDS 'bin\brcc32.exe' if (Test-Path $candidate) { $brcc = $candidate } } if (-not $brcc) { $cmd = Get-Command brcc32.exe -ErrorAction SilentlyContinue if ($cmd) { $brcc = $cmd.Source } } if (-not $brcc) { throw "brcc32.exe not found. Set the BDS environment variable to your Delphi install root, or add brcc32.exe to PATH." } Log "Using $brcc" & $brcc -32 -fo "$OutRES" "$OutRC" if ($LASTEXITCODE -ne 0) { throw "brcc32 failed with exit code $LASTEXITCODE" } Log "Wrote $OutRES" Log "OK" } catch { Log "FATAL: $($_.Exception.Message)" Log "Stack: $($_.ScriptStackTrace)" exit 1 }