131 lines
3.9 KiB
JavaScript
131 lines
3.9 KiB
JavaScript
import { readFile, readdir } from "node:fs/promises";
|
|
import { dirname, extname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const sourceRoots = [
|
|
resolve(packageRoot, "devtools/browser/src"),
|
|
resolve(packageRoot, "src/react"),
|
|
resolve(packageRoot, "examples"),
|
|
];
|
|
|
|
async function sourceFiles(directory) {
|
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
const nested = await Promise.all(
|
|
entries.map((entry) => {
|
|
const path = resolve(directory, entry.name);
|
|
return entry.isDirectory() ? sourceFiles(path) : [path];
|
|
}),
|
|
);
|
|
return nested.flat();
|
|
}
|
|
|
|
const componentRules = [
|
|
["hex color", /#[0-9a-f]{3,8}\b/i],
|
|
["raw pixel value", /\b\d+(?:\.\d+)?px\b/],
|
|
["inline style", /\bstyle\s*=\s*\{/],
|
|
["raw font size", /\bfont(?:-size|Size)\s*[:=]/],
|
|
["arbitrary Tailwind value", /\b[a-z-]+-\[[^\]]+\]/],
|
|
];
|
|
const violations = [];
|
|
|
|
for (const path of (await Promise.all(sourceRoots.map(sourceFiles))).flat()) {
|
|
if (![".ts", ".tsx"].includes(extname(path))) {
|
|
continue;
|
|
}
|
|
const source = await readFile(path, "utf8");
|
|
for (const [label, pattern] of componentRules) {
|
|
if (pattern.test(source)) {
|
|
violations.push(`${path.slice(packageRoot.length + 1)}: ${label}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const path of await sourceFiles(resolve(packageRoot, "src/react"))) {
|
|
if (![".ts", ".tsx"].includes(extname(path))) continue;
|
|
if (/\bui-[a-z-]+/.test(await readFile(path, "utf8"))) {
|
|
violations.push(`${path.slice(packageRoot.length + 1)}: legacy ui- class name`);
|
|
}
|
|
}
|
|
|
|
const styles = await readFile(resolve(packageRoot, "devtools/browser/src/styles.css"), "utf8");
|
|
for (const token of [
|
|
"--background",
|
|
"--foreground",
|
|
"--card",
|
|
"--primary",
|
|
"--muted",
|
|
"--border",
|
|
"--ring",
|
|
"--font-sans",
|
|
"--font-mono",
|
|
"--space-1",
|
|
"--radius-sm",
|
|
"--motion-fast",
|
|
]) {
|
|
if (!styles.includes(`${token}:`)) {
|
|
violations.push(`devtools/browser/src/styles.css: missing ${token}`);
|
|
}
|
|
}
|
|
|
|
const sdkStyles = await readFile(resolve(packageRoot, "styles.css"), "utf8");
|
|
for (const token of [
|
|
"--pcr-background",
|
|
"--pcr-foreground",
|
|
"--pcr-card",
|
|
"--pcr-primary",
|
|
"--pcr-muted",
|
|
"--pcr-border",
|
|
"--pcr-ring",
|
|
"--pcr-font-sans",
|
|
"--pcr-font-mono",
|
|
"--pcr-space-1",
|
|
"--pcr-radius-sm",
|
|
"--pcr-motion-fast",
|
|
"--pcr-touch-target",
|
|
]) {
|
|
if (!sdkStyles.includes(`${token}:`)) {
|
|
violations.push(`styles.css: missing ${token}`);
|
|
}
|
|
}
|
|
if (!sdkStyles.startsWith(".pcr-root {")) {
|
|
violations.push("styles.css: token sheet must be scoped to .pcr-root");
|
|
}
|
|
if (sdkStyles.includes(":root")) {
|
|
violations.push("styles.css: SDK tokens may not leak onto :root");
|
|
}
|
|
if (sdkStyles.includes("\\n")) {
|
|
violations.push("styles.css: contains a literal \\n escape sequence");
|
|
}
|
|
if (/--pcr---|\.pcr-[a-z-]+--pcr-/i.test(sdkStyles)) {
|
|
violations.push("styles.css: malformed repeated pcr prefix");
|
|
}
|
|
const sdkTokenNames = new Set([
|
|
...[...sdkStyles.matchAll(/--([a-z][a-z0-9-]*)\s*:/gi)].map((match) => match[1]),
|
|
...[...sdkStyles.matchAll(/var\(\s*--([a-z][a-z0-9-]*)/gi)].map(
|
|
(match) => match[1],
|
|
),
|
|
]);
|
|
for (const tokenName of sdkTokenNames) {
|
|
if (!tokenName.startsWith("pcr-")) {
|
|
violations.push(`styles.css: unprefixed SDK token --${tokenName}`);
|
|
}
|
|
}
|
|
const cssWithoutComments = sdkStyles.replaceAll(/\/\*[\s\S]*?\*\//g, "");
|
|
const openBraces = [...cssWithoutComments].filter((character) => character === "{").length;
|
|
const closeBraces = [...cssWithoutComments].filter((character) => character === "}").length;
|
|
if (openBraces !== closeBraces) {
|
|
violations.push(
|
|
`styles.css: unbalanced braces (${openBraces} open, ${closeBraces} close)`,
|
|
);
|
|
}
|
|
|
|
if (violations.length > 0) {
|
|
process.stderr.write(`Standalone browser token check failed:\n- ${violations.join("\n- ")}\n`);
|
|
process.exitCode = 1;
|
|
} else {
|
|
process.stdout.write(
|
|
"Standalone browser token check passed; component visual values stay in styles.css.\n",
|
|
);
|
|
}
|