import fs from "node:fs"; import path from "node:path"; import postcss from "postcss"; const unsupportedColor = /(?:color-mix|oklch|oklab|lab|lch)\(/i; const assetsDir = path.resolve("dist/assets"); if (!fs.existsSync(assetsDir)) { throw new Error("dist/assets 不存在,请先运行 pnpm build"); } const cssFiles = fs .readdirSync(assetsDir) .filter((file) => file.endsWith(".css")); if (cssFiles.length === 0) { throw new Error("dist/assets 中没有 CSS 构建产物"); } const failures = []; let modernDeclarations = 0; const requiredLegacyFallbacks = [ { file: "src/components/ui/textarea.tsx", pattern: /rows=\{rows\s*\?\?\s*3\}/, description: "Textarea needs a native rows fallback", }, { file: "src/components/ai-elements/prompt-input.tsx", pattern: //, description: "Prompt textarea must not rely on :has() for its parent height", }, { file: "src/core/utils/mermaid.ts", pattern: /htmlLabels:\s*false[\s\S]*secure:\s*\[[\s\S]*"htmlLabels"/, description: "Sandbox flowcharts must use native SVG labels in legacy browsers", }, { file: "src/core/utils/legacy-html-preview.ts", pattern: /export function downlevelModernColors[\s\S]*function injectLegacyChartShim/, description: "Generated HTML charts must downlevel modern CSS colors for Chrome 109", }, ]; for (const file of cssFiles) { const root = postcss.parse( fs.readFileSync(path.join(assetsDir, file), "utf8"), { from: file }, ); root.walkDecls((declaration) => { if (!unsupportedColor.test(declaration.value)) return; modernDeclarations += 1; let ancestor = declaration.parent; while (ancestor) { if ( ancestor.type === "atrule" && ancestor.name === "supports" && unsupportedColor.test(ancestor.params) ) { return; } ancestor = ancestor.parent; } let previous = declaration.prev(); while (previous?.type === "comment") previous = previous.prev(); const hasSafeFallback = !declaration.prop.startsWith("--") && previous?.type === "decl" && previous.prop === declaration.prop && !unsupportedColor.test(previous.value); if (!hasSafeFallback) { failures.push( `${file}:${declaration.source?.start?.line ?? "?"} ${declaration.prop}: ${declaration.value}`, ); } }); } function walk(directory) { return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { const fullPath = path.join(directory, entry.name); return entry.isDirectory() ? walk(fullPath) : [fullPath]; }); } for (const file of walk(path.resolve("src"))) { if (!/\.[jt]sx?$/.test(file)) continue; const source = fs.readFileSync(file, "utf8"); if (unsupportedColor.test(source)) { failures.push( `${path.relative(process.cwd(), file)} contains a runtime-only modern color function`, ); } } for (const fallback of requiredLegacyFallbacks) { const source = fs.readFileSync(path.resolve(fallback.file), "utf8"); if (!fallback.pattern.test(source)) { failures.push(`${fallback.file}: ${fallback.description}`); } } if (failures.length > 0) { console.error("Legacy Chrome CSS compatibility check failed:\n"); console.error(failures.slice(0, 50).join("\n")); if (failures.length > 50) { console.error(`... and ${failures.length - 50} more`); } process.exit(1); } console.log( `Legacy Chrome (100+) CSS compatibility check passed (${modernDeclarations} guarded modern declarations).`, );