#!/usr/bin/env node import { readdir, readFile } from "node:fs/promises"; import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, ".."); const IGNORED_DIRS = new Set([ ".git", "node_modules", "vendor", "dist", "build", ".next", "out", "coverage", ".cache", ".astro", ".svelte-kit", ".vercel", "tmp", "logs", "playwright-report", "test-results", "issues", "scripts", ]); const DIST_SCAN_PREFIXES = new Set([ "torrent/dist", ]); const DIST_SCAN_PREFIXES_ARRAY = [...DIST_SCAN_PREFIXES]; const IGNORED_FILES = new Set([ "css/tailwind.generated.css", "js/webtorrent.min.js", "js/webtorrent.min.js.map", "scripts/check-inline-styles.mjs", "scripts/daily-design-system-audit.mjs", "sw.min.js", ]); const VIOLATION_ALLOWLIST = new Map([ [ "js/themeController.js", new Set(["Direct .style usage"]), ], [ "js/ui/components/VideoCard.js", new Set(["Direct .style usage"]), ], [ "torrent/app.js", new Set(["Direct .style usage"]), ], [ "torrent/dist/beacon.js", new Set(["Direct .style usage"]), ], [ "js/embed.js", new Set(["Direct .style usage"]), ], [ "js/ui/components/VideoModal.js", new Set(["Direct .style usage"]), ], ]); const TEXT_EXTENSIONS = new Set([ ".astro", ".cjs", ".html", ".htm", ".js", ".jsx", ".liquid", ".mjs", ".njk", ".svelte", ".svg", ".ts", ".tsx", ".vue", ]); const INLINE_STYLE_ATTR_REGEX = /<[^>]*\sstyle\s*=/gim; const DIRECT_STYLE_REGEX = /(? posixPath === prefix || posixPath.startsWith(`${prefix}/`) ) ) { continue; } return true; } if (IGNORED_FILES.has(posixPath)) { return true; } return false; } function shouldCheckFile(relPath) { const ext = path.extname(relPath).toLowerCase(); if (ext === "") { return false; } return TEXT_EXTENSIONS.has(ext); } async function walk(dir, results) { const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); const relPath = path.relative(repoRoot, fullPath); if (shouldIgnore(relPath)) { continue; } if (entry.isDirectory()) { await walk(fullPath, results); continue; } if (!entry.isFile()) { continue; } if (!shouldCheckFile(relPath)) { continue; } const content = await readFile(fullPath, "utf8"); const fileViolations = collectViolations(relPath, content); results.push(...fileViolations); } } async function main() { const results = []; await walk(repoRoot, results); const filteredResults = results.filter((violation) => { const allowlistedLabels = VIOLATION_ALLOWLIST.get(violation.file); if (!allowlistedLabels) { return true; } return !allowlistedLabels.has(violation.label); }); if (filteredResults.length > 0) { console.error("\nInline style usage detected:\n"); for (const violation of filteredResults) { console.error( `${violation.file}:${violation.line} — ${violation.label}\n ${violation.snippet}\n` ); } console.error("Inline styles are disallowed. Move styles into tokens or CSS."); process.exitCode = 1; return; } console.log("No inline style usage found."); } main().catch((error) => { console.error("Failed to scan for inline styles."); console.error(error); process.exitCode = 1; });