105 lines
4.8 KiB
PowerShell
105 lines
4.8 KiB
PowerShell
$ErrorActionPreference = 'Stop'
|
||
|
||
$root = Split-Path -Parent $PSScriptRoot
|
||
$manifest = 'design-pipeline/manifests/runtime-assets.json'
|
||
$validator = 'design-pipeline/scripts/validate-runtime-asset-inventory.mjs'
|
||
|
||
foreach ($relativePath in @($manifest, $validator)) {
|
||
if (-not (Test-Path -LiteralPath (Join-Path $root $relativePath) -PathType Leaf)) {
|
||
throw "缺少运行时资产合同文件:$relativePath"
|
||
}
|
||
}
|
||
|
||
# 注册表是正式资产物理规格的唯一入口。递归验证同时拒绝缺失、循环、重复导入、重复 id 和重复 output。
|
||
$json = & node (Join-Path $root $validator) $manifest
|
||
if ($LASTEXITCODE -ne 0) { throw '运行时资产注册表验证失败' }
|
||
$inventory = $json | ConvertFrom-Json
|
||
$owned = @{}
|
||
foreach ($asset in $inventory.assets) {
|
||
if ($owned.ContainsKey($asset.output)) { throw "运行时资产存在重复所有者:$($asset.output)" }
|
||
$owned[$asset.output] = $asset
|
||
}
|
||
|
||
# static/assets 不允许存在“先留着再说”的文件:每个物理文件都必须有 owner,每个 owner 也必须有真实输出。
|
||
$physicalAssets = @(
|
||
Get-ChildItem -LiteralPath (Join-Path $root 'static/assets') -Recurse -File |
|
||
ForEach-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
|
||
)
|
||
$unownedFiles = @($physicalAssets | Where-Object { -not $owned.ContainsKey($_) })
|
||
$missingFiles = @($owned.Keys | Where-Object { $_ -notin $physicalAssets })
|
||
if ($unownedFiles.Count -gt 0) { throw "存在未登记的 static/assets 文件:$($unownedFiles -join ', ')" }
|
||
if ($missingFiles.Count -gt 0) { throw "资产 owner 指向不存在的输出:$($missingFiles -join ', ')" }
|
||
|
||
Add-Type -AssemblyName System.Drawing
|
||
foreach ($asset in $inventory.assets) {
|
||
$absolutePath = Join-Path $root $asset.output
|
||
$image = [System.Drawing.Image]::FromFile($absolutePath)
|
||
try {
|
||
if ($image.Width -ne $asset.width -or $image.Height -ne $asset.height) {
|
||
throw "运行时资产尺寸漂移:$($asset.output)"
|
||
}
|
||
$hasAlpha = [System.Drawing.Image]::IsAlphaPixelFormat($image.PixelFormat)
|
||
if ($hasAlpha -ne [bool]$asset.alpha) {
|
||
throw "运行时资产透明通道漂移:$($asset.output)"
|
||
}
|
||
} finally {
|
||
$image.Dispose()
|
||
}
|
||
|
||
if ($asset.sha256) {
|
||
$actualBytes = (Get-Item -LiteralPath $absolutePath).Length
|
||
$actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $absolutePath).Hash.ToLowerInvariant()
|
||
if ($actualBytes -ne $asset.bytes -or $actualHash -ne $asset.sha256) {
|
||
throw "直接提交的运行时资产发生漂移:$($asset.output)"
|
||
}
|
||
} elseif ((Get-Item -LiteralPath $absolutePath).Length -gt $asset.maxBytes) {
|
||
throw "清单生成资产超过体积上限:$($asset.output)"
|
||
}
|
||
}
|
||
|
||
# 消费者只从运行时代码中提取;清单、测试和文档本身不能替孤立输出制造虚假引用。
|
||
$referenceFiles = Get-ChildItem -LiteralPath @(
|
||
(Join-Path $root 'pages'),
|
||
(Join-Path $root 'components'),
|
||
(Join-Path $root 'styles'),
|
||
(Join-Path $root 'data'),
|
||
(Join-Path $root 'utils')
|
||
) -Recurse -File | Where-Object { $_.Extension -in '.vue', '.scss', '.js' }
|
||
|
||
$referenceCounts = @{}
|
||
foreach ($output in $owned.Keys) { $referenceCounts[$output] = 0 }
|
||
foreach ($file in $referenceFiles) {
|
||
$content = Get-Content -Raw -Encoding UTF8 -LiteralPath $file.FullName
|
||
foreach ($match in [regex]::Matches($content, '/?static/assets/[A-Za-z0-9_./-]+\.(?:png|jpe?g|webp|svg)')) {
|
||
$assetPath = $match.Value.TrimStart('/')
|
||
if (-not $owned.ContainsKey($assetPath)) {
|
||
throw "运行时代码引用了无 owner 的资产:$assetPath($($file.FullName.Substring($root.Length + 1)))"
|
||
}
|
||
$referenceCounts[$assetPath]++
|
||
}
|
||
}
|
||
|
||
# ModulePage 的两个路径由四个受控模块名动态展开;这里验证模板和有限集合,禁止扫描器漏掉动态消费者。
|
||
$modulePagePath = Join-Path $root 'components/ModulePage.vue'
|
||
$modulePage = Get-Content -Raw -Encoding UTF8 -LiteralPath $modulePagePath
|
||
foreach ($template in @(
|
||
'/static/assets/modules/${assetModule.value}/transparent/module-content-frame.png',
|
||
'/static/assets/modules/${assetModule.value}/transparent/module-field-frame.png'
|
||
)) {
|
||
if (-not $modulePage.Contains($template)) { throw "ModulePage 动态资产模板漂移:$template" }
|
||
}
|
||
foreach ($module in @('family', 'records', 'notification', 'profile')) {
|
||
foreach ($fileName in @('module-content-frame.png', 'module-field-frame.png')) {
|
||
$assetPath = "static/assets/modules/$module/transparent/$fileName"
|
||
if (-not $owned.ContainsKey($assetPath)) { throw "ModulePage 动态资产没有 owner:$assetPath" }
|
||
$referenceCounts[$assetPath]++
|
||
}
|
||
}
|
||
|
||
$orphanedOutputs = @($referenceCounts.Keys | Where-Object { $referenceCounts[$_] -eq 0 })
|
||
if ($orphanedOutputs.Count -gt 0) {
|
||
throw "资产清单保留了没有运行时消费者的输出:$($orphanedOutputs -join ', ')"
|
||
}
|
||
|
||
Write-Output "RUNTIME-ASSETS-CONTRACT PASS ($($owned.Count) assets)"
|