Skip to content

fix: handle @scope at-rule edge cases#91

Open
merklebonsai wants to merge 3 commits intocss-modules:masterfrom
merklebonsai:fix/scope-params-tokenizer
Open

fix: handle @scope at-rule edge cases#91
merklebonsai wants to merge 3 commits intocss-modules:masterfrom
merklebonsai:fix/scope-params-tokenizer

Conversation

@merklebonsai
Copy link
Copy Markdown

@merklebonsai merklebonsai commented Apr 29, 2026

Two related fixes for the @scope at-rule handler. Two atomic commits.

Commit 1 — fix: parse @scope params with paren-aware tokenizer

Fixes #90.

The current @scope handler splits atRule.params on the substring "to":

atRule.params.split("to")

This naively matches to anywhere in the params, including inside class names (.button contains bu + to + n; same goes for .tooltip, .toolbar, .photo, .story, .into-view, etc.) and inside attribute selector values ([data-section="footer"], [role="button"]). The result is malformed CSS with extra to () clauses and class names truncated mid-token, that never match the DOM.

The fix replaces .split("to") with parseScopeParams — a small character-by-character tokenizer that walks the params with paren-depth and string-literal tracking. The to keyword is detected only at depth 0, between two parenthesized groups, with a word boundary check so identifiers starting with to (like tools, topology) don't match.

The same tokenizer also handles two adjacent legal-CSS edge cases that the old split("to") would have mishandled regardless of the substring issue:

  • CSS comments inside params (/* something with parens */)
  • CSS identifier escapes (\(, \)) — legal per the CSS spec, occasionally emitted by CSS-in-JS pipelines

When params can't be parsed, the plugin now emits result.warn(...) via the postcss API so module-name leakage is diagnosable rather than silent.

Commit 2 — fix: prevent crash on body-less @scope at-rule

The @scope branch in the at-rule walker calls atRule.nodes.forEach(...) unconditionally. When postcss parses an
@scope at-rule with no body (e.g. @scope (.foo);), atRule.nodes is undefined and .forEach throws Cannot read properties of undefined (reading 'forEach').

The non-scope at-rule branch already handles this with else if (atRule.nodes). This commit adds the same guard to the @scope branch.

Why we hit this

We're shipping a visual app builder that emits CSS with @scope rules where component-boundary markers can land in any of the params positions, and component names are user-supplied. Most names hit at least one of the failure modes (any class containing the substring to). Without these fixes, next build — which uses postcss-modules through webpack by default — silently produces broken styles in our generated output. Our codegen needs arbitrary user input to round-trip correctly through every CSS Modules mangler in the ecosystem.

Test plan

  • All 249 existing tests pass unchanged.
  • 9 new tests across both commits:
    • Class name contains to substring (canonical Bug in handling @scope rule - "to" as a part of a selector #90 case)
    • Multiple classes with to in scope-end
    • Attribute selector value contains to
    • Bare class with to inside but no scope-end clause
    • Uppercase TO keyword (CSS keywords are case-insensitive)
    • Nested :is() / :not() selectors (parser depth correctness)
    • CSS comment containing to and unbalanced parens
    • Escaped \( in identifier
    • Body-less @scope no longer crashes
  • Total: 258 passing.
  • yarn lint (eslint + prettier) clean.

@merklebonsai
Copy link
Copy Markdown
Author

Under honest disclosure - this PR is produced together with Claude. Yet, this is not a slop; more of an opposite actually. I've specifically ran Claude to identify edge cases I'm missing (e.g. comments that include ( or ) - he found several genuine ones I did not think about). I'm using @scope in automated codegen scenarios and needed to be sure it will not break under any edge cases.

Comment thread src/index.js
}

return { start, end };
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it AI generated code? because I see a lot of rooms to improve

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ask Claude to simplify it and make faster, he knows how to do it

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fair, thanks for comment - I've been aimed (and aimed Claude) at spec compliance firsthand, not primitives like postcss-value-parser reuse.

Optimized in 988304c

Tried to benchmark, and the difference is very subtle between all versions, starting with pre-commits. With 5000 iterations run, it's within few nanosecs per run (some paths are 25-26 usec, some paths are 100-102 usec). So there was almost no degradation in performance initially - and sadly, I was unable to improve it more. So, only did code quality improves.

If you have some specific code style preferences (e.g. use switch in parseScopeParams) I can do it, just need an advice on preferred shape here.

Replace the hand-rolled tokenizer with a postcss-value-parser pass
(already a dependency). Match three explicit grammar shapes that mirror
the spec: (start), to (end), and (start) to (end). The `to(...)` form
without whitespace, which value-parser tokenizes as a single function,
is normalized into [to, (...)] upfront so all spacings collapse to one
match.

Also dedupe the at-rule body iteration that was identical between the
@scope branch and the non-scope branch.

Net: -93 LOC vs the prior @scope chunk, same end-to-end performance.
@alexander-akait
Copy link
Copy Markdown
Member

We still support old nodejs, yeah I know we should drop them, but let's avoid breaking changes here

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.

Bug in handling @scope rule - "to" as a part of a selector

2 participants