The device renders in Chrome 113, which predates relaxed CSS nesting, so
a nested rule whose selector starts with an element name is not a parse
error anyone would notice -- the rule simply does not exist, there and
nowhere else. Three were live in `index.css`, and the one that mattered
was the `text-overflow: ellipsis` on the bottom bar's title and artist,
which had therefore never truncated on the device. No tier here can see
the class at all: the component tier, the e2e tier and `make ui-visual`
all run a current engine, where the rule applies normally.
So `make css-check` carries a second script. It reads `index.css` and
the `css` literals in `src/**/*.ts` alike, since a shadow-root
stylesheet is parsed by the same engine, and it names the file, the line
and the fix -- a leading `&`, which is valid in both syntaxes.
The detection walks blocks rather than matching lines, and both things
it has to get right fall out of one rule: a rule is nested when a
*style* rule is somewhere above it, not when its immediate parent is a
block. That leaves `@media (...) { bottom-nav { ... } }` at the top
level alone, which is the majority of what a regex over the file would
report, and still flags the same rule inside an at-rule that is itself
inside a style rule. Strings and comments are read through, so a brace
in a `url()` is not a block.
The tree has no violation left, so the check would pass just as happily
over an empty glob: it refuses one, and `test/utils/css-nesting.test.ts`
pins the semantics that make the sweep mean something. The literal
scanner the two checks share is lifted into `css-literals.mjs`
unchanged, except that a `${}` substitution is now blanked keeping its
newlines so a line number survives it.
Closes #154
66 lines
2.1 KiB
JavaScript
66 lines
2.1 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 = [];
|
|
|
|
for (const { line, selector } of findBareNestedRules(
|
|
readFileSync('index.css', 'utf8'),
|
|
)) {
|
|
problems.push({ file: 'index.css', 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: index.css + ${sources.length} files, no bare nested rules`,
|
|
);
|