Files
NewBlog/scripts/verify-build-css.mjs

97 lines
3.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 构建产物 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 简写里的 <single-animation-timeline> 分量
* (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)