feat(transform): fold static JSX past opaque spreads and styled() chains - #3699
Merged
Conversation
A spread the extractor already folded into the element's styles still
counted as a runtime spread, so an element like
`<styled.div {...baseStyle} css={x} />` bailed even though nothing dynamic
was left. Recognize the resolved identifier and member spreads and let the
element rewrite.
`StyleSpread::Open` now carries the span of the spread that produced it, so
a later pass can tell which source spread is opaque rather than only that
one is.
…read
`<styled.button {...props} type="button" css={x} />` bailed entirely because
of the spread, so every style source on it was resolved per render. The
factory and the spread now stay — style-prop splitting and DOM filtering
still happen for whatever the spread turns out to hold — while every style
Panda can see collapses into one className.
Requires a bare identifier spread, exactly one opaque spread, no style
source ahead of it, and no explicit className, so the precedence the
factory gives them is reproducible. The class read is optional-chained
because a nullish spread is a legal no-op in JSX.
`const Button = styled('button', { base: … })` renders through forwardRef,
which costs a component level per instance even when the class never
changes. When the chain is base-only and rooted in an intrinsic tag, every
`<Button>` folds to `<button className="…">` and the definition is left for
the bundler to drop.
A tag folds only when it resolves to the symbol the chain declared, so a
local binding that shadows the module-level one is untouched — the same
check the imported-tag branch beside it already makes.
Variants, an options argument, a non-local base, `let`/`var`, or a
declaration inside a function all keep the runtime chain.
🦋 Changeset detectedLatest commit: 43e68b3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 21 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two folds that remove per-render work from JSX, plus the spread fix that unblocks them.
An element whose spreads are all style-only can be rewritten
A spread the extractor already folded into the element's styles still counted as a runtime spread, so this bailed even though nothing dynamic was left:
Resolved identifier and member spreads are now recognized, and the element rewrites to a class string.
The static half of an element with
{...props}is precomputedAn opaque spread used to bail the whole element, which is costly in the common wrapper shape where everything else is a module const:
The factory and the spread stay, so style-prop splitting and DOM filtering still happen for whatever the spread holds. Everything Panda can see collapses into one
className.Requires a bare identifier spread, exactly one opaque spread, no style source ahead of it, and no explicit
className— the conditions under which the factory's precedence is reproducible. The class read is optional-chained, because{...props}with a nullish value is a legal no-op in JSX andprops.classNamewould throw.A same-file
styled()chain folds to its host elementstyled()renders throughforwardRef, which costs a component level per instance even when the class never changes. When the chain is base-only and rooted in an intrinsic tag, the sites fold and the definition is left for the bundler to drop.A tag folds only when it resolves to the symbol the chain declared, so a local binding that shadows the module-level one is untouched — the same check the imported-tag branch beside it already makes. Variants, an options argument, a non-local base,
let/var, or a declaration inside a function all keep the runtime chain.Tests
27 new cases in
pandacss_project, roughly half of them negative — the shapes that must not fold. Two were written for the two judgement calls in this PR specifically:{...props}spread still yieldsprops?.classNameconst Buttonshadowing a module-level chain keeps its own identity and gains no classVerified locally
Against a binary built from this branch:
cargo nextest run --workspace— 2288 passedpnpm --filter @pandacss/compiler test— 533 passedpnpm --filter sandbox-codegen test— all 11 framework scenarioscargo fmt --checkandclippy -D warningsclean🤖 Generated with Claude Code
https://claude.ai/code/session_01GVzc85Z4YGMGwQz2FnEfFo