Skip to content

[codemods] Convert conditional css (:hover, :focus, media) to StyleX (Emotion→StyleX, 3/6) - #1770

Closed
ahmedsadid wants to merge 14 commits into
facebook:mainfrom
ahmedsadid:codemod/emotion-m2-conditions
Closed

[codemods] Convert conditional css (:hover, :focus, media) to StyleX (Emotion→StyleX, 3/6)#1770
ahmedsadid wants to merge 14 commits into
facebook:mainfrom
ahmedsadid:codemod/emotion-m2-conditions

Conversation

@ahmedsadid

Copy link
Copy Markdown
Contributor

What changed / motivation ?

Part of migrating Emotion styles to StyleX (#1766). The previous PR (#1769) did
flat static styles; this adds the conditional ones — pseudo-classes,
pseudo-elements, and media queries — which StyleX writes as condition-keyed
values.

// before
<input css={{
  color: 'black',
  ':focus': { color: 'blue' },
  '@media (min-width: 600px)': { fontSize: 18 },
}} />

// after
const styles = stylex.create({
  field: {
    color: { default: 'black', ':focus': 'blue' },
    fontSize: { default: null, '@media (min-width: 600px)': 18 },
  },
});
<input {...stylex.props(styles.field)} />
  • Handles :hover, :focus, pseudo-elements (::before/::after), and media
    queries. :hover also gets StyleX's @media (hover: hover) guard.
  • Emotion resolves overlapping conditions by source order, StyleX by fixed
    priority. When they'd pick different winners (e.g. :focus vs :hover), the
    file is refused rather than converted with different behavior.
  • Still refuses what it can't handle safely: cross-element selectors (& > li),
    plus everything the previous PR skipped (dynamic values, shorthands, template
    literals).

Testing

  • Before/after fixtures for :hover, :focus + media, and ::before — each
    result compiles, passes lint, and renders the same CSS as the Emotion input.
  • Two "leave it unchanged" fixtures: one where Emotion and StyleX would disagree
    on the winner, one with a selector targeting a child element. Both confirm the
    file is left untouched.
  • Unit tests that run the priority-conflict check and the StyleX code generator
    on plain style objects.

Linked PR/Issues

Part of #1766 · builds on #1769

Additional Context

Stacked on #1769, but stands on its own to review.

Pre-flight checklist

  • I have read the contributing guidelines
  • Performed a self-review of my code

ahmedsadid and others added 14 commits July 23, 2026 17:03
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…te proven to fail on a broken pair

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-written IR

Source-neutral: emit is fed hand-written IR (no Emotion) so the engine is
provably library-agnostic. Emits alphabetically-sorted keys (satisfies
stylex/sort-keys with zero autofixes) and refuses conditions/duplicates/
non-static values rather than mis-emitting. styleToObjectAst added to the
rewriter wrapper is emit's AST-rendering bridge, keeping core/emit AST-free.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…orts, orchestrator

Reads static css={{...}} / css({...}) on host elements into neutral
declarations; rewrites each site to {...stylex.props(styles.key)}; inserts a
single per-file registry above the first converted component; cleans up the
@emotion/react import and @jsxImportSource pragma. Whole-file-or-nothing
(M1 policy): any blocker (pseudo/media, template literal, shorthand overlap,
component css prop, pre-existing stylex) skips the file untouched with reasons.
core/ never imported here — the seam holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CSS treats 'rgb(10, 20, 30)' and 'rgb(10,20,30)' as identical and the StyleX
compiler emits the latter; without this the gate flagged every rgb()/multi-arg
value as a false diff. Comma-whitespace is now canonicalized on both sides.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…add 8 fixtures

The two M0-skipped checks (byte-exact match, per-fixture semantic-diff) now run
for real against transformEmotionFile. Skip-fixtures assert the transform
refused loudly and left the file byte-identical. New fixtures: css-call-form,
static-values, two-sites (convert) + skip-{pseudo,template-literal,
shorthand-conflict,component-css,existing-stylex} (refuse).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…red coverage

Replaces the M0 throwing stub with a real StyleX-object -> IR reader (flat
static values + fallback arrays). The harness now round-trips each covered
corpus entry (read -> emit -> compile -> semantic-diff vs original, empty
allowlist) and reports measured coverage; condition/keyframe entries remain
typed, SAFE coverage gaps (2/5 covered at M1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Emotion emits plain CSS (browser resolves by cascade: specificity then source
order); StyleX resolves by fixed priority per condition (imported from
@stylexjs/shared, never reinvented). They disagree only among equal-specificity
conditions where CSS uses source order but StyleX uses priority. For each
property, within each pseudo-element target (different box = no competition),
the cascade order must equal the priority order — else refuse. Prevents the
silent-wrong-cascade bug class (e.g. :focus-then-:hover). Tested against
hand-written IR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
emit groups atoms by property and nests conditions into StyleX's
property-grouped shape (the inverse of Emotion's selector-grouped input),
canonicalizing nesting order and filling default: null where a condition has
no base. Hover-guard (:hover wrapped in @media (hover: hover)) is on by
default, toggleable. buildIR carries conditions + fallback-array values; the
rewriter renders nested condition objects with string-literal keys. Tested
against hand-written IR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
read walks nested condition objects into declarations carrying condition
paths — pseudo-classes, pseudo-elements, media queries and their nesting —
refusing non-self-targeting selectors, functional pseudos, and conditions
nested inside a pseudo-element. transform runs each rule through the referee
(refuse on disagreement) and threads the hoverGuard option into emit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eree & descendant refuse

Renames skip-pseudo -> hover-pseudo (now converts, hover-guarded). Adds
focus-media, pseudo-element (convert) and skip-referee-conflict (:focus before
:hover), skip-descendant-selector (refuse). Each verified across all four
harness checks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… 5/5

The test-only StyleX-object reader now parses condition-in-value objects
(pseudo/at-rule keys, nested), proving the flip round-trips from the StyleX
side (read -> emit with hoverGuard off -> compile -> semantic-diff, identity).
The whole seed corpus now round-trips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 23, 2026
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

@ahmedsadid is attempting to deploy a commit to the Meta Open Source Team on Vercel.

A member of the Team first needs to authorize it.

@ahmedsadid
ahmedsadid force-pushed the codemod/emotion-m2-conditions branch from c173be7 to 7f972fa Compare July 23, 2026 23:47
@ahmedsadid
ahmedsadid marked this pull request as ready for review July 24, 2026 00:35
@ahmedsadid
ahmedsadid requested review from mellyeliu and nmn as code owners July 24, 2026 00:35
@ahmedsadid
ahmedsadid force-pushed the codemod/emotion-m2-conditions branch from 7f972fa to 74bf550 Compare July 25, 2026 02:34
@ahmedsadid ahmedsadid closed this Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant