check:shadow-css-parses reads the style id: it must be a plain identifier

gospel-plugin's trap (COMMON.md): a CSS rule moved past the first argument of useShadowStyles concatenates
onto the style id ('@media(...){...}doxa-gospel-map'). That is valid JS, so the build stays green and the rule
is silently lost; the brace-balance scan never sees it because the id is not CSS. The walker now folds the
second argument too, and a literal id must match /^[a-z0-9-]+$/ (a computed id is left alone). Measured on
the current tree first: 0 offenders, so this breaks no owner. Self-tests cover the trap and a plain id.
From dx-shepherd (END-DXS-CSSID).
This commit is contained in:
Doxa Maps 2026-09-27 04:43:23 +00:00
parent 12a647028e
commit e9e1adc0d6

View file

@ -73,6 +73,12 @@ function shadowCssOf(script, filename) {
if (node.type === 'CallExpression' && node.callee && node.callee.name === 'useShadowStyles') {
const acc = { css: '', complete: true }
fold(node.arguments[0], acc)
// The style ID (second argument). A plain identifier is the only correct shape: a CSS rule moved
// past the first argument concatenates onto it ('@media(...){...}doxa-gospel-map'), which is valid
// JS, keeps the build green and silently loses the rule (gospel-plugin's trap, COMMON.md).
const lab = { text: '', literal: true, present: !!node.arguments[1] }
if (node.arguments[1]) { const la = { css: '', complete: true }; fold(node.arguments[1], la); lab.text = la.css; lab.literal = la.complete }
acc.label = lab
found.push(acc)
}
for (const k of Object.keys(node)) {
@ -153,6 +159,14 @@ ok(r.open === 1 && !r.orphans.length, 'a data: URL with a ; inside url() does no
ok(got.length === 1 && got[0].complete === false,
'an argument built partly from an imported constant is reported PARTIAL, not scanned as a whole sheet')
}
{
// gospel-plugin's trap: a rule moved past the CSS argument concatenates onto the style id
const trap = "useShadowStyles('.a{ color:red; }', '@media (max-width:1px){ .b{ color:red; } }' + 'doxa-gospel-map')"
const lab = shadowCssOf(trap, 'x.js')[0].label
ok(lab.literal && !/^[a-z0-9-]+$/.test(lab.text), 'the style-id arm reads a rule concatenated onto the id as NOT a plain identifier')
const good = shadowCssOf("useShadowStyles('.a{ color:red; }', 'doxa-gospel-map')", 'x.js')[0].label
ok(good.literal && /^[a-z0-9-]+$/.test(good.text), 'NEG CONTROL: a plain id passes the style-id arm')
}
// ── the tree ────────────────────────────────────────────────────────────────────────────────────
const walk = (d) => readdirSync(d).flatMap((f) => {
@ -172,7 +186,13 @@ for (const f of files) {
let blocks
try { blocks = scriptsOf(f, src).flatMap((s) => shadowCssOf(s, f)) }
catch (e) { console.log(` FAIL - ${rel(f)}: could not be parsed (${e.message.slice(0, 70)})`); fails++; continue }
for (const { css, complete } of blocks) {
for (const { css, complete, label } of blocks) {
// ID arm: a literal style ID must be a plain identifier. A computed one (a variable) is left alone.
if (label && label.present && label.literal && !/^[a-z0-9-]+$/.test(label.text)) {
console.log(` FAIL - ${rel(f)}: useShadowStyles id is not a plain identifier: ${JSON.stringify(label.text.slice(0, 80))}`)
console.log(' a CSS rule has probably been concatenated onto the id; move it back into the first argument')
fails++
}
if (!/[{}]/.test(css)) { partials.push(f); continue }
// ★ A PARTIAL ARGUMENT IS STILL SCANNED, and skipping it was a false green waiting to happen.
// region-dashboards.vue builds its sheet as DXLIFE_TOKENS + SHADOW_BOX_SIZING_RESET + literals,