mirror of
https://github.com/hhftechnology/Dock-Dploy.git
synced 2026-08-11 01:46:28 -05:00
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.
129 lines
4.1 KiB
JavaScript
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);
|