Spaces:
Running
Running
Eric Putnam
Claude Opus 5 (1M context)
feat: deploy DocEditorPlayground as a static Hugging Face Space
8a8c5bb Download scripts/build-static.ps1 from Hoctar77/DocEditorPlayground: direct link, hf CLI and curl.
- Browser
- Download file 9.6 kB
-
https://huggingface.co/spaces/Hoctar77/DocEditorPlayground/resolve/main/scripts/build-static.ps1
- Command line
-
hf download hf://spaces/Hoctar77/DocEditorPlayground/scripts/build-static.ps1
-
curl -L -o build-static.ps1 https://huggingface.co/spaces/Hoctar77/DocEditorPlayground/resolve/main/scripts/build-static.ps1
9.6 kB
| <# | |
| .SYNOPSIS | |
| Build the static site that the Hugging Face Static Space serves. | |
| .DESCRIPTION | |
| Pipeline: | |
| app/ (clean upstream mirror) | |
| + overlay/ (static-only additions) | |
| -> .build/ (scratch, gitignored) | |
| -> next build with output:"export" | |
| -> .build/out/ copied to the repository root | |
| 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, so stale hashed | |
| chunks never accumulate. | |
| .PARAMETER Clean | |
| Discard .build entirely first, including cached node_modules. Slower, but | |
| the way to rule out a stale-cache problem. | |
| .PARAMETER SkipInstall | |
| Skip the dependency check. Only valid when .build/node_modules is current. | |
| .EXAMPLE | |
| .\scripts\build-static.ps1 | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [switch] $Clean, | |
| [switch] $SkipInstall | |
| ) | |
| $ErrorActionPreference = 'Stop' | |
| $repoRoot = Split-Path -Parent $PSScriptRoot | |
| $appDir = Join-Path $repoRoot 'app' | |
| $overlayDir = Join-Path $repoRoot 'overlay' | |
| $buildDir = Join-Path $repoRoot '.build' | |
| $manifestPath = Join-Path $repoRoot '.space-build-manifest' | |
| # 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', | |
| 'README.md', 'DEPLOY.md', 'CLAUDE.md', 'Dockerfile', 'UPSTREAM.json', | |
| 'dep-repo.md', 'fsv-example.md' | |
| ) | |
| if (-not (Test-Path -LiteralPath (Join-Path $appDir 'package.json'))) { | |
| throw 'app/ is empty or missing. Run .\scripts\sync-upstream.ps1 first.' | |
| } | |
| # --- Guards: the overlay must stay additive ------------------------------- | |
| # _document.tsx is the overlay's injection point precisely because upstream has | |
| # none. If upstream adds one, silently shadowing it would drop whatever it does. | |
| if (Test-Path -LiteralPath (Join-Path $appDir 'pages/_document.tsx')) { | |
| throw @' | |
| Upstream now ships pages/_document.tsx, which overlay/pages/_document.tsx would | |
| silently replace. | |
| Merge upstream's version into overlay/pages/_document.tsx - keep the | |
| <script src="/ai-suggest-shim.js" /> tag - then re-run. | |
| '@ | |
| } | |
| if (Test-Path -LiteralPath (Join-Path $appDir 'next.config.base.mjs')) { | |
| throw @' | |
| Upstream now ships next.config.base.mjs, which collides with the name this build | |
| renames upstream's next.config.js to. | |
| Rename the intermediate in scripts/build-static.ps1 and overlay/next.config.js. | |
| '@ | |
| } | |
| if (-not (Test-Path -LiteralPath (Join-Path $appDir 'next.config.js'))) { | |
| throw 'app/next.config.js not found - overlay/next.config.js expects to import it as next.config.base.mjs.' | |
| } | |
| # --- Stage app/ + overlay/ into .build/ ----------------------------------- | |
| if ($Clean -and (Test-Path -LiteralPath $buildDir)) { | |
| Write-Host 'Removing .build (clean build)...' -ForegroundColor Yellow | |
| Remove-Item -LiteralPath $buildDir -Recurse -Force | |
| } | |
| New-Item -ItemType Directory -Force -Path $buildDir | Out-Null | |
| Write-Host 'Staging app/ -> .build/' -ForegroundColor Cyan | |
| # /MIR so files deleted upstream disappear here too, but node_modules, the Next | |
| # cache, and the dependency stamp are excluded from both sides so they survive. | |
| $robocopyArgs = @( | |
| $appDir, $buildDir, | |
| '/MIR', '/NFL', '/NDL', '/NJH', '/NP', '/R:2', '/W:1', | |
| '/XD', 'node_modules', '.next', 'out', | |
| '/XF', '.deps-stamp', '.gitconfig-npm' | |
| ) | |
| & robocopy @robocopyArgs | Out-Null | |
| if ($LASTEXITCODE -ge 8) { throw "robocopy (app -> .build) failed with exit code $LASTEXITCODE" } | |
| # API routes cannot exist in an output:"export" build; next build errors on them. | |
| $apiDir = Join-Path $buildDir 'pages/api' | |
| if (Test-Path -LiteralPath $apiDir) { | |
| Remove-Item -LiteralPath $apiDir -Recurse -Force | |
| Write-Host ' removed pages/api (unsupported by static export; see overlay/public/ai-suggest-shim.js)' | |
| } | |
| # Upstream config becomes the base that overlay/next.config.js imports. | |
| Move-Item -LiteralPath (Join-Path $buildDir 'next.config.js') ` | |
| -Destination (Join-Path $buildDir 'next.config.base.mjs') -Force | |
| Write-Host 'Applying overlay/ -> .build/' -ForegroundColor Cyan | |
| & robocopy $overlayDir $buildDir '/E' '/NFL' '/NDL' '/NJH' '/NP' '/R:2' '/W:1' | Out-Null | |
| if ($LASTEXITCODE -ge 8) { throw "robocopy (overlay -> .build) failed with exit code $LASTEXITCODE" } | |
| # --- Install dependencies (only when the lockfile changed) ---------------- | |
| $stampPath = Join-Path $buildDir '.deps-stamp' | |
| $lockHash = (Get-FileHash -LiteralPath (Join-Path $buildDir 'package-lock.json') -Algorithm SHA256).Hash | |
| $haveModules = Test-Path -LiteralPath (Join-Path $buildDir 'node_modules') | |
| $stamp = if (Test-Path -LiteralPath $stampPath) { (Get-Content -LiteralPath $stampPath -Raw).Trim() } else { '' } | |
| if ($SkipInstall) { | |
| if (-not $haveModules) { throw '-SkipInstall was passed but .build/node_modules does not exist.' } | |
| Write-Host 'Skipping dependency install (-SkipInstall).' -ForegroundColor Yellow | |
| } | |
| elseif (-not $haveModules -or $stamp -ne $lockHash) { | |
| Write-Host 'Installing dependencies (npm ci)...' -ForegroundColor Cyan | |
| # Two dependencies (mark-review, y-webrtc) are pinned in package-lock.json as | |
| # git+ssh:// GitHub URLs, which would need an SSH key. Rewrite them to | |
| # anonymous https:// for this npm run only, through a throwaway git config - | |
| # the same trick the Dockerfile uses, without touching the machine's real | |
| # git configuration. | |
| $gitConfigPath = Join-Path $buildDir '.gitconfig-npm' | |
| $gitConfigLines = @( | |
| '[url "https://github.com/"]', | |
| "`tinsteadOf = ssh://git@github.com/", | |
| "`tinsteadOf = git+ssh://git@github.com/", | |
| "`tinsteadOf = git@github.com:" | |
| ) | |
| Set-Content -LiteralPath $gitConfigPath -Value $gitConfigLines -Encoding ascii | |
| Push-Location $buildDir | |
| try { | |
| $previousGitConfig = $env:GIT_CONFIG_GLOBAL | |
| $env:GIT_CONFIG_GLOBAL = $gitConfigPath | |
| try { | |
| & npm ci --include=dev --no-audit --no-fund | |
| if ($LASTEXITCODE -ne 0) { throw 'npm ci failed' } | |
| } | |
| finally { $env:GIT_CONFIG_GLOBAL = $previousGitConfig } | |
| } | |
| finally { Pop-Location } | |
| Set-Content -LiteralPath $stampPath -Value $lockHash -Encoding ascii | |
| } | |
| else { | |
| Write-Host 'Dependencies already current (lockfile unchanged).' -ForegroundColor Green | |
| } | |
| # --- Build ---------------------------------------------------------------- | |
| Write-Host 'Running next build (output: export)...' -ForegroundColor Cyan | |
| Push-Location $buildDir | |
| try { | |
| $previousNodeEnv = $env:NODE_ENV | |
| $previousTelemetry = $env:NEXT_TELEMETRY_DISABLED | |
| $env:NODE_ENV = 'production' | |
| $env:NEXT_TELEMETRY_DISABLED = '1' | |
| try { | |
| & npm run build | |
| if ($LASTEXITCODE -ne 0) { throw 'next build failed' } | |
| } | |
| finally { | |
| $env:NODE_ENV = $previousNodeEnv | |
| $env:NEXT_TELEMETRY_DISABLED = $previousTelemetry | |
| } | |
| } | |
| finally { Pop-Location } | |
| $outDir = Join-Path $buildDir 'out' | |
| if (-not (Test-Path -LiteralPath (Join-Path $outDir 'index.html'))) { | |
| throw "Build finished but $outDir\index.html is missing - static export produced no site." | |
| } | |
| # --- 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 | |
| $sizeMb = [math]::Round((($outFiles | Measure-Object -Property Length -Sum).Sum / 1MB), 1) | |
| Write-Host '' | |
| Write-Host "Static site built: $($outFiles.Count) files, $sizeMb MB" -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' | |