[CmdletBinding()] param( [string]$NodeVersion = '20.20.2', [int]$MaxOldSpaceSizeMB = 8192 ) $ErrorActionPreference = 'Stop' $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path $toolRoot = Join-Path $env:LOCALAPPDATA 'CaiWuXiTong\tools' $nodeFolderName = "node-v$NodeVersion-win-x64" $nodeRoot = Join-Path $toolRoot $nodeFolderName $nodeExe = Join-Path $nodeRoot 'node.exe' $npmCmd = Join-Path $nodeRoot 'npm.cmd' $archiveName = "$nodeFolderName.zip" $archivePath = Join-Path $toolRoot $archiveName $checksumPath = Join-Path $toolRoot 'SHASUMS256.txt' $trackedCssPath = Join-Path $repoRoot 'public/css/app.css' $frontendOutputRoot = Join-Path $repoRoot '.caiwu-build/public' $fingerprintPath = Join-Path $repoRoot '.caiwu-build/frontend.sha' function Invoke-Native { param( [Parameter(Mandatory = $true)] [scriptblock]$Command, [Parameter(Mandatory = $true)] [string]$FailureMessage ) & $Command if ($LASTEXITCODE -ne 0) { throw $FailureMessage } } $fingerprintInputs = @( 'HEAD:resources', 'HEAD:package.json', 'HEAD:package-lock.json', 'HEAD:webpack.mix.js', 'HEAD:tailwind.config.js' ) $fingerprintHashes = @(& git -C $repoRoot rev-parse @fingerprintInputs) if ($LASTEXITCODE -ne 0) { throw '无法计算前端输入指纹。' } $frontendFingerprint = "$NodeVersion|$($fingerprintHashes -join '|')" $cachedOutputs = @( (Join-Path $frontendOutputRoot 'js/auth/common.min.js'), (Join-Path $frontendOutputRoot 'css/app.css'), (Join-Path $frontendOutputRoot 'mix-manifest.json'), $fingerprintPath ) $cacheComplete = -not ($cachedOutputs | Where-Object { -not (Test-Path -LiteralPath $_) }) if ($cacheComplete -and (Get-Content -Raw -LiteralPath $fingerprintPath).Trim() -eq $frontendFingerprint) { Write-Host '前端输入未变化,复用已有构建产物。' return } if (-not (Test-Path -LiteralPath $nodeExe)) { New-Item -ItemType Directory -Force -Path $toolRoot | Out-Null try { $downloadRoot = "https://nodejs.org/dist/v$NodeVersion" Invoke-WebRequest -Uri "$downloadRoot/$archiveName" -OutFile $archivePath Invoke-WebRequest -Uri "$downloadRoot/SHASUMS256.txt" -OutFile $checksumPath $checksumLine = Select-String -Path $checksumPath -Pattern ([regex]::Escape($archiveName) + '$') | Select-Object -First 1 if (-not $checksumLine) { throw 'Node 安装包缺少官方 SHA-256 校验值。' } $expectedHash = ($checksumLine.Line -split '\s+')[0].ToLowerInvariant() $actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $archivePath).Hash.ToLowerInvariant() if ($actualHash -ne $expectedHash) { throw 'Node 安装包 SHA-256 校验失败。' } Expand-Archive -LiteralPath $archivePath -DestinationPath $toolRoot -Force } finally { if (Test-Path -LiteralPath $archivePath) { Remove-Item -LiteralPath $archivePath -Force } if (Test-Path -LiteralPath $checksumPath) { Remove-Item -LiteralPath $checksumPath -Force } } } if (-not (Test-Path -LiteralPath $nodeExe) -or -not (Test-Path -LiteralPath $npmCmd)) { throw "项目专用 Node $NodeVersion 安装不完整。" } $originalPath = $env:Path $originalNodeOptions = $env:NODE_OPTIONS $trackedCssBackup = if (Test-Path -LiteralPath $trackedCssPath) { [IO.File]::ReadAllBytes($trackedCssPath) } else { $null } Push-Location $repoRoot try { $env:Path = "$nodeRoot;$originalPath" $env:NODE_OPTIONS = "--max-old-space-size=$MaxOldSpaceSizeMB" Invoke-Native { & $nodeExe --version } '项目专用 Node 无法运行。' Invoke-Native { & $npmCmd ci --prefer-offline --no-audit --no-fund } '前端依赖安装失败。' Invoke-Native { & $npmCmd run production } '前端资源构建失败。' $requiredOutputs = @( 'public/js/auth/common.min.js', 'public/css/app.css', 'public/mix-manifest.json' ) foreach ($output in $requiredOutputs) { if (-not (Test-Path -LiteralPath (Join-Path $repoRoot $output))) { throw "前端构建缺少产物:$output" } } if (Test-Path -LiteralPath $frontendOutputRoot) { Remove-Item -LiteralPath $frontendOutputRoot -Recurse -Force } New-Item -ItemType Directory -Force -Path (Join-Path $frontendOutputRoot 'css') | Out-Null Copy-Item -LiteralPath (Join-Path $repoRoot 'public/js') -Destination $frontendOutputRoot -Recurse Copy-Item -LiteralPath (Join-Path $repoRoot 'public/css/app.css') -Destination (Join-Path $frontendOutputRoot 'css/app.css') Copy-Item -LiteralPath (Join-Path $repoRoot 'public/mix-manifest.json') -Destination $frontendOutputRoot Set-Content -LiteralPath $fingerprintPath -Value $frontendFingerprint -NoNewline } finally { Pop-Location $env:Path = $originalPath $env:NODE_OPTIONS = $originalNodeOptions if ($null -ne $trackedCssBackup) { [IO.File]::WriteAllBytes($trackedCssPath, $trackedCssBackup) } }