Files
homepage/scripts/verify-production.ps1
2026-08-18 09:57:55 +08:00

152 lines
9.4 KiB
PowerShell

param(
[string]$Root = (Join-Path $PSScriptRoot "..\site"),
[string]$PublicRoot = "",
[string]$PublicArchive = ""
)
$ErrorActionPreference = "Stop"
$Root = (Resolve-Path -LiteralPath $Root).Path
function Assert-Check {
param([bool]$Condition, [string]$Message)
if (-not $Condition) { throw "FAIL: $Message" }
Write-Host "PASS: $Message"
}
$requiredFiles = @(
"index.html",
"404.html",
"robots.txt",
"sitemap.xml",
"site.webmanifest",
"assets/css/main.css",
"assets/js/main.js",
"assets/js/site-config.json",
"assets/fonts/NotoSerifSC-subset.woff2",
"assets/fonts/OFL.txt",
"assets/images/avatar.jpg",
"assets/images/avatar-frame-animated.png",
"assets/images/avatar-frame-mask.svg",
"assets/images/avatar-frame-static.png",
"assets/images/favicon.svg"
)
foreach ($relative in $requiredFiles) {
Assert-Check (Test-Path -LiteralPath (Join-Path $Root $relative) -PathType Leaf) "required file exists: $relative"
}
$index = Get-Content -Raw -Encoding UTF8 (Join-Path $Root "index.html")
$notFound = Get-Content -Raw -Encoding UTF8 (Join-Path $Root "404.html")
$manifestPath = Join-Path $Root "site.webmanifest"
$manifest = Get-Content -Raw -Encoding UTF8 $manifestPath | ConvertFrom-Json
$siteConfig = Get-Content -Raw -Encoding UTF8 (Join-Path $Root "assets/js/site-config.json") | ConvertFrom-Json
[Uri]$siteUri = $siteConfig.site.url
[string]$siteRoot = $siteUri.AbsoluteUri.TrimEnd('/') + '/'
$robots = Get-Content -Raw -Encoding UTF8 (Join-Path $Root "robots.txt")
[xml]$sitemap = Get-Content -Raw -Encoding UTF8 (Join-Path $Root "sitemap.xml")
[xml]$favicon = Get-Content -Raw -Encoding UTF8 (Join-Path $Root "assets/images/favicon.svg")
$nginxPath = Join-Path $PSScriptRoot "..\nginx\midctrl.com.example.conf"
Assert-Check (Test-Path -LiteralPath $nginxPath -PathType Leaf) "sanitized Nginx example exists"
$nginx = Get-Content -Raw -Encoding UTF8 $nginxPath
$css = Get-Content -Raw -Encoding UTF8 (Join-Path $Root "assets/css/main.css")
$jsPath = Join-Path $Root "assets/js/main.js"
$js = Get-Content -Raw -Encoding UTF8 $jsPath
$indexScriptVersion = [regex]::Match($index, 'main\.js\?v=([^"'']+)').Groups[1].Value
$notFoundScriptVersion = [regex]::Match($notFound, 'main\.js\?v=([^"'']+)').Groups[1].Value
$fontVersion = [regex]::Match($css, 'NotoSerifSC-subset\.woff2\?v=([^"'']+)').Groups[1].Value
Assert-Check ($notFound -match 'href="/assets/images/avatar\.jpg"' -and $notFound -match 'href="/assets/css/main\.css(?:\?[^" ]*)?"' -and $notFound -match 'src="/assets/js/main\.js(?:\?[^" ]*)?"' -and $notFound -match 'src="/assets/images/avatar\.jpg"') "404 assets use root-absolute URLs"
Assert-Check ($notFound -match 'class="portrait-image"' -and $notFound -match 'avatar-frame-animated\.png' -and $notFound -notmatch 'axis-line') "404 page uses the current avatar structure"
Assert-Check ($manifest.icons[0].src -eq "/assets/images/avatar.jpg") "manifest icon uses root-absolute URL"
Assert-Check ($manifest.icons[0].sizes -eq "512x512") "manifest icon size matches the avatar"
Assert-Check ([string]$manifest.start_url -eq "/") "manifest starts at the site root"
Assert-Check ($siteConfig.site.name -and $siteConfig.site.description -and $siteUri.IsAbsoluteUri -and $siteUri.Scheme -eq "https") "site identity and HTTPS URL are centralized in site-config.json"
Assert-Check ($siteConfig.links.Count -gt 0 -and @($siteConfig.links | Where-Object { -not $_.name -or $_.url -notmatch '^https://' }).Count -eq 0) "navigation configuration contains named HTTPS links"
Assert-Check ([string]$sitemap.urlset.url.loc -eq $siteRoot -and $robots -match [regex]::Escape("Sitemap: ${siteRoot}sitemap.xml")) "robots and sitemap use the configured canonical homepage"
Assert-Check ($index -match 'rel="canonical" href="/"' -and $index -match 'property="og:url" content="/"' -and $js -match 'applySiteMetadata') "HTML keeps portable metadata fallbacks and applies the configured site URL"
Assert-Check ([string]$favicon.svg.viewBox -eq "0 0 64 64") "favicon is valid SVG XML"
Add-Type -AssemblyName System.Drawing
$avatar = [System.Drawing.Image]::FromFile((Join-Path $Root "assets/images/avatar.jpg"))
try {
Assert-Check ($avatar.Width -eq 512 -and $avatar.Height -eq 512) "avatar is 512x512"
} finally {
$avatar.Dispose()
}
$node = Get-Command node -ErrorAction SilentlyContinue
Assert-Check ($null -ne $node) "Node.js is available for JavaScript syntax validation"
& $node.Source --check $jsPath
Assert-Check ($LASTEXITCODE -eq 0) "main.js passes node --check"
$renderTargetTest = Join-Path $PSScriptRoot "..\tests\test-render-target-resize.js"
& $node.Source $renderTargetTest $jsPath
Assert-Check ($LASTEXITCODE -eq 0) "Render Target resize and recovery tests pass"
Assert-Check ($js -match 'pointFlower\.numFlowers\s*=\s*1600') "particle count remains 1600"
Assert-Check (([regex]::Matches($js, 'gl\.drawArrays\(gl\.POINTS')).Count -eq 5) "flower draw call count remains 5"
Assert-Check ($js -match 'for\(var i = 0; i < 2; i\+\+\)' -and ([regex]::Matches($js, 'drawEffect\(effectLib\.dirBlur\)').Count -eq 2)) "Bloom retains four directional blur draws"
Assert-Check ($js -match 'maximumFrameRate\s*=\s*120') "animation cap remains 120 FPS"
Assert-Check ($js -match 'requestAnimationFrame' -and $js -match 'matchMedia\(' -and $js -match 'prefers-reduced-motion') "refresh scheduling and reduced-motion handling are present"
Assert-Check ($js -match 'DOMContentLoaded' -and $js -match 'initializeSakura' -and $js -notmatch "window\.addEventListener\('load'") "WebGL starts when the DOM is ready without waiting for fonts or images"
Assert-Check ($js -match "addEventListener\('pagehide'" -and $js -match "addEventListener\('pageshow'" -and $js -match 'synchronizeAnimationState') "page lifecycle recovery re-synchronizes the animation loop"
Assert-Check ($js -match 'request\.timeout\s*=\s*3000') "navigation configuration has a bounded network timeout"
Assert-Check ($indexScriptVersion -and $indexScriptVersion -eq $notFoundScriptVersion -and $indexScriptVersion -eq $fontVersion) "homepage, 404 page, and font use one cache version"
Assert-Check ($js -match 'createRenderTargetSet' -and $js -match 'deleteRenderTargetSet') "render target sets are swapped atomically"
Assert-Check ($js -match 'normalRenderPixelBudget\s*=\s*10000000' -and $js -match 'constrainedRenderPixelBudget\s*=\s*6000000') "render target pixel budgets are configured"
Assert-Check ($js -match 'resizeDebounceMs\s*=\s*140' -and $js -match 'measureCanvasSize' -and $js -match 'commitCanvasSize') "resize measurement and delayed commit are present"
Assert-Check ($js -match 'shouldReleaseBeforeAllocate' -and $js -match 'gl\.finish\(\)' -and $js -match 'getResizeCandidates') "high-memory resize and allocation fallback paths are present"
Assert-Check ($js -notmatch 'gl\.enable\(gl\.TEXTURE_2D\)' -and $js -notmatch 'gl\.disable\(gl\.TEXTURE_2D\)' -and $js -notmatch 'gl\.POINT\b') "invalid WebGL capability enums are absent"
Assert-Check ($nginx -match 'root /var/www/midctrl\.com;' -and $nginx -match 'try_files \$uri \$uri/ =404') "Nginx serves only the dedicated static web root"
Assert-Check ($nginx -match 'location ~\* \^/assets/\(\?:css\|js\)/' -and $nginx -match 'expires -1') "unversioned CSS and JavaScript revalidate"
Assert-Check ($nginx -match 'location \^~ /assets/fonts/' -and $nginx -match 'Cache-Control "public, immutable"') "self-hosted fonts use long-lived immutable caching"
Assert-Check ($nginx -notmatch 'location\s+/[a-f0-9]{16,}' -and $nginx -notmatch 'proxy_pass\s+http://127\.0\.0\.1:\d+') "public Nginx example omits host-private proxy details"
Assert-Check ($js -match 'frameRequestGeneration' -and $js -match 'cancelScheduledAnimationFrame') "animation loop guards stale callbacks after context loss"
Assert-Check ((Get-Item -LiteralPath (Join-Path $Root "assets/fonts/NotoSerifSC-subset.woff2")).Length -gt 1000 -and (Get-Content -Raw -Encoding UTF8 (Join-Path $Root "assets/fonts/OFL.txt")) -match 'SIL Open Font License') "Noto Serif SC subset and license are present"
$allowedPublic = @(
"index.html",
"404.html",
"robots.txt",
"sitemap.xml",
"site.webmanifest",
"assets/css/main.css",
"assets/js/main.js",
"assets/js/site-config.json",
"assets/fonts/NotoSerifSC-subset.woff2",
"assets/fonts/OFL.txt",
"assets/images/avatar.jpg",
"assets/images/avatar-frame-animated.png",
"assets/images/avatar-frame-mask.svg",
"assets/images/avatar-frame-static.png",
"assets/images/favicon.svg"
) | Sort-Object
if (-not $PublicRoot) {
$PublicRoot = $Root
}
if ($PublicRoot) {
$PublicRoot = (Resolve-Path -LiteralPath $PublicRoot).Path
$actualPublic = @(Get-ChildItem -LiteralPath $PublicRoot -Recurse -File | ForEach-Object {
$_.FullName.Substring($PublicRoot.TrimEnd('\').Length + 1).Replace('\', '/')
} | Sort-Object)
Assert-Check (($actualPublic -join "`n") -eq ($allowedPublic -join "`n")) "public bundle contains only web-root files"
}
if ($PublicArchive) {
$PublicArchive = (Resolve-Path -LiteralPath $PublicArchive).Path
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [System.IO.Compression.ZipFile]::OpenRead($PublicArchive)
try {
$archiveFiles = @($archive.Entries | Where-Object { $_.Name } | ForEach-Object {
$_.FullName.Replace('\', '/').TrimStart('/')
} | Sort-Object)
Assert-Check (($archiveFiles -join "`n") -eq ($allowedPublic -join "`n")) "public ZIP contains only web-root files"
} finally {
$archive.Dispose()
}
}
Write-Host "Production checks complete."