feat(build): 最小门禁与状态机闭环
Task 6(Plan 4):validating 接真实门禁——五项产物检查(①内部死链 ②sitemap 一致 ③SEO 基线 ④大小写冲突 ⑤产物完整性),P0 阻断/P2 报告分级;validation_ runs/validation_issues 落库(severity/rule/url/evidence),任一 P0 → job failed (VALIDATION_FAILED + 产物清场),全过 → succeeded。HTML 解析用 htmlparser2 (sanitize-html 既有传递依赖,lock 已解析 12.0.0——apps/api 显式声明复用,零新 包版本),禁正则提标签。报告 json/html 双落盘且 HTML 先写(Plan 3 窄窗口修, html 转义模板)。顺手:build-snapshot 测试 spawn 缩减 7→2(六文件物化/bodyHtml 重写/ADR-010 改 materializeRenderSnapshot 直调;复用两场景并谱共享单次成功构建), 根套件 97.3s→75.8s;两测试文件清场补 validation 域反序(FK 链先行)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
// 构建报告 HTML 渲染(Plan 4 Task 6——"报告 HTML 先写"窄窗口修的渲染面):
|
||||
// 沿用 Plan 2/3 模板纪律(import 域 report-html.ts 同款):
|
||||
// - 对象层(build.json)保留原文——JSON 归档/机器消费的语义层;
|
||||
// - 渲染层对**所有**动态文本经 escapeHtml(<>&"' 全转义)——报告动态文本含站点键/
|
||||
// 幂等键/路径等操作者可控串,未转义渲染会使报告本身成为注入载体;
|
||||
// - 静态骨架无 script/事件属性;计数与清单按确定序渲染。
|
||||
import type { BuildReport } from './build.service.js'
|
||||
|
||||
/** HTML 转义(渲染层唯一动态文本通道):<>&"' 全转义,& 最先避免二次转义陷阱 */
|
||||
function escapeHtml(text: string): string {
|
||||
return text.replace(/[&<>"']/g, (ch) => {
|
||||
switch (ch) {
|
||||
case '&':
|
||||
return '&'
|
||||
case '<':
|
||||
return '<'
|
||||
case '>':
|
||||
return '>'
|
||||
case '"':
|
||||
return '"'
|
||||
default:
|
||||
return '''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const esc = escapeHtml
|
||||
|
||||
/** 构建报告 → HTML 片段(结构:概览/门禁/环境/快照计数/产物/耗时/问题清单) */
|
||||
export function renderBuildReportHtml(report: BuildReport): string {
|
||||
const lines: string[] = []
|
||||
lines.push('<h2>构建报告</h2>')
|
||||
|
||||
lines.push('<h3>概览</h3>')
|
||||
lines.push(
|
||||
`<ul><li>job: ${esc(report.jobId)}</li><li>状态: ${esc(report.status)}</li>` +
|
||||
`<li>站点: ${esc(report.siteKey)}(${esc(report.siteId)})</li>` +
|
||||
`<li>幂等键: ${esc(report.idempotencyKey)}</li><li>主题: ${esc(report.theme.key)}@${esc(report.theme.version)}</li>` +
|
||||
`<li>生成时间: ${esc(report.generatedAt)}</li></ul>`,
|
||||
)
|
||||
|
||||
lines.push('<h3>门禁</h3>')
|
||||
lines.push(
|
||||
`<ul><li>规则版本: ${esc(report.validation.ruleVersion)}</li>` +
|
||||
`<li>状态: ${esc(report.validation.status)}</li>` +
|
||||
`<li>run: ${esc(report.validation.runId)}</li>` +
|
||||
`<li>P0(阻断): ${report.validation.p0Count}</li>` +
|
||||
`<li>P2(报告): ${report.validation.p2Count}</li></ul>`,
|
||||
)
|
||||
|
||||
lines.push('<h3>构建环境</h3>')
|
||||
lines.push(
|
||||
`<ul><li>node: ${esc(report.buildEnv.node)}</li><li>pnpm: ${esc(report.buildEnv.pnpm)}</li>` +
|
||||
`<li>sourceCommit: ${esc(report.buildEnv.sourceCommit)}</li>` +
|
||||
`<li>locale: ${esc(report.buildEnv.locale)}</li><li>timezone: ${esc(report.buildEnv.timezone)}</li></ul>`,
|
||||
)
|
||||
|
||||
lines.push('<h3>快照</h3>')
|
||||
lines.push(
|
||||
`<ul><li>config 修订: ${esc(report.snapshot.siteConfigRevisionId)}</li>` +
|
||||
`<li>manifest: ${esc(report.snapshot.manifestPath)}</li>` +
|
||||
`<li>contents: ${report.snapshot.counts.contents}</li>` +
|
||||
`<li>taxonomies: ${report.snapshot.counts.taxonomies}</li>` +
|
||||
`<li>routes: ${report.snapshot.counts.routes}</li>` +
|
||||
`<li>assets: ${report.snapshot.counts.assets}</li>` +
|
||||
`<li>URL 重写: rewritten=${report.urlRewrite.rewritten} unresolved=${report.urlRewrite.unresolved}</li></ul>`,
|
||||
)
|
||||
|
||||
lines.push('<h3>产物</h3>')
|
||||
lines.push(
|
||||
`<ul><li>dist: ${esc(report.artifacts.distPath)}</li>` +
|
||||
`<li>dist sha256: ${esc(report.artifacts.distSha256)}</li>` +
|
||||
`<li>dist 文件数: ${report.artifacts.distFileCount}</li>` +
|
||||
`<li>route-manifest: ${esc(report.artifacts.routeManifestPath)}</li>` +
|
||||
`<li>route-manifest sha256: ${esc(report.artifacts.routeManifestSha256)}</li>` +
|
||||
`<li>sitemap 条目: ${report.artifacts.sitemapEntries}</li>` +
|
||||
`<li>媒体复制: ${report.artifacts.mediaCopied}</li></ul>`,
|
||||
)
|
||||
|
||||
lines.push('<h3>问题清单</h3>')
|
||||
if (report.issues.length === 0) {
|
||||
lines.push('<p>(无)</p>')
|
||||
} else {
|
||||
lines.push('<ul>')
|
||||
for (const issue of report.issues) {
|
||||
lines.push(`<li>${esc(issue)}</li>`)
|
||||
}
|
||||
lines.push('</ul>')
|
||||
}
|
||||
|
||||
lines.push('<h3>耗时</h3>')
|
||||
lines.push(`<ul><li>total: ${report.durationMs.toFixed(1)}ms</li></ul>`)
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
Reference in New Issue
Block a user