[babel-plugin] Fix corrupted class names from null coercion in dynamic styles - #1731
henryqdineen wants to merge 5 commits into
Conversation
…c styles
When a dynamic style has multiple conditional class names for a single
CSS property (e.g. multiple media queries or pseudo-classes), the
generated code joins them with `+`. If any condition evaluates to `null`
or `undefined` at runtime, JavaScript coerces it to the string "null" or
"undefined", producing corrupted class names like "nullxafpxz5".
The fix changes the else branch of the null-check ternary from `expr` to
`t.stringLiteral('')`. This is a regression from 7a1ed95 which replaced
the original `""` with `expr` during a refactor.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@henryqdineen is attempting to deploy a commit to the Meta Open Source Team on Vercel. A member of the Team first needs to authorize it. |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| const styles = { | ||
| color: color => [{ | ||
| "--__hashed_var__1jqb1tb": color != null ? "__hashed_var__1w8wjxo" : color, | ||
| "--__hashed_var__1jqb1tb": color != null ? "__hashed_var__1w8wjxo" : "", |
There was a problem hiding this comment.
This is a small regression because the reason the fallback value was color is to account for the way undefined vs null is handled by stylex. null means intentionally unsetting a value. undefined is to ignored and the previously set value wins. Here all nullish values are represented as "" which behaves like null.
There was a problem hiding this comment.
Perhaps what we need here is:
color !== undefined && {"--__hashed_var__1jqb1tb": color != null ? "__hashed_var__1w8wjxo" : "", $$css: true}This way, we can skip the object for undefined entirely.
There was a problem hiding this comment.
Ah, thanks for catching that! I worked through it with Claude to implement your suggestion.
From Claude 🤖 :
Implemented as suggested: a property that resolves to a single conditional class is now emitted as
expr !== undefined && { … }. When the value isundefinedthe object is skipped (styleq dropsfalsestyles) so the previously set value wins; when it'snullthe class still resolves to""to unset the slot. That restores theundefinedvsnulldistinction the""-only fallback had flattened. So(color) => ({ color })now compiles to:color => [color !== undefined && { kMwMTN: color != null ? "…" : "", $$css: true }, { "--x-color": … }]Two things shaped the approach:
Why not just restore the original
exprfallback? Withcolor != null ? cls : color, passingundefinedleaves a literalundefinedas a value in the$$cssobject — and styleq errors on that (console.error(… is not "string" or "null")), since it only accepts a string ornullthere. Guarding the object with!== undefinedmeansundefinednever ends up inside it.Why one guarded object per property, not one around the whole style? When two variables drive the same style, a single shared guard is wrong — it would drop a set value whenever the other is
undefined. So each property is guarded on its own value:// (bg, oc) => ({ backgroundColor: bg, outlineColor: oc }) [bg !== undefined && { kWkggS: … }, oc !== undefined && { kjBf7l: … }, { … }](Multi-class concatenations — e.g. several media queries on one property — keep the
""fallback instead: the slot is already claimed by the sibling classes, andundefinedvsnullcan't be expressed inside a joined string anyway.)Possible followup: properties driven by the same value could be grouped into one guarded object rather than one-per-property. Left out to keep the diff focused.
— Claude
nmn
left a comment
There was a problem hiding this comment.
Either way can change the way the object is generated in the way I suggested, or we can change the runtime to fix how we merge.
The prior null-coercion fix changed the single-class fallback from `expr` to `""`. That stopped the "nullxafpxz5" corruption, but it collapsed the distinction styleq draws between `undefined` and `null`: `undefined` means "ignore this, let the previously set value win" while `null` means "intentionally unset". With `""` both behave like `null`, so passing `undefined` for a dynamic value wrongly unset the base style instead of deferring to it (raised in review by @nmn). Reverting to the `expr` fallback is not an option either: a bare `undefined` value inside a compiled ($$css) object trips styleq's `console.error(... not "string" or "null")`, since styleq only accepts `string | null` there. Instead, per @nmn's suggestion, a property that resolves to a single conditional class is emitted as its own object guarded by `expr !== undefined && { ... }`. styleq skips a `false` style entirely, so `undefined` defers to the previous value with no error, while `null` still claims the slot with `""` to unset it. Each such property becomes its own guarded object so the semantics hold independently when multiple variables drive one style. Multi-class concatenations keep the `""` fallback: the slot is already claimed by the other classes, so the distinction is both moot and unrepresentable in a joined string there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolved conflicts in three babel-plugin test files by taking main's
snapshots and regenerating them against this branch's transform, so the
only remaining diff is the intended `expr !== undefined && {...}` shape.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What changed / motivation ?
When a dynamic style has multiple conditional class names for a single CSS property (e.g. multiple media queries or pseudo-classes), the generated code joins them with
+string concatenation. If any condition evaluates tonullorundefinedat runtime, JavaScript coerces it to the string"null"or"undefined", producing corrupted class names like"nullxafpxz5".The fix changes the else branch of the null-check ternary from
exprtot.stringLiteral(''), so a non-matching condition contributes nothing to the concatenation instead of contributing"null".This is a regression from 7a1ed95 which refactored the single-class and multi-class code paths into one. The original code before that refactor already used
""for the multi-class case — the refactor replaced it withexpr, likely reasoning thatexprwould be null anyway in the else branch. That's true, butnull + "string"produces"nullstring"in JavaScript.As a side benefit, this also produces smaller output since
""replaces what was previously a duplicated copy of the original expression in the else branch (unless the expression is a single-character variable).Note that
""andnullare not identical in styleq's merge logic —nullis the intentional "unset" semantic while""is treated as an empty string value. In practice they behave the same (both claim the property slot, neither adds a real class name), and the all-null case was already broken before this fix (producing"nullnull"), so this is strictly an improvement.Linked PR/Issues
Fixes #1702
Additional Context
Existing snapshot tests updated to reflect the new
""fallback. All tests pass.Pre-flight checklist
Contribution Guidelines