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')
|
|||
|
|
}
|