// 构建报告 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('
构建报告
')
lines.push('概览
')
lines.push(
`- job: ${esc(report.jobId)}
- 状态: ${esc(report.status)}
` +
`- 站点: ${esc(report.siteKey)}(${esc(report.siteId)})
` +
`- 幂等键: ${esc(report.idempotencyKey)}
- 主题: ${esc(report.theme.key)}@${esc(report.theme.version)}
` +
`- 生成时间: ${esc(report.generatedAt)}
`,
)
lines.push('门禁
')
lines.push(
`- 规则版本: ${esc(report.validation.ruleVersion)}
` +
`- 状态: ${esc(report.validation.status)}
` +
`- run: ${esc(report.validation.runId)}
` +
`- P0(阻断): ${report.validation.p0Count}
` +
`- P2(报告): ${report.validation.p2Count}
`,
)
lines.push('构建环境
')
lines.push(
`- node: ${esc(report.buildEnv.node)}
- pnpm: ${esc(report.buildEnv.pnpm)}
` +
`- sourceCommit: ${esc(report.buildEnv.sourceCommit)}
` +
`- locale: ${esc(report.buildEnv.locale)}
- timezone: ${esc(report.buildEnv.timezone)}
`,
)
lines.push('快照
')
lines.push(
`- config 修订: ${esc(report.snapshot.siteConfigRevisionId)}
` +
`- manifest: ${esc(report.snapshot.manifestPath)}
` +
`- contents: ${report.snapshot.counts.contents}
` +
`- taxonomies: ${report.snapshot.counts.taxonomies}
` +
`- routes: ${report.snapshot.counts.routes}
` +
`- assets: ${report.snapshot.counts.assets}
` +
`- URL 重写: rewritten=${report.urlRewrite.rewritten} unresolved=${report.urlRewrite.unresolved}
`,
)
lines.push('产物
')
lines.push(
`- dist: ${esc(report.artifacts.distPath)}
` +
`- dist sha256: ${esc(report.artifacts.distSha256)}
` +
`- dist 文件数: ${report.artifacts.distFileCount}
` +
`- route-manifest: ${esc(report.artifacts.routeManifestPath)}
` +
`- route-manifest sha256: ${esc(report.artifacts.routeManifestSha256)}
` +
`- sitemap 条目: ${report.artifacts.sitemapEntries}
` +
`- 媒体复制: ${report.artifacts.mediaCopied}
`,
)
lines.push('问题清单
')
if (report.issues.length === 0) {
lines.push('(无)
')
} else {
lines.push('')
for (const issue of report.issues) {
lines.push(`- ${esc(issue)}
`)
}
lines.push('
')
}
lines.push('耗时
')
lines.push(`- total: ${report.durationMs.toFixed(1)}ms
`)
return lines.join('\n')
}