#!/usr/bin/env node // Every rule in the built stylesheet must sit inside a cascade layer // (milestone 23). Unlayered author CSS outranks every layer whatever its // selector says, so a single unlayered rule silently beats the StyleX atomic // rules it was written to sit under. That failure renders wrong and passes // every other gate: no test asserts computed style, and the bundler is happy. // This check runs from web/ as part of `npm run build`. import { readdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const distDir = join(dirname(dirname(fileURLToPath(import.meta.url))), "dist", "assets"); const sheets = readdirSync(distDir).filter((name) => name.endsWith(".css")); if (sheets.length === 0) { console.error("assert-css-layers: no stylesheet in dist/assets — did the build emit one?"); process.exit(1); } // At-rules that describe a resource or a name rather than styling an element. // They carry no cascade priority against a layer, so being outside one is // correct, and StyleX emits `@property` for its custom properties. const unlayerable = String.raw`@(?:layer|property|keyframes|font-face|counter-style|charset|import)`; /** Strip comments, then every balanced block and statement the rule above allows. */ function outsideLayers(css) { let rest = css.replace(/\/\*[\s\S]*?\*\//g, ""); for (;;) { const at = rest.search(new RegExp(`${unlayerable}[^{;]*\\{`)); if (at === -1) break; let depth = 0; let end = rest.indexOf("{", at); for (let i = end; i < rest.length; i += 1) { if (rest[i] === "{") depth += 1; else if (rest[i] === "}") { depth -= 1; if (depth === 0) { end = i; break; } } } rest = rest.slice(0, at) + rest.slice(end + 1); } return rest.replace(new RegExp(`${unlayerable}[^;{}]*;`, "g"), ""); } /** * A rule that only sets custom properties styles nothing on its own — StyleX * emits its token `:root` block that way, ahead of its layers, and a variable * is consumed through `var()` rather than competing with a layered rule. */ function stylesSomething(body) { return body .split(";") .map((declaration) => declaration.trim()) .some((declaration) => declaration.length > 0 && !declaration.startsWith("--")); } let failed = false; for (const sheet of sheets) { const leftover = outsideLayers(readFileSync(join(distDir, sheet), "utf8")); for (const [, selector, body] of leftover.matchAll(/([^{}]+)\{([^{}]*)\}/g)) { if (!stylesSomething(body)) continue; console.error( `assert-css-layers: ${sheet} styles elements outside every @layer:\n` + ` ${selector.trim().slice(0, 80)} { ${body.trim().slice(0, 60)} … }`, ); failed = true; break; } } if (failed) { console.error("Wrap it in a layer declared before StyleX's, as web/src/styles.css does."); process.exit(1); } console.log( `every rule in ${sheets.length === 1 ? "the stylesheet" : `${sheets.length} stylesheets`} sits inside a cascade layer`, );