diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72c235b9..d7dd8494 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,8 +43,8 @@ jobs: if: steps.node-modules-cache.outputs.cache-hit != 'true' run: npm ci - - name: Run CSS lint - run: npm run lint:css + - name: Run lint suite + run: npm run lint - name: Build CSS run: npm run build:css diff --git a/README.md b/README.md index dfd6ce65..564c3f0e 100644 --- a/README.md +++ b/README.md @@ -84,13 +84,20 @@ the shared tokens in `css/tokens.css`. Install dependencies once and lean on the package scripts to keep formatting, linting, and generated output consistent: ```bash -npm install # install Prettier, Stylelint, and Tailwind toolchain -npm run format # format CSS/HTML/JS/MD with Prettier + tailwindcss plugin -npm run lint:css # enforce token usage and forbid raw hex colors -npm run build:css # rebuild css/tailwind.generated.css from tailwind.source.css -npm run check:css # CI-friendly guard that fails if tailwind.generated.css is dirty +npm install # install Prettier, Stylelint, and Tailwind toolchain +npm run format # format CSS/HTML/JS/MD with Prettier + tailwindcss plugin +npm run lint # run CSS, hex color, and inline-style guards in one pass +npm run lint:css # enforce token usage and forbid raw hex colors +npm run lint:inline-styles # fail CI if inline style attributes or element.style usage slip in +npm run build:css # rebuild css/tailwind.generated.css from tailwind.source.css +npm run check:css # CI-friendly guard that fails if tailwind.generated.css is dirty ``` +Inline styles are intentionally blocked. `npm run lint:inline-styles` scans HTML +and scripts for `style=` attributes, `element.style`, or `style.cssText` usage +and will fail CI until offending markup is moved into the shared CSS/token +system. + The build command compiles Tailwind with `tailwind.config.cjs`, runs it through the PostCSS pipeline defined in `postcss.config.cjs` (for autoprefixing), and emits the purged, minified stylesheet at `css/tailwind.generated.css`. Commit the diff --git a/package.json b/package.json index 3e29befe..31ef208b 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,10 @@ "scripts": { "build:css": "npx tailwindcss -c tailwind.config.cjs -i css/tailwind.source.css -o css/tailwind.generated.css --minify", "format": "prettier --cache --write css/tailwind.source.css css/tokens.css \"docs/**/*.html\" tailwind.config.cjs README.md", - "lint": "npm run lint:css && npm run lint:hex", + "lint": "npm run lint:css && npm run lint:hex && npm run lint:inline-styles", "lint:css": "stylelint css/tailwind.source.css css/tokens.css", "lint:hex": "node scripts/check-hex.js", + "lint:inline-styles": "node scripts/check-inline-styles.mjs", "check:css": "npm run build:css && git diff --exit-code -- css/tailwind.generated.css", "test:visual": "npm run build:css && npx playwright test", "test:visual:update": "npm run build:css && cross-env UPDATE_VISUAL_BASELINES=1 npx playwright test", diff --git a/scripts/check-hex.js b/scripts/check-hex.js index d4d26b57..c0a7b181 100755 --- a/scripts/check-hex.js +++ b/scripts/check-hex.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -const { spawnSync } = require('node:child_process'); +import { spawnSync } from "node:child_process"; const SEARCH_PATTERN = '\\b#[0-9a-fA-F]{3,8}\\b'; // Allow-list tokens and vector logos so brand assets can keep their baked colors. diff --git a/scripts/check-inline-styles.mjs b/scripts/check-inline-styles.mjs new file mode 100644 index 00000000..9cb9a01e --- /dev/null +++ b/scripts/check-inline-styles.mjs @@ -0,0 +1,203 @@ +#!/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", + "torrent", + "dist", + "build", + ".next", + "out", + "coverage", + ".cache", + ".astro", + ".svelte-kit", + ".vercel", + "tmp", + "logs", +]); + +const IGNORED_FILES = new Set([ + "js/ui/styleSystem.js", + "css/tailwind.generated.css", + "js/webtorrent.min.js", + "js/webtorrent.min.js.map", + "scripts/check-inline-styles.mjs", + "sw.min.js", +]); + +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 = /(? IGNORED_DIRS.has(part))) { + return true; + } + const posixPath = toPosix(relPath); + 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); + + if (results.length > 0) { + console.error("\nInline style usage detected:\n"); + for (const violation of results) { + console.error( + `${violation.file}:${violation.line} — ${violation.label}\n ${violation.snippet}\n` + ); + } + console.error( + "Inline styles are disallowed. Move styles into tokens, CSS, or styleSystem." + ); + 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; +}); diff --git a/tests/video-modal-zap.test.mjs b/tests/video-modal-zap.test.mjs index bb6c20f1..e97639b6 100644 --- a/tests/video-modal-zap.test.mjs +++ b/tests/video-modal-zap.test.mjs @@ -11,7 +11,12 @@ class DummyElement { toggle: noop, contains: () => false, }; - this.style = {}; + Object.defineProperty(this, "style", { + configurable: true, + enumerable: true, + writable: true, + value: {}, + }); this.dataset = {}; this.children = []; }