8f2b9825a6
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>
96 lines
3.8 KiB
TypeScript
96 lines
3.8 KiB
TypeScript
// 构建报告 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')
|
||
}
|