/* 构建产物 CSS 自检:拦住「滚动驱动动画被压缩器并进 animation 简写」这一类静默回归。 * * 背景(2026-09-21 的实际事故): * 线上「回到顶部」按钮不显示,本地 dev 却正常。 * 原因是 Lightning CSS(Tailwind v4 的 CSS 压缩器)把 * animation: back-to-top-reveal linear both; * animation-timeline: scroll(root block); * 合并成了 * animation: linear both back-to-top-reveal scroll(root); * 而 WebKit 不接受 animation 简写里的 分量 * (webkit#293276),于是**整条 animation 声明被判无效丢弃**,元素永远停在 * 初始的 opacity:0 / visibility:hidden。 * * 最要命的是它只在构建产物里出现:astro dev 不做这个压缩,所以本地怎么试都正常; * 而 CSS.supports("animation-timeline: scroll()") 又照样返回 true, * 运行时特性检测也发现不了。见 lightningcss#815。 * * 这个脚本在 build 之后跑,扫 dist 下所有 CSS:只要 animation 简写的值里出现 * scroll() / view() 时间线函数,就让构建失败。 * * 用法: * node scripts/verify-build-css.mjs # 失败时退出码 1 * node scripts/verify-build-css.mjs --warn # 只警告,不失败 */ import fs from "node:fs" import path from "node:path" const DIST = "./dist" const WARN_ONLY = process.argv.includes("--warn") /** animation 简写里不该出现的时间线函数 */ const TIMELINE_FN = /(?:^|[^-\w])(?:scroll|view)\(/ /** 收集 dist 下所有 css 文件 */ function collectCssFiles(dir) { if (!fs.existsSync(dir)) return [] const found = [] for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name) if (entry.isDirectory()) found.push(...collectCssFiles(full)) else if (entry.name.endsWith(".css")) found.push(full) } return found } /** * 只检查 `animation:` 简写(以及 -webkit-animation:),不碰 * `animation-timeline:` / `animation-range:` 这两个长写属性 —— 后者是正确写法。 */ function findShorthandViolations(css) { const hits = [] // 负向回顾避免匹配到 animation-timeline / animation-name 这类长写属性 const re = /(?:^|[;{}(])\s*(?:-webkit-)?animation\s*:\s*([^;}]*)/g let match while ((match = re.exec(css)) !== null) { const value = match[1] if (TIMELINE_FN.test(value)) { hits.push(value.trim().slice(0, 120)) } } return hits } const files = collectCssFiles(DIST) if (files.length === 0) { console.error(`❌ ${DIST} 下没有找到 CSS,构建产物异常`) process.exit(1) } const problems = [] for (const file of files) { const css = fs.readFileSync(file, "utf-8") for (const value of findShorthandViolations(css)) { problems.push({ file, value }) } } if (problems.length === 0) { console.log(`✅ 构建产物 CSS 自检通过(${files.length} 个文件,没有时间线函数混进 animation 简写)`) process.exit(0) } console.error("❌ 构建产物 CSS 自检失败:animation 简写里混进了时间线函数。") console.error("") for (const { file, value } of problems) { console.error(` ${file}`) console.error(` animation: ${value}`) } console.error("") console.error(" 这类声明会被 WebKit 整条丢弃(webkit#293276),元素会停在初始样式上。") console.error(" 修法:把 animation-timeline / animation-range 挪到**独立的规则块**里,") console.error(" 不要和 animation 简写放在同一组声明中(lightningcss#815)。") console.error("") process.exit(WARN_ONLY ? 0 : 1)