Files
yellowjacket/frontend/scripts/check-css-literals.mjs
T
logan 7f8e185d7c build(frontend): fail css-check on a nested rule the phone drops
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
2026-08-21 15:16:47 +00:00

63 lines
2.3 KiB
JavaScript

#!/usr/bin/env node
/**
* A backtick inside a comment in a `css` tagged template literal ends
* the literal.
*
* This has cost four sessions across three plans. It is written down in
* CLAUDE.md, in the yellowjacket-dev skill and in NOTES.md, and it was
* read twice in the session it then cost a cycle in — so it is a check
* now rather than a fourth paragraph. Knowledge that has been ignored
* three times is not a knowledge problem.
*
* What makes it expensive is not the mistake but the *report*. The
* literal ends early, the rest of the CSS is parsed as JavaScript, and
* what comes back is `Expected "]" but found "inline"` pointing at a
* line of prose — or, when it happens in a shared module like
* `tokens.css.ts`, every test in the suite failing to import and an
* output that reads like a broken test runner. And `make dev-headless`
* leaves the dev server serving the last good bundle, so the page still
* works and still shows the old behaviour.
*
* The detection is exact rather than heuristic. If a backtick inside a
* comment closed the literal early, then the text the parser *did* take
* as the literal contains an unterminated `/*`. Nothing else produces
* that, and a legitimate literal cannot contain one.
*/
import { globSync, readFileSync } from 'node:fs';
import { taggedLiterals } from './css-literals.mjs';
const TAGS = ['css', 'html', 'svg'];
const files = globSync('src/**/*.ts', { cwd: process.cwd() });
const problems = [];
for (const file of files) {
const src = readFileSync(file, 'utf8');
for (const { tag, body, line } of taggedLiterals(src, TAGS)) {
const opens = (body.match(/\/\*/g) ?? []).length;
const closes = (body.match(/\*\//g) ?? []).length;
if (opens > closes) problems.push({ file, line, tag });
}
}
if (problems.length > 0) {
for (const p of problems) {
console.error(
`${p.file}:${p.line}: unterminated /* inside a ${p.tag}\`\` literal — ` +
'a backtick in a comment ends the literal early',
);
}
console.error(
`\ncss-literal-check: ${problems.length} problem(s). ` +
'Remove the backticks from the comment; markdown quoting does not ' +
'survive a tagged template.',
);
process.exit(1);
}
console.log(`css-literal-check: ${files.length} files, no broken literals`);