Add inline style guard to lint suite

This commit is contained in:
thePR0M3TH3AN committed 2025-10-13 16:51:06 -04:00
1 parent d6d7024a95
commit 693bd2bef2
6 files changed
+226 -10

No files matched your search

+2 -2
View File
@@ -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
+12 -5
View File
@@ -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
+2 -1
View File
@@ -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",
+1 -1
View File
@@ -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.
+203
View File
@@ -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 = /(?<!['"`])\.\s*style\b/gim;
function toPosix(relPath) {
return relPath.split(path.sep).join("/");
}
function buildLineBreaks(content) {
const breaks = [];
let index = content.indexOf("\n");
while (index !== -1) {
breaks.push(index);
index = content.indexOf("\n", index + 1);
}
return breaks;
}
function getLineNumber(lineBreaks, index) {
let low = 0;
let high = lineBreaks.length;
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (index <= lineBreaks[mid]) {
high = mid;
} else {
low = mid + 1;
}
}
return low + 1;
}
function getLineSnippet(content, index) {
const start = content.lastIndexOf("\n", index - 1) + 1;
let end = content.indexOf("\n", index);
if (end === -1) {
end = content.length;
}
return content.slice(start, end).trim();
}
function collectViolations(relPath, content) {
const lineBreaks = buildLineBreaks(content);
const results = [];
let match;
INLINE_STYLE_ATTR_REGEX.lastIndex = 0;
while ((match = INLINE_STYLE_ATTR_REGEX.exec(content))) {
const index = match.index;
results.push({
file: toPosix(relPath),
line: getLineNumber(lineBreaks, index),
label: "Inline style attribute",
snippet: getLineSnippet(content, index),
});
}
DIRECT_STYLE_REGEX.lastIndex = 0;
while ((match = DIRECT_STYLE_REGEX.exec(content))) {
const index = match.index;
const after = content.slice(DIRECT_STYLE_REGEX.lastIndex);
const isCssText = /^\s*\.\s*cssText/i.test(after);
results.push({
file: toPosix(relPath),
line: getLineNumber(lineBreaks, index),
label: isCssText ? "style.cssText usage" : "Direct .style usage",
snippet: getLineSnippet(content, index),
});
}
return results;
}
function shouldIgnore(relPath) {
const parts = relPath.split(path.sep);
if (parts.some((part) => 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;
});
+6 -1
View File
@@ -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 = [];
}