157 lines
5.1 KiB
JavaScript
157 lines
5.1 KiB
JavaScript
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: /<InputGroup className="h-auto min-h-16 flex-col items-stretch">/,
|
|
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",
|
|
},
|
|
];
|
|
|
|
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).`,
|
|
);
|