初始化

This commit is contained in:
gozeros committed 2026-09-07 18:24:55 +08:00
1 parent 34474bd5fc
commit b638b541d0
3008 files changed
+837699

No files matched your search

@@ -0,0 +1,119 @@
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">/,
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).`,
);
@@ -0,0 +1,70 @@
/**
* Tailwind 4 gracefully falls back from color-mix() to the base semantic
* color, but that turns utilities such as bg-primary/5 into an opaque fill on
* Chrome 109. Replace that fallback with an sRGB-channel based alpha color.
* The modern color-mix() declaration remains in its @supports block.
*/
const LEGACY_RGB_TOKENS = new Set([
"accent",
"background",
"border",
"brand-200",
"card",
"destructive",
"foreground",
"input",
"muted",
"muted-foreground",
"primary",
"primary-foreground",
"ring",
"secondary",
"sidebar-accent",
"sidebar-border",
"sidebar-foreground",
"workspace-brand",
]);
const TRANSPARENT_MIX_RE =
/color-mix\(\s*in\s+[\w-]+\s*,\s*var\(--([\w-]+)\)\s+([\d.]+)%\s*,\s*transparent\s*\)/i;
function legacyAlphaColor(value) {
const match = value.match(TRANSPARENT_MIX_RE);
if (!match || !LEGACY_RGB_TOKENS.has(match[1])) return null;
const [, token, percentage] = match;
return `rgb(var(--legacy-${token}-rgb) / ${percentage}%)`;
}
export default function legacyColorFallback() {
return {
postcssPlugin: "postcss-legacy-color-fallback",
OnceExit(root) {
root.walkAtRules("supports", (atRule) => {
if (!atRule.params.includes("color-mix")) return;
atRule.walkDecls((modernDeclaration) => {
const fallbackValue = legacyAlphaColor(modernDeclaration.value);
if (!fallbackValue) return;
// Tailwind emits the old-browser declaration immediately before
// the nested @supports block, in the same utility/variant rule.
let fallbackDeclaration = atRule.prev();
while (fallbackDeclaration?.type === "comment") {
fallbackDeclaration = fallbackDeclaration.prev();
}
if (
fallbackDeclaration?.type === "decl" &&
fallbackDeclaration.prop === modernDeclaration.prop
) {
fallbackDeclaration.value = fallbackValue;
}
});
});
},
};
}
legacyColorFallback.postcss = true;
@@ -0,0 +1,33 @@
import postcss from "postcss";
import { transform } from "lightningcss";
const LEGACY_CHROME = 100 << 16;
/**
* Keep Vite's PostCSS pipeline (required by Tailwind 4), then compile the
* resulting CSS to Chrome 100-compatible syntax for both dev and production.
*/
export default function lightningCssLegacyChrome() {
return {
postcssPlugin: "postcss-lightningcss-legacy-chrome",
OnceExit(root, { result }) {
const filename = root.source?.input.file ?? result.opts.from ?? "style.css";
const output = transform({
filename,
code: Buffer.from(root.toString()),
targets: { chrome: LEGACY_CHROME },
minify: false,
});
const compiled = postcss.parse(output.code.toString(), { from: filename });
root.removeAll();
root.append(compiled.nodes);
for (const warning of output.warnings) {
result.warn(warning.message, { plugin: "lightningcss" });
}
},
};
}
lightningCssLegacyChrome.postcss = true;