fix(scoping): rewrite prefix/substring composition-id selectors in place#2262
fix(scoping): rewrite prefix/substring composition-id selectors in place#2262Thomaswebstich wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Review — fix(scoping): rewrite prefix/substring composition-id selectors in place
SSOT check: clean. The fix is exactly one character class addition to the canonical composition-id regex in scopeSelector. The [\\^\\*\\$]?= pattern correctly matches ^=, *=, $= in addition to the existing =, all in one place.
Root cause is correctly identified: prefix-match selectors ([data-composition-id^="x"]) fell through to the prepend branch, creating ${scope} [data-composition-id^="x"] … — a two-level selector that needs a nested composition-id element. The loader strips the inner root's data-composition-id at mount, so nothing matches. The fix rewrites them in place, same as exact match.
The in-place replacement to exact match is correct: [data-composition-id^="scene"] → [data-composition-id="scene__hf2"]. Each instance gets its own scoped CSS copy, so exact match on the instance ID is the right semantics — the prefix match was an authoring-time workaround that the scoper now handles.
Regex backward compatibility: the ? makes the operator optional, so exact = still matches. ~= (word match) is correctly excluded — composition IDs aren't space-separated. Good.
Test: covers ^=, *=, and includes negative assertions (not.toContain) verifying the prefix/substring forms are consumed in place and never left behind a prepended scope. This is the regression guard.
Minor observation (non-blocking): normalizeCompositionRootSelector (line 142) also has a composition-id regex that only matches exact =. If a sub-comp uses [data-composition-id^="scene"][data-start="0"], the timing attributes won't be stripped — they'll persist in the scoped output as extra specificity. Functionally correct (the timing attributes exist on the element), just inconsistent with how exact = handles it. Same applies to the runtime scoping pattern at line 211. Both are pre-existing and not made worse by this PR — just noting for a potential follow-up.
|
@Thomaswebstich after addressing the review, can you rebase main pls? I'll stamp afterwards and merge it! Thanks for contributing ❤️ |
scopeSelector() only rewrote the EXACT `[data-composition-id="x"]` root
selector in place; prefix/substring forms (`^=`, `*=`, `$=`) of the authored
composition id fell through to the prepend branch, becoming a two-level
`${scope} [data-composition-id^="x"] …` selector. That needs a NESTED
composition-id element, but the loader strips the inner root's composition-id
at mount, so the rule matches nothing and the sub-comp's class-scoped styles
silently drop (class-styled elements render unstyled).
This surfaces when a sub-comp ships prefix-match root selectors — e.g. an
authoring pipeline rewrites `="x"` to `^="x"` so rules survive the
duplicate-instance rename (x -> x__hf2). In a multi-scene document each such
sub-comp then loses its styling on mount, while single-mount renders (which
keep the inner structure) are unaffected.
Match the substring operators alongside `=` so the authored-root selector is
rewritten in place to the instance scope in every form. Adds a regression test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3f182f8 to
b4a91e9
Compare
|
Rebased onto @miga-heygen thanks for the thorough review 🙏 — noted the follow-up on @Thomaswebstich ready for your stamp + merge whenever. 🤖 Addressed by Claude Code |
Problem
A sub-composition whose
<style>scopes rules by a prefix/substring composition-id selector —[data-composition-id^="x"](also*=,$=) — has those rules silently dropped when mounted in a multi-composition document.scopeSelector()(incompositionScoping.ts) rewrites the exact[data-composition-id="x"]root selector in place to the instance scope, but any prefix/substring form falls through to the prepend branch and becomes a two-level selector:That now requires a nested composition-id element — but
compositionLoaderstrips the inner root'sdata-composition-idat mount, so nothing matches and every class-scoped rule in that sub-comp stops applying (class-styled elements render unstyled). Single-mount renders keep the inner structure, so they're unaffected — the drop only shows when the loader renames/flattens (e.g. duplicate instances in one document).This bites any authoring pipeline that rewrites
="x"to^="x"so a sub-comp's own root rules survive the duplicate-instance rename (x→x__hf2).Fix
Widen the composition-id match to accept the substring operators (
^=,*=,$=) of the authored id, so the authored-root selector is rewritten in place to the instance scope in every form — exactly like the exact=case — instead of being prepended.One-line change to the selector regex, plus a regression test covering
^=/*=/$=.Test
packages/core/src/compiler/compositionScoping.test.ts— new case asserts the prefix/substring forms are consumed in place (and never left intact behind a prepended scope). Full scoping suite passes.🤖 Generated with Claude Code