$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]++ } } $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)"