css sanity checks

This commit is contained in:
Ben Floyd 2025-11-19 11:45:37 -07:00
parent dbb8d15a9e
commit 0b1f4d673b
3 changed files with 127 additions and 2 deletions

View file

@ -485,7 +485,7 @@ styles/
- [x] Introduce a lightweight CSS lint configuration (`stylelint` + [`.stylelintrc.json`](.stylelintrc.json:1)) scoped to `styles/**/*.css` to enforce:
- No ID selectors in plugin CSS.
- A small, explicit set of allowed class prefixes for plugin and layout classes (`coalesce-`, `backlinks-`, `markdown-`, `theme-`, `is-`, `has-`, `no-`).
- [ ] Optionally add a simple “unused selector” check in CI (build + grep for key class names in HTML/DOM tests).
- [x] Add a simple “unused selector” check via `npm run lint:css:unused` using [`scripts/check-unused-css.mjs`](scripts/check-unused-css.mjs:1), which heuristically reports plugin-scoped CSS classes not referenced in `src/**/*.ts`/`.js` files (informational only; does not fail the build).
This plan keeps the runtime surface unchanged (still a single `dist/styles.css`) while making the CSS layout more modular and slice-aware.

View file

@ -10,7 +10,8 @@
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint:css": "stylelint \"styles/**/*.css\""
"lint:css": "stylelint \"styles/**/*.css\"",
"lint:css:unused": "node scripts/check-unused-css.mjs"
},
"keywords": [],
"author": "",

View file

@ -0,0 +1,124 @@
#!/usr/bin/env node
import { promises as fs } from 'fs';
import path from 'path';
const ROOT = process.cwd();
const CSS_DIR = path.join(ROOT, 'styles');
const SRC_DIR = path.join(ROOT, 'src');
const SKIP_DIRS = ['node_modules', '.git', 'dist'];
const SOURCE_EXTS = ['.ts', '.tsx', '.js', '.jsx'];
async function collectFiles(dir, exts) {
const files = [];
async function walk(current) {
let entries;
try {
entries = await fs.readdir(current, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const full = path.join(current, entry.name);
if (entry.isDirectory()) {
if (SKIP_DIRS.some(skip => full.includes(path.sep + skip + path.sep))) {
continue;
}
await walk(full);
} else if (exts.some(ext => full.endsWith(ext))) {
files.push(full);
}
}
}
await walk(dir);
return files;
}
async function collectCssFiles() {
return collectFiles(CSS_DIR, ['.css']);
}
async function collectSourceFiles() {
return collectFiles(SRC_DIR, SOURCE_EXTS);
}
function extractClassesFromCss(css) {
const classRegex = /\.([a-zA-Z0-9_-]+)/g;
const classes = new Set();
let match;
while ((match = classRegex.exec(css)) !== null) {
const name = match[1];
if (/^(coalesce|backlinks|markdown|theme|is|has|no)-/.test(name)) {
classes.add(name);
}
}
return classes;
}
async function buildFileCache(files) {
const cache = new Map();
for (const file of files) {
try {
const content = await fs.readFile(file, 'utf8');
cache.set(file, content);
} catch {
// ignore read errors
}
}
return cache;
}
function isClassUsedInSources(className, sourceCache) {
for (const content of sourceCache.values()) {
if (content.includes(className)) {
return true;
}
}
return false;
}
async function main() {
console.log('[coalesce] CSS unused-selector check starting...');
const cssFiles = await collectCssFiles();
const sourceFiles = await collectSourceFiles();
if (cssFiles.length === 0) {
console.log('[coalesce] No CSS files found under styles/');
return;
}
const sourceCache = await buildFileCache(sourceFiles);
const allClasses = new Set();
for (const cssFile of cssFiles) {
const css = await fs.readFile(cssFile, 'utf8');
const classes = extractClassesFromCss(css);
for (const cls of classes) {
allClasses.add(cls);
}
}
const possiblyUnused = [];
for (const cls of allClasses) {
const used = isClassUsedInSources(cls, sourceCache);
if (!used) {
possiblyUnused.push(cls);
}
}
if (possiblyUnused.length === 0) {
console.log('[coalesce] No obvious unused plugin CSS classes found.');
} else {
console.log('[coalesce] Potentially unused plugin CSS classes (heuristic):');
for (const cls of possiblyUnused) {
console.log(' - .' + cls);
}
console.log('[coalesce] Review these and remove any truly unused selectors. This check is informational and does not fail the build.');
}
}
main().catch((err) => {
console.error('[coalesce] CSS unused-selector check failed:', err);
process.exitCode = 1;
});