mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 04:25:52 +01:00
fix(runner): restore browser token verification
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
c191e24ab0
commit
f057078687
2 files changed
+132
No files matched your search
@@ -2,5 +2,7 @@
|
||||
/dist-phase5/
|
||||
/playwright-report/
|
||||
/test-results/
|
||||
/runner/target/
|
||||
!scripts/check-browser-tokens.mjs
|
||||
!scripts/check-forbidden-imports.mjs
|
||||
!scripts/check-forbidden-imports.test.mjs
|
||||
@@ -0,0 +1,130 @@
|
||||
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",
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user