<# .SYNOPSIS Rebuild all bundle artifacts by concatenating inputs per bundleconfig.json (strip BOM, no minify). .DESCRIPTION No dependency on BuildBundlerMinifier. Deterministic pure concatenation: 1. Read bundleconfig.json 2. For each bundle: read input files in order, strip leading UTF-8 BOM, join with CRLF 3. Only write out minify.enabled=false artifacts (overwrite committed files) For minify.enabled=true artifacts (minified CSS), skip generation (requires tooling) Output is byte-identical to BuildBundlerMinifier (SHA256 verified). .PARAMETER Verify Verify-only mode: compare generated content with committed files, report mismatches, do NOT modify files. .EXAMPLE .\rebuild-bundles.ps1 # Rebuild and overwrite committed bundle artifacts .\rebuild-bundles.ps1 -Verify # Verify only, do not modify files #> [CmdletBinding()] param( [switch]$Verify ) $ErrorActionPreference = 'Stop' $projectDir = Split-Path -Parent $MyInvocation.MyCommand.Path $configPath = Join-Path $projectDir 'bundleconfig.json' if (-not (Test-Path $configPath)) { throw "bundleconfig.json not found: $configPath" } # Read bundleconfig.json (strip // comments) $configJson = Get-Content $configPath -Raw $configJson = $configJson -replace '(?m)//.*$', '' $bundles = $configJson | ConvertFrom-Json $utf8NoBom = [System.Text.UTF8Encoding]::new($false) $mismatches = @() # CI 环境检测:CI 下 git checkout 后所有文件 mtime 几乎相同,增量检查不可靠 # 因此 CI 环境强制全量重建,本地开发才走增量 $isCI = -not [string]::IsNullOrEmpty($env:CI) ` -or -not [string]::IsNullOrEmpty($env:JENKINS_HOME) ` -or -not [string]::IsNullOrEmpty($env:BUILD_NUMBER) ` -or -not [string]::IsNullOrEmpty($env:GITHUB_ACTIONS) foreach ($bundle in $bundles) { $outputRel = $bundle.outputFileName $outputPath = Join-Path $projectDir $outputRel $isMinified = $bundle.minify.enabled # 增量检查:非 Verify、非 CI、非 minified,且 bundle 产物已存在时, # 若所有源文件都不比 bundle 产物新,则跳过此 bundle # 目的:让 target 挂到 Build 之前时,后端-only 改动不会触发重建,IDE F5 不变慢 # CI 环境强制全量:git checkout 后 mtime 不可靠,必须重建保证部署产物正确 if (-not $Verify -and -not $isCI -and -not $isMinified -and (Test-Path $outputPath)) { $outputMtime = (Get-Item $outputPath).LastWriteTime $stale = $false foreach ($inputRel in $bundle.inputFiles) { $inputPath = Join-Path $projectDir $inputRel if ((Test-Path $inputPath) -and (Get-Item $inputPath).LastWriteTime -gt $outputMtime) { $stale = $true break } } if (-not $stale) { Write-Host ("[UP-TO-DATE] {0}" -f $outputRel) -ForegroundColor DarkGray continue } } Write-Host ("[{0}] {1}" -f $(if ($isMinified) { 'SKIP-MIN' } else { 'BUILD' }), $outputRel) # Check input files exist + no BOM $parts = [System.Collections.Generic.List[string]]::new() $missingInputs = @() $bomInputs = @() foreach ($inputRel in $bundle.inputFiles) { $inputPath = Join-Path $projectDir $inputRel if (-not (Test-Path $inputPath)) { $missingInputs += $inputRel continue } # Detect BOM by reading raw bytes $bytes = [System.IO.File]::ReadAllBytes($inputPath) $hasBom = ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) if ($hasBom) { $bomInputs += $inputRel } # Read text (UTF8, BOM auto-stripped by ReadAllText) $text = [System.IO.File]::ReadAllText($inputPath, [System.Text.Encoding]::UTF8) # Normalize CRLF to LF (bundle artifacts use LF in git repo) $text = $text -replace "`r`n", "`n" $parts.Add($text) } if ($missingInputs.Count -gt 0) { # 缺失输入文件:Verify 模式报错;重建模式只警告不 fail,避免阻塞 CI 主构建 # 重建模式下跳过此 bundle(保留已提交的产物) $level = $(if ($Verify) { 'ERROR' } else { 'WARN' }) $color = $(if ($Verify) { 'Red' } else { 'Yellow' }) Write-Host " [$level] Missing input files:" -ForegroundColor $color foreach ($m in $missingInputs) { Write-Host " - $m" } if ($Verify) { $mismatches += "$outputRel : missing inputs ($($missingInputs -join ', '))" } continue } if ($bomInputs.Count -gt 0) { # Verify 模式:BOM 是源文件问题,报错阻止提交 # 重建模式:BOM 会在读取时自动剥离(ReadAllText 处理),只警告不 fail,避免阻塞 CI 部署 $level = $(if ($Verify) { 'ERROR' } else { 'WARN' }) $color = $(if ($Verify) { 'Red' } else { 'Yellow' }) Write-Host " [$level] Input files have BOM (will be stripped on rebuild):" -ForegroundColor $color foreach ($b in $bomInputs) { Write-Host " - $b" } if ($Verify) { $mismatches += "$outputRel : input BOM ($($bomInputs -join ', '))" } } # Pure concatenation with LF separator between files (matches committed bundle artifacts) $content = [string]::Join("`n", $parts) # For minified artifacts, only report, do not write if ($isMinified) { Write-Host " (minified artifact, skip write, only input check)" -ForegroundColor Yellow continue } if ($Verify) { # Verify mode: compare with committed file if (-not (Test-Path $outputPath)) { Write-Host " [ERROR] Output file not found: $outputRel" -ForegroundColor Red $mismatches += "$outputRel : output file not found" continue } $existing = [System.IO.File]::ReadAllText($outputPath, [System.Text.Encoding]::UTF8) if ($existing -ne $content) { Write-Host " [MISMATCH] Differs from committed version" -ForegroundColor Red $mismatches += "$outputRel : out of sync (source changed but bundle not rebuilt)" } else { Write-Host " [OK] Matches committed version" -ForegroundColor Green } } else { # Rebuild mode: write (no BOM) $dir = Split-Path $outputPath -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } [System.IO.File]::WriteAllText($outputPath, $content, $utf8NoBom) Write-Host " [DONE] Written ($($content.Length) chars)" -ForegroundColor Green } } Write-Host "" if ($mismatches.Count -gt 0) { # Verify 模式:有问题则 fail(阻止提交不规范的状态) # 重建模式:永远不 fail,避免阻塞 CI 主构建(缺失/BOM 等已在循环内警告并跳过) if ($Verify) { Write-Host "=== Result: ISSUES FOUND (Verify mode) ===" -ForegroundColor Red foreach ($m in $mismatches) { Write-Host " - $m" } exit 1 } else { Write-Host "=== Result: COMPLETED WITH WARNINGS (Rebuild mode) ===" -ForegroundColor Yellow foreach ($m in $mismatches) { Write-Host " - $m" } Write-Host " (warnings do not block build in rebuild mode)" -ForegroundColor DarkGray exit 0 } } else { Write-Host "=== Result: ALL PASSED ===" -ForegroundColor Green exit 0 }