Skip to content

Check every @theme and :root block in the theme mirror guard, not just the first - #484

Merged
nedtwigg merged 2 commits into
mainfrom
fix/theme-mirror-guard-all-blocks
Aug 29, 2026
Merged

Check every @theme and :root block in the theme mirror guard, not just the first#484
nedtwigg merged 2 commits into
mainfrom
fix/theme-mirror-guard-all-blocks

Conversation

@dormouse-bot

@dormouse-bot dormouse-bot commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

The mirror guard that landed in #478 reads only the first @theme block and the first :root block, so a second one of either is skipped in silence — and its @theme \{ literal never matches Tailwind v4's modifier forms like @theme inline { ... }, which is the shape you reach for precisely when a token's value is a var() chain, i.e. exactly the tokens the guard exists to check. Switching blockBody to matchAll over global patterns, and matching the @theme header with a bounded wildcard rather than a literal, closes both gaps; the check itself is unchanged.

Verified locally: vitest run src/lib/themes/consumed-keys.test.ts is green on theme.css as it stands (34 document-level declarations, 27 body-level, no misses — identical to before), and the mutations below all passed under the old version and fail under this one.

Mutation checks

Each check appends a block to lib/src/theme.css declaring a var()-bound token that is never mirrored onto body, then runs the test:

@theme inline {
  --color-new-thing: var(--vscode-badge-background);
}

The guard must fail with AssertionError: expected [ '--color-new-thing' ] to deeply equal []. Results by block form:

block before after
@theme inline skipped caught
@theme static skipped caught
@theme default skipped caught
@theme static inline skipped caught
@theme reference skipped caught
second :root { --mt-extra: var(--vscode-font-size); } skipped caught

("skipped" = the test stays green because the declaration never enters the map.)

Tailwind v4's @theme takes four modifiers — inline, static, reference, default — and they combine, so @theme static inline { ... } is valid. Matching the header with @theme\b[^{}\n]*\{ covers all of them without enumerating. The [^{}\n]* bound matters twice: it stops the run from swallowing a following @theme block when there are two (the case matchAll is here for), and it keeps the two prose mentions of @theme in theme.css's file-header comment from matching, since neither line contains a {.

@theme reference emits no custom properties, so a var()-bound token inside one arguably needn't be mirrored and this will flag it. That trade is deliberate: a false failure is loud, names the token, and is one line to fix, while the current behavior is a skip nobody sees.

Every package in the workspace is on tailwindcss ^4.3.0, where more than one @theme block — and the modifier forms — are ordinary. No spec change: docs/specs/theme.md already states the invariant this enforces, and the invariant is unchanged.

The mirror guard added in #478 located its blocks with themeCss.match(),
which stops at the first hit, and matched the literal '@theme {'. A second
@theme or :root block was therefore read past in silence, and Tailwind v4's
'@theme inline { ... }' never matched at all -- the form you reach for when
a token's value is a var() chain, which is exactly what the guard checks.

matchAll over global patterns closes both gaps. Verified: green on
theme.css as it stands (34 document-level declarations, 27 body-level);
appending '@theme inline { --color-new-thing: var(--vscode-badge-background); }'
or a second ':root { --mt-extra: var(--vscode-font-size); }' passed before
and fails now.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 29, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 75add88
Status:🚫  Build failed.

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The matchAll switch is right and I verified the "identical to before" claim — replaying both the old and new logic against theme.css gives 34 document-level and 27 body-level declarations either way, with no misses.

One gap survives, and it is the same silent-skip the PR exists to close. Tailwind v4's @theme takes four modifiers — inline, static, reference, default — and they combine, so @theme static inline { ... } is valid. The (?: inline| static)? alternation covers two of the four and none of the combinations, so @theme default { ... } and @theme static inline { ... } are still read past in silence. Replaying the new pattern against theme.css plus an appended block declaring --color-new-thing: var(--vscode-badge-background):

block new pattern @theme\b[^{}\n]*\{
@theme inline caught caught
@theme static caught caught
@theme default skipped caught
@theme static inline skipped caught
@theme reference skipped caught

The inline suggestion swaps the enumeration for a bounded wildcard. [^{}\n]* rather than [^{]* matters twice: it stops the greedy run from swallowing a following @theme block when there are two (the exact case this PR adds matchAll for), and it keeps the two prose mentions of @theme in the file header comment from matching, since neither line contains a {. Baseline is unchanged under it — still 34/27, still no misses.

@theme reference is the one judgment call: it emits no custom properties, so a var()-bound token inside one arguably needn't be mirrored, and the wildcard would flag it. That trade seems right anyway — a false failure is loud and one edit away, while the current behavior is a skip nobody sees.

Comment thread lib/src/lib/themes/consumed-keys.test.ts Outdated
Tailwind v4 @theme takes four combinable modifiers (inline, static,
reference, default), so the (?: inline| static)? alternation still read
@theme default and @theme static inline past in silence -- the same
silent-skip this guard exists to close. Match the header with a bounded
wildcard instead.
@dormouse-bot

Copy link
Copy Markdown
Collaborator Author

Build & Test went red on this commit for a reason outside the diff, and is green again after a rerun of the failed job — no code change was needed.

pnpm install got a GitHub 504 fetching the xterm.js fork tarball and exhausted its retries:

[WARN] GET https://github.com/diffplug/xterm.js/releases/download/sdf-v0.20.0-sdf301.1/diffplug-xterm-addon-webgl-sdf-0.20.0-sdf301.1.tgz error (ERR_PNPM_FETCH_504). Will retry in 10 seconds. 2 retries left.
[ERR_PNPM_FETCH_504] GET https://.../diffplug-xterm-addon-webgl-sdf-0.20.0-sdf301.1.tgz: Gateway Time-out - 504

The one remaining red is Cloudflare Pages, which I can't rerun — it builds on push, and its logs live in the Cloudflare dashboard rather than in Actions. It's very likely the same outage rather than anything in this PR: it started at 17:11:58Z, inside the same window as the 504 retries above (17:11:19Z–17:12:29Z), it's the only Cloudflare build to fail across the last six PRs, and this diff touches one test file, which the website build doesn't consume. Not verified from its log, though — a rebuild would settle it.

Evidence

@nedtwigg
nedtwigg merged commit 68c6243 into main Aug 29, 2026
17 of 19 checks passed
@nedtwigg
nedtwigg deleted the fix/theme-mirror-guard-all-blocks branch August 29, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants