Files
Dock-Dploy/scripts/check-no-magic-css.mjs
hhftechnologies 06fc45b87a Add ESLint, compose-builder, YAML utils & tests
Introduce a project-wide ESLint configuration with TypeScript/React rules (including a ban on direct useEffect and inline style object literals) and file-specific overrides. Update index.html to add theme-color meta tags and preload Google fonts. Expand package.json with new scripts (lint, typecheck, validate, extended test commands) and bump/add dev/dependencies for testing, linting, Radix UI, TanStack and tooling. Add many new components and tests (BrandMark, SetupSidebar, compose-builder UI and ServiceForm tabs, VPN tab, CodeMirror runtime, ThemeProvider, template UI and tests, icons), update existing components (CodeEditor, ConversionDialog, Header, Footer, MetaTags, SidebarUI) and hooks/routes. Add YAML generator and related utils, validation schemas, and numerous unit tests and fixtures. Misc: update tsconfig/vite config and styles.
2026-05-11 17:48:46 +05:30

129 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Hard rule enforcement: no hardcoded CSS values in components/routes.
* Allowed locations: src/styles.css, *.css files, SVG `viewBox`/`d` attrs.
*
* Bans (in src/**\/*.{ts,tsx}, excluding tests + generated files):
* - Hex color literals: #fff, #cc785c, #181715, #ff112233 etc.
* - Tailwind arbitrary lengths: w-[340px], h-[72px], text-[14px]
* - Tailwind arbitrary colors: bg-[#cc785c], text-[#181715]
* - Inline style={{ ... }} object literals (also caught by ESLint, double-checked here)
*
* Exit non-zero on any hit. Used in `pnpm validate`.
*/
import { readFile, readdir, stat } from "node:fs/promises";
import { join, sep } from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = fileURLToPath(new URL("../src", import.meta.url));
const SKIP_FILES = new Set([
"routeTree.gen.ts",
]);
const SKIP_PATH_FRAGMENTS = [
`${sep}__tests__${sep}`,
`${sep}icons${sep}BrandIcons.tsx`,
// shadcn/ui primitives are vendored upstream — they use a fixed set of
// arbitrary Tailwind values (ring-[3px], top-[50%], w-[8rem]) that are
// idiomatic to shadcn. We accept them rather than diverging from the
// upstream templates. Our own components must still comply.
`${sep}components${sep}ui${sep}`,
];
const HEX_COLOR = /#[0-9a-fA-F]{3,8}\b/g;
const TW_ARB_PX = /\b\w+-\[\d+(?:\.\d+)?(?:px|rem|em|%|vh|vw|svh|svw)\]/g;
const TW_ARB_COLOR = /\b\w+-\[#[0-9a-fA-F]{3,8}\]/g;
const INLINE_STYLE = /style=\{\{/g;
const violations = [];
async function walk(dir) {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const full = join(dir, entry.name);
if (entry.isDirectory()) {
await walk(full);
continue;
}
if (!/\.(ts|tsx)$/.test(entry.name)) continue;
if (SKIP_FILES.has(entry.name)) continue;
if (SKIP_PATH_FRAGMENTS.some((frag) => full.includes(frag))) continue;
await scan(full);
}
}
async function scan(file) {
const src = await readFile(file, "utf8");
const lines = src.split(/\r?\n/);
lines.forEach((line, i) => {
// Respect explicit per-line opt-outs for genuinely-dynamic values
// (e.g. lazy-loaded CodeMirror inner height passed via style prop):
// `style={{ width: w }} // check-no-magic-css-allow`
if (line.includes("check-no-magic-css-allow")) return;
// Skip lines that look like SVG path data ("d=") or viewBox attributes,
// and tolerate hex colors in pure SVG fill/stroke attributes if any creep
// into icon files. We deny everywhere else.
const stripped = line
.replace(/d="[^"]*"/g, "")
.replace(/viewBox="[^"]*"/g, "");
const hexMatches = stripped.match(HEX_COLOR);
if (hexMatches) {
for (const m of hexMatches) {
violations.push({ file, line: i + 1, kind: "hex-color", value: m });
}
}
const arbPxMatches = stripped.match(TW_ARB_PX);
if (arbPxMatches) {
for (const m of arbPxMatches) {
violations.push({ file, line: i + 1, kind: "tw-arb-length", value: m });
}
}
const arbColorMatches = stripped.match(TW_ARB_COLOR);
if (arbColorMatches) {
for (const m of arbColorMatches) {
violations.push({ file, line: i + 1, kind: "tw-arb-color", value: m });
}
}
const inlineStyleMatches = stripped.match(INLINE_STYLE);
if (inlineStyleMatches) {
for (const _m of inlineStyleMatches) {
violations.push({
file,
line: i + 1,
kind: "inline-style",
value: "style={{...}}",
});
}
}
});
}
try {
await stat(ROOT);
} catch {
console.error(`[check-no-magic-css] src/ not found at ${ROOT}`);
process.exit(1);
}
await walk(ROOT);
if (violations.length === 0) {
console.log("[check-no-magic-css] OK — no hardcoded CSS literals in src/");
process.exit(0);
}
console.error(
`[check-no-magic-css] FAILED — ${violations.length} hardcoded CSS literal(s) found:\n`,
);
for (const v of violations) {
const rel = v.file.replace(ROOT, "src");
console.error(` ${rel}:${v.line} [${v.kind}] ${v.value}`);
}
console.error(
"\nFix: move tokens into src/styles.css and reference via classes / var(--token).",
);
process.exit(1);