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 layout", }, { 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", }, { file: "src/pages/ChatPage.tsx", pattern: /chat-landing-composer-offset[\s\S]*chat-landing-content-lift/, description: "Chat landing layout must not rely on Tailwind's individual translate property", }, { file: "src/styles/globals.css", pattern: /\.chat-landing-composer-offset\s*\{\s*position:\s*relative;\s*top:\s*calc\(96px\s*-\s*50vh\);\s*\}/, description: "Chat landing composer must offset with top, not Tailwind translate", }, { file: "src/styles/globals.css", pattern: /\.legacy-dialog-viewport\s*\{[\s\S]*?display:\s*flex;[\s\S]*?align-items:\s*center;[\s\S]*?justify-content:\s*center;/, description: "Dialog content must be flex-centered without Tailwind translate", }, { file: "src/components/ui/dialog.tsx", pattern: /legacy-dialog-viewport[\s\S]*relative grid/, description: "Dialog panel must not use fixed + translate centering", }, { file: "src/styles/globals.css", pattern: /\.legacy-dialog-viewport\s*>\s*\[data-slot="dialog-content"\]\s*\{[\s\S]*?min-height:\s*0;[\s\S]*?max-height:\s*100%;/, description: "Flex-centered dialogs must keep min-height:0 so inner scroll works", }, { file: "src/styles/globals.css", pattern: /\.legacy-floating-layer\s*\{\s*z-index:\s*300;/, description: "Portaled select/dropdown layers must sit above dialog overlay z-index 200", }, { file: "src/components/workspace/skills/skill-file-preview-dialog.tsx", pattern: /minHeight:[\s\S]*100vh[\s\S]*maxHeight:[\s\S]*100vh/, description: "Skill file preview must size with vh, not dvh, for Chrome 102", }, { file: "src/core/compat/abort-signal.ts", pattern: /export function installAbortSignalTimeout[\s\S]*AbortSignal/, description: "LangGraph SDK timeoutMs needs AbortSignal.timeout for Chrome 102", }, { file: "src/core/api/api-client.ts", pattern: /function installLegacyControlTimeout[\s\S]*AbortSignal\.timeout/, description: "Chat send must skip AbortSignal.timeout when Chrome 102 lacks it", }, { file: "index.html", pattern: /AbortSignal\.timeout = function/, description: "Chrome 102 needs AbortSignal.timeout before any module loads", }, { file: "src/styles/globals.css", pattern: /\.workspace-chat-bg,\s*\n\s*\.canvas-writing-bg\s*\{\s*background-color:\s*var\(--background\);/, description: "Agent chat canvas needs an explicit token background for Chrome 102", }, { file: "src/styles/globals.css", pattern: /@supports not \(translate: 0\)[\s\S]*\.dialog-content-center\s*\{\s*transform:\s*translate\(-50%,\s*-50%\);/, description: "Dialog centering needs a transform fallback for Chrome 102", }, ]; 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).`, );