Spaces:
Running
Running
Eric Putnam
Claude Opus 5 (1M context)
feat: deploy simpledocreview as a browser-only static space
3d968d9 Download scripts/build-static.ps1 from Hoctar77/SimpleDocReview: direct link, hf CLI and curl.
- Browser
- Download file 10 kB
-
https://huggingface.co/spaces/Hoctar77/SimpleDocReview/resolve/main/scripts/build-static.ps1
- Command line
-
hf download hf://spaces/Hoctar77/SimpleDocReview/scripts/build-static.ps1
-
curl -L -o build-static.ps1 https://huggingface.co/spaces/Hoctar77/SimpleDocReview/resolve/main/scripts/build-static.ps1
10 kB
| <# | |
| .SYNOPSIS | |
| Build the static site that the Hugging Face Static Space serves. | |
| .DESCRIPTION | |
| Pipeline: | |
| app/ (clean upstream mirror) | |
| + overlay/ (the browser-side API and its wiring) | |
| -> .build/out/ | |
| -> copied to the repository root | |
| There is no compiler and no package manager here: upstream's front end is | |
| plain HTML, CSS, and JavaScript. The build is a copy, one HTML edit that | |
| inserts the overlay's <script> tags ahead of upstream's app.js, and the | |
| bookkeeping that keeps the repository root free of stale output. | |
| Hugging Face Static Spaces serve the repository root, so the generated files | |
| have to sit there next to README.md. Everything the build emits is recorded | |
| in .space-build-manifest and removed before the next copy. | |
| .PARAMETER AcceptUpstreamChanges | |
| Re-record the upstream hashes in overlay/ported-from.json. Use this only | |
| after reading the upstream diff and carrying any behavior change into | |
| overlay/sdr-api-core.js - it is an acknowledgement, not a fix. | |
| .EXAMPLE | |
| .\scripts\build-static.ps1 | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [switch] $AcceptUpstreamChanges | |
| ) | |
| $ErrorActionPreference = 'Stop' | |
| $repoRoot = Split-Path -Parent $PSScriptRoot | |
| $appDir = Join-Path $repoRoot 'app' | |
| $overlayDir = Join-Path $repoRoot 'overlay' | |
| $buildDir = Join-Path $repoRoot '.build' | |
| $outDir = Join-Path $buildDir 'out' | |
| $manifestPath = Join-Path $repoRoot '.space-build-manifest' | |
| $portedFromPath = Join-Path $overlayDir 'ported-from.json' | |
| # Repository files that are never build output. The cleanup step refuses to | |
| # touch these even if the manifest somehow names one. | |
| $protected = @( | |
| '.git', '.github', '.claude', '.build', | |
| '.gitignore', '.gitattributes', '.dockerignore', '.space-build-manifest', | |
| 'app', 'overlay', 'scripts', 'tests', | |
| 'README.md', 'DEPLOY.md', 'CLAUDE.md', 'Dockerfile', 'UPSTREAM.json', | |
| 'references.md' | |
| ) | |
| # The overlay files copied to the site root, in the order the page loads them. | |
| $overlayScripts = @('sdr-api-core.js', 'sdr-api-shim.js') | |
| if (-not (Test-Path -LiteralPath (Join-Path $appDir 'app.py'))) { | |
| throw 'app/ is empty or missing. Run .\scripts\sync-upstream.ps1 first.' | |
| } | |
| function Get-Sha256([string] $path) { | |
| return (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() | |
| } | |
| # --- Guard: the port must still match what it was ported from ------------- | |
| # overlay/sdr-api-core.js reimplements app.py in JavaScript. Nothing about a | |
| # copy-and-inject build would fail if upstream changed a rubric rule or a status | |
| # transition, so the only protection is refusing to ship until someone has | |
| # looked at the diff. | |
| $ported = Get-Content -LiteralPath $portedFromPath -Raw | ConvertFrom-Json | |
| $actual = [ordered]@{} | |
| foreach ($name in $ported.sources.PSObject.Properties.Name) { | |
| $sourcePath = Join-Path $appDir $name | |
| if (-not (Test-Path -LiteralPath $sourcePath -PathType Leaf)) { | |
| throw "overlay/ported-from.json names $name, which is not in app/. Re-run sync-upstream.ps1." | |
| } | |
| $actual[$name] = Get-Sha256 $sourcePath | |
| } | |
| $drifted = $actual.Keys | Where-Object { $actual[$_] -ne $ported.sources.$_ } | |
| if ($AcceptUpstreamChanges) { | |
| if ($drifted.Count -eq 0) { | |
| Write-Host 'Upstream hashes already current; nothing to accept.' -ForegroundColor Yellow | |
| } | |
| else { | |
| $upstreamJsonPath = Join-Path $repoRoot 'UPSTREAM.json' | |
| $commit = if (Test-Path -LiteralPath $upstreamJsonPath) { | |
| (Get-Content -LiteralPath $upstreamJsonPath -Raw | ConvertFrom-Json).commit | |
| } else { 'unknown' } | |
| $ported.portedAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd') | |
| $ported.upstreamCommit = $commit | |
| foreach ($name in $drifted) { $ported.sources.$name = $actual[$name] } | |
| $json = ($ported | ConvertTo-Json -Depth 5) + [Environment]::NewLine | |
| [System.IO.File]::WriteAllText($portedFromPath, $json, (New-Object System.Text.UTF8Encoding $false)) | |
| Write-Host "Recorded new upstream hashes for: $($drifted -join ', ')" -ForegroundColor Green | |
| } | |
| } | |
| elseif ($drifted.Count -gt 0) { | |
| throw @" | |
| Upstream changed in files the overlay reimplements: $($drifted -join ', ') | |
| overlay/sdr-api-core.js is a hand-written port of app.py's API, so an upstream | |
| change to a route, a rubric rule, a status transition, or the front end's | |
| request shapes will not reach the Space on its own. | |
| 1. Read the diff: | |
| git -C C:\dev\SimpleDocReview diff $($ported.upstreamCommit) -- $($drifted -join ' ') | |
| 2. Carry any behavior change into overlay/sdr-api-core.js. | |
| 3. Re-check the port: node tests\parity\run-parity.mjs | |
| 4. Acknowledge it: .\scripts\build-static.ps1 -AcceptUpstreamChanges | |
| "@ | |
| } | |
| # --- Guard: the overlay must stay additive ------------------------------- | |
| # Upstream's files are copied verbatim. If the overlay ever contained a file | |
| # that lands on the same path, the copy order would decide which one shipped - | |
| # and a silently shadowed upstream file is exactly the drift app/ exists to | |
| # prevent. | |
| $upstreamTargets = @{ 'index.html' = $true } | |
| Get-ChildItem -LiteralPath (Join-Path $appDir 'static') -File | | |
| ForEach-Object { $upstreamTargets["static/$($_.Name)"] = $true } | |
| foreach ($script in $overlayScripts) { | |
| if ($upstreamTargets.ContainsKey($script)) { | |
| throw "overlay/$script would overwrite an upstream file of the same name. Rename it." | |
| } | |
| if (-not (Test-Path -LiteralPath (Join-Path $overlayDir $script) -PathType Leaf)) { | |
| throw "overlay/$script is missing." | |
| } | |
| } | |
| # --- Stage --------------------------------------------------------------- | |
| if (Test-Path -LiteralPath $buildDir) { | |
| Remove-Item -LiteralPath $buildDir -Recurse -Force | |
| } | |
| New-Item -ItemType Directory -Force -Path (Join-Path $outDir 'static') | Out-Null | |
| Write-Host 'Staging app/static -> .build/out' -ForegroundColor Cyan | |
| # index.html moves to the site root: Hugging Face serves the repository root as | |
| # the web root, and README.md's front matter names index.html as the app file. | |
| Copy-Item -LiteralPath (Join-Path $appDir 'static/index.html') -Destination (Join-Path $outDir 'index.html') | |
| # Everything else keeps its /static/ path, which is what index.html asks for. | |
| Get-ChildItem -LiteralPath (Join-Path $appDir 'static') -File | | |
| Where-Object { $_.Name -ne 'index.html' } | | |
| ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $outDir 'static') } | |
| Write-Host 'Applying overlay/ -> .build/out' -ForegroundColor Cyan | |
| foreach ($script in $overlayScripts) { | |
| Copy-Item -LiteralPath (Join-Path $overlayDir $script) -Destination $outDir | |
| } | |
| # --- Insert the overlay's script tags ------------------------------------ | |
| # The shim has to be installed before upstream's app.js runs, because app.js | |
| # fires its first three requests at import time. Inserting the tags is a | |
| # transform of upstream's HTML rather than a copy of it, so there is no second | |
| # index.html in this repo to drift from upstream's. | |
| $indexPath = Join-Path $outDir 'index.html' | |
| $html = [System.IO.File]::ReadAllText($indexPath) | |
| $anchor = '<script src="/static/app.js"></script>' | |
| if ($html -notlike "*$anchor*") { | |
| throw @" | |
| Could not find upstream's app.js script tag in static/index.html. | |
| Expected: $anchor | |
| The overlay has to load before app.js does. Update the anchor in | |
| scripts/build-static.ps1 to match upstream's markup. | |
| "@ | |
| } | |
| $injected = (($overlayScripts | ForEach-Object { "<script src=`"/$_`"></script>" }) -join "`n ") | |
| $html = $html.Replace($anchor, "$injected`n $anchor") | |
| [System.IO.File]::WriteAllText($indexPath, $html, (New-Object System.Text.UTF8Encoding $false)) | |
| Write-Host " inserted $($overlayScripts.Count) overlay script tag(s) before app.js" | |
| # --- Remove the previous build's output from the repository root ---------- | |
| if (Test-Path -LiteralPath $manifestPath) { | |
| $stale = Get-Content -LiteralPath $manifestPath | | |
| Where-Object { $_.Trim() -and -not $_.StartsWith('#') } | |
| foreach ($entry in $stale) { | |
| $name = $entry.Trim() | |
| if ($protected -contains $name) { | |
| Write-Warning "Manifest names a protected path ('$name'); refusing to delete it." | |
| continue | |
| } | |
| # Reject anything that is not a plain top-level name. | |
| if ($name -match '[\\/]' -or $name -eq '.' -or $name -eq '..') { | |
| Write-Warning "Manifest entry '$name' is not a top-level name; skipping." | |
| continue | |
| } | |
| $target = Join-Path $repoRoot $name | |
| if (Test-Path -LiteralPath $target) { | |
| Remove-Item -LiteralPath $target -Recurse -Force | |
| } | |
| } | |
| } | |
| # --- Copy the new output to the repository root --------------------------- | |
| Write-Host 'Copying build output to the repository root' -ForegroundColor Cyan | |
| $generated = @() | |
| foreach ($item in Get-ChildItem -LiteralPath $outDir -Force) { | |
| if ($protected -contains $item.Name) { | |
| throw "Build output contains '$($item.Name)', which collides with a repository file. Resolve before deploying." | |
| } | |
| Copy-Item -LiteralPath $item.FullName -Destination $repoRoot -Recurse -Force | |
| $generated += $item.Name | |
| } | |
| $manifestLines = @( | |
| '# Generated by scripts/build-static.ps1 - do not edit.', | |
| '# Top-level repository paths that are build output, deleted and rewritten', | |
| '# on each build.' | |
| ) + ($generated | Sort-Object) | |
| Set-Content -LiteralPath $manifestPath -Value $manifestLines -Encoding ascii | |
| # --- Summary -------------------------------------------------------------- | |
| $outFiles = Get-ChildItem -LiteralPath $outDir -Recurse -File | |
| $sizeKb = [math]::Round((($outFiles | Measure-Object -Property Length -Sum).Sum / 1KB), 1) | |
| Write-Host '' | |
| Write-Host "Static site built: $($outFiles.Count) files, $sizeKb KB" -ForegroundColor Green | |
| Write-Host "Generated at repo root: $($generated -join ', ')" | |
| Write-Host '' | |
| Write-Host 'Preview it locally with:' -ForegroundColor Cyan | |
| Write-Host " python -m http.server 8000 --directory `"$repoRoot`"" | |
| Write-Host ' then open http://localhost:8000' | |