99 lines
4.1 KiB
JavaScript
99 lines
4.1 KiB
JavaScript
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import postcss from "postcss";
|
||
|
||
/**
|
||
* Firefox 52 CSS 产物守卫:构建后运行,防止回归。
|
||
* 迁移后的结构性保证(见 tailwind.config.ts / postcss.config.js):
|
||
* - Tailwind v3 输出无 @layer(Chrome 99+)与 @property(Chrome 85+);
|
||
* - dark: 变体经 postcss-dark-descendant 改写为 .dark 后代形式;
|
||
* - :where() 由 Firefox 专用 PostCSS 插件展开;
|
||
* - 媒体查询 range 语法(Chrome 104+)由 lightningcss 按 target 转译;
|
||
* - 手写 CSS 不使用 :is()(复杂参数无法安全降级,源头展开为逗号选择器);
|
||
* - color-mix()/oklch() 仅允许出现在 @supports 守护内或带前置回退声明。
|
||
* :has() 按渐进增强放行,关键输入框布局由显式 class 和 :focus-within 兜底。
|
||
*/
|
||
const unsupportedColor = /(?:color-mix|oklch|oklab|lab|lch)\(/i;
|
||
const assetsDir = path.resolve("dist-firefox52/assets");
|
||
|
||
if (!fs.existsSync(assetsDir)) {
|
||
throw new Error("dist-firefox52/assets 不存在,请先运行 pnpm build:firefox52:fast");
|
||
}
|
||
|
||
const cssFiles = fs.readdirSync(assetsDir).filter((f) => f.endsWith(".css"));
|
||
if (cssFiles.length === 0) throw new Error("dist-firefox52/assets 中没有 CSS 构建产物");
|
||
|
||
const failures = [];
|
||
const hardTokens = [
|
||
[/@layer\b/, "@layer(Chrome 99+)"],
|
||
[/@property\b/, "@property(Chrome 85+)"],
|
||
[/:is\(/, ":is()(Chrome 88+)"],
|
||
[/:where\(/, ":where()(Firefox 78+)"],
|
||
[/\(width\s*>=|\(width\s*<=|\(height\s*>=/, "媒体查询 range 语法(Chrome 104+)"],
|
||
[/rgb\(\s*var\(/i, "rgb(var(...)) 现代通道语法"],
|
||
[/rgba\([^;{}]*,\s*calc\(/i, "rgba() 的 calc alpha(Firefox 52 不解析)"],
|
||
[/rgba\([^;{}]*,\s*[\d.]+%\s*\)/i, "rgba() 百分比 alpha(Firefox 52 不解析)"],
|
||
];
|
||
let guardedColors = 0;
|
||
let progressiveHas = 0;
|
||
const requiredComposerWidths = new Map([
|
||
[".max-w-\\[var\\(--container-width-md\\)\\]", "816px"],
|
||
[".max-w-\\[calc\\(var\\(--container-width-md\\)\\+150px\\)\\]", "966px"],
|
||
[".max-w-\\[var\\(--container-width-lg\\)\\]", "1024px"],
|
||
]);
|
||
const foundComposerWidths = new Map();
|
||
|
||
for (const file of cssFiles) {
|
||
const raw = fs.readFileSync(path.join(assetsDir, file), "utf8");
|
||
for (const [re, label] of hardTokens) {
|
||
const hits = raw.match(new RegExp(re.source, "g")) || [];
|
||
if (hits.length > 0) failures.push(`${file}: ${hits.length} 处 ${label}`);
|
||
}
|
||
progressiveHas += (raw.match(/:has\(/g) || []).length;
|
||
|
||
const root = postcss.parse(raw, { from: file });
|
||
root.walkRules((rule) => {
|
||
for (const [selector, expectedWidth] of requiredComposerWidths) {
|
||
if (!rule.selector?.includes(selector)) continue;
|
||
const width = rule.nodes?.find((node) => node.type === "decl" && node.prop === "max-width");
|
||
if (width?.value === expectedWidth) foundComposerWidths.set(selector, true);
|
||
}
|
||
});
|
||
root.walkDecls((decl) => {
|
||
if (!unsupportedColor.test(decl.value)) return;
|
||
guardedColors += 1;
|
||
let anc = decl.parent;
|
||
while (anc) {
|
||
if (anc.type === "atrule" && anc.name === "supports" && unsupportedColor.test(anc.params)) return;
|
||
anc = anc.parent;
|
||
}
|
||
let prev = decl.prev();
|
||
while (prev?.type === "comment") prev = prev.prev();
|
||
const hasFallback =
|
||
!decl.prop.startsWith("--") &&
|
||
prev?.type === "decl" &&
|
||
prev.prop === decl.prop &&
|
||
!unsupportedColor.test(prev.value);
|
||
if (!hasFallback) {
|
||
failures.push(`${file}: 未守护的现代颜色 ${decl.prop}: ${decl.value.slice(0, 60)}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
for (const selector of requiredComposerWidths.keys()) {
|
||
if (!foundComposerWidths.has(selector)) {
|
||
failures.push(`缺少 Firefox 52 输入区宽度回退:${selector}`);
|
||
}
|
||
}
|
||
|
||
if (failures.length > 0) {
|
||
console.error("Firefox 52 CSS 兼容检查失败:\n");
|
||
console.error(failures.slice(0, 50).join("\n"));
|
||
if (failures.length > 50) console.error(`... 及另外 ${failures.length - 50} 处`);
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log(
|
||
`Firefox 52 CSS 兼容检查通过(${guardedColors} 处守护的现代颜色声明,${progressiveHas} 处渐进式 :has())。`,
|
||
);
|