#!/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`, );