Files
yellowjacket/frontend/scripts/check-css-nesting.mjs
T
logan 11ba7b3180
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m32s
CI / e2e (pull_request) Successful in 9m16s
build(frontend): sweep every stylesheet, not index.css by name
The hook fires on frontend/**/*.{ts,css} while the script read one hardcoded path, so a second stylesheet would have been silently unswept while the hook still went green over it. There is only index.css today, which is exactly when this is cheap to fix. Watched catching a planted nested rule in a second file.
2026-08-21 15:16:47 +00:00

80 lines
2.6 KiB
JavaScript

#!/usr/bin/env node
/**
* Fail on a nested rule whose selector starts with an element name.
*
* See `css-nesting.mjs` for what the phone does with one. No tier here
* can see it: the component tier, the e2e tier and `make ui-visual` all
* run a current Chromium, where the rule applies normally, so the only
* report is a screenshot of the device — which is how the bottom bar's
* title came to have never truncated there.
*
* It covers `index.css` and the `css` literals in the components alike,
* because a shadow-root stylesheet is parsed by the same engine.
*/
import { globSync, readFileSync } from 'node:fs';
import { taggedLiterals } from './css-literals.mjs';
import { findBareNestedRules } from './css-nesting.mjs';
const problems = [];
// Every stylesheet, not `index.css` by name: the hook that runs this
// fires on `frontend/**/*.{ts,css}`, so naming one file promises a
// coverage the sweep does not deliver -- a second stylesheet would be
// silently unswept while the hook still went green over it. There is
// only `index.css` today, which is exactly when this is free to fix.
const stylesheets = globSync('*.css', { cwd: process.cwd() });
if (stylesheets.length === 0) {
console.error('css-nesting-check: no stylesheet matched *.css');
process.exit(1);
}
for (const file of stylesheets) {
for (const { line, selector } of findBareNestedRules(
readFileSync(file, 'utf8'),
)) {
problems.push({ file, line, selector });
}
}
const sources = globSync('src/**/*.ts', { cwd: process.cwd() });
// A sweep over an empty glob passes, and this one is expected to find
// nothing, so "it found nothing" has to mean it looked.
if (sources.length === 0) {
console.error('css-nesting-check: no sources matched src/**/*.ts');
process.exit(1);
}
for (const file of sources) {
const src = readFileSync(file, 'utf8');
for (const literal of taggedLiterals(src, ['css'])) {
for (const { line, selector } of findBareNestedRules(literal.body)) {
problems.push({ file, line: literal.line + line - 1, selector });
}
}
}
if (problems.length > 0) {
for (const p of problems) {
console.error(
`${p.file}:${p.line}: nested rule "${p.selector.split('\n')[0]}" starts ` +
'with an element name — write it as "& ' +
`${p.selector.split('\n')[0]}"`,
);
}
console.error(
`\ncss-nesting-check: ${problems.length} problem(s). ` +
'Chrome 113 (the device) drops a nested rule that does not start ' +
'with a symbol; the leading & is valid in both syntaxes.',
);
process.exit(1);
}
console.log(
`css-nesting-check: ${stylesheets.length} stylesheet(s) + ${sources.length} files, no bare nested rules`,
);