codemods] Partial conversion — flag unsupported spots, merge into existing StyleX (Emotion→StyleX, 5/6)#1772
Open
ahmedsadid wants to merge 32 commits into
Open
Conversation
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>
Maps inline-axis physical properties to logical (marginLeft->marginInlineStart,
left->insetInlineStart, ...) — a doc-sanctioned RTL behavior change covered by
the semantic-diff physical->logical allowlist and matching StyleX's own
preference. Block-axis (top/bottom) stays physical (no RTL flip). On by
default, { logicalProperties: false } to disable. Tested against hand-written IR.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e output verifyAndFix with all @stylexjs/eslint-plugin rules at error, so key ordering and shorthands match StyleX's canonical form instead of a heuristic we maintain (StyleX sort-keys is not alphabetical). Residual unfixable errors -> caller refuses the file. File-wide is safe now (a converted file has no pre-existing user stylex.create); M4 adds scoping. The semantic-diff gate runs on this output, catching any sort-keys reorder that changes rendering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A property with >=2 sibling at-rule (e.g. @media) conditions sharing a pseudo context becomes sibling media keys; sort-keys reorders them but the compiler treats sibling media order as semantic, and the per-coordinate semantic-diff gate cannot see it. Refuse (never mis-emit) until the upstream inconsistency is resolved. Verified empirically that verifyAndFix does reorder such keys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t) into transform Threads logicalProperties + hoverGuard options; refuses on postprocess residual errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dia refuses Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… longhands Emotion writes 'margin: 8px 16px' as one declaration; the codemod emits StyleX's expanded logical form (marginBlock/marginInline). Compared by property name these read as a diff even though they render identically. So before diffing, both sides' margin/padding shorthands (and their logical variants) are expanded to the four physical per-side longhands, resolving logical->physical in LTR. This unblocks multi-value shorthand conversion, which the transform already performs via the postprocess autofix. inset/border keep the allowlist path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
IR Value gains a 'reference' variant (a bare identifier, e.g. animationName pointing at a keyframes var). emit renders it as a $$ref sentinel and emits KeyframesRule -> named frame objects (reusing the property-grouping machinery); the rewriter renders $$ref as an identifier. normalize maps physical->logical inside frames too. Tested against hand-written IR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Detects 'const NAME = keyframes({...})' from @emotion/react, reads its frames,
rewrites to stylex.keyframes, and allows 'animationName: NAME' as a preserved
identifier reference in css props. Import/pragma cleanup now strips 'keyframes'
alongside 'css'. Non-object/tagged-template/reassigned keyframes are refused.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… generated animation-name The animation-name hash differs between Emotion and StyleX, so the gate skips @Keyframes rules in the per-property net CSS, extracts their frame contents separately (keyframesFromStylexMetadata), and the harness compares them as an order-independent multiset against Emotion's serialized frames. animation-name present only on the after side is allowlisted (it references converted keyframes; a literal animationName stays diffed). New fixture: keyframes-spin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Records why token->defineVars is deferred to the config milestone: it needs module-resolution gate config + multi-file fixtures (M6 infra), and a token's value is external to the file so the semantic-diff cannot verify it (the project's first trusted transformation). M3 is complete with a/b/c. Also documents the trusted-transformation boundary as a principle and refreshes the README converts/refuses table + status. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t reservedKeys postprocess gains an excludeRules option (used to drop no-unused when linting a standalone snippet whose styles are used elsewhere). emit gains reservedKeys so generated style-name keys avoid colliding with a pre-existing registry's keys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Postprocess now fixes ONLY the emitted stylex (built as a standalone snippet, fixed, then the fixed objects spliced back), so a user's pre-existing stylex is never linted or reordered. A pre-existing 'import * as stylex' + stylex.create is now a MERGE target (detectExistingRegistry): our fixed style entries are appended to it, keys de-duped, no second registry/import. A final whole-file lint VERIFY (not fix) refuses a merge whose user code is lint-dirty rather than silently rewriting it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unit tests prove the scoping guarantee: merge appends to a pre-existing registry leaving the user entry verbatim; a lint-dirty user registry is refused (never silently reordered); colliding keys get a numeric suffix. skip-existing- stylex renamed to merge-existing-registry (now converts). The fixture harness's semantic-diff before-side now also compiles the INPUT's pre-existing stylex, so merged output verifies correctly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
core/todos.js owns the TODO marker text, canonical REASONS, and the re-run marker check. rewriter gains jsxComment(): a braced JSX comment node (parsed so recast reprints it correctly) — a bare block comment would render as text. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…le-file Each css site now converts or is flagged with a // TODO(stylex-migration): … marker; the file is no longer skipped because of one unconvertible site. detectSites classifies each css prop convertible/flagged; readSite and the per-rule referee flag their own sites. Emotion wiring is kept where still used (a flagged css prop keeps the pragma; a still-referenced css/keyframes keeps its import — the reference check excludes member-property/object-key positions). A re-run guard leaves already-flagged sites alone. Genuinely file-level issues (non-namespace stylex import, ≥2 registries, unconvertible keyframes) still refuse the whole file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
skip-* fixtures renamed flag-* (they now convert what they can and flag the rest in place). New: flag-mixed (convert two sites, flag a referee conflict between them), refuse-non-namespace-stylex (a genuine whole-file refusal). flagging-test.js proves the re-run guard (no double-flag), whole-file refusal still fires, and a clean file reports no flags. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…refs)
The reason strings land as comments in the user's committed source, so they
must be short, plain, and timeless. Removed the codemod-version/milestone
references ('(v1.1)', 'needs the M2 referee', 'not analyzable yet') and dropped
three unused REASONS entries. Anything shipped-later belongs in the dry-run
report (M6), not in a committed comment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@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. |
Open
2 tasks
ahmedsadid
force-pushed
the
codemod/emotion-m5-bail-loudly
branch
from
July 23, 2026 23:47
3e53498 to
385a988
Compare
ahmedsadid
marked this pull request as ready for review
July 24, 2026 00:35
ahmedsadid
force-pushed
the
codemod/emotion-m5-bail-loudly
branch
from
July 25, 2026 02:34
385a988 to
988af79
Compare
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.
What changed / motivation ?
Part of migrating Emotion styles to StyleX (#1766). Until now the codemod was
all-or-nothing per file: one thing it couldn't handle and it left the whole file
untouched. That's too conservative for real code, where a file usually has a few
tricky spots among many easy ones. This PR makes it convert what it safely can
and leave the rest clearly marked.
TODO(stylex-migration: …)comment,with the reason, on each site it can't — instead of skipping the whole file.
existing
stylex.create({ … })block, adding keys without touching the oldones. Only the code it adds gets reformatted.
uses StyleX through named imports (
import { create }) rather than thenamespace form (
import * as stylex) the tool expects.Testing
converted output still compiles, passes lint, and renders the same CSS.
stylex.createwithout changing its existing keys.
unsupported case (priority conflict, shorthand conflict, template literal,
custom-component
css, cross-element selector, order-dependent media) leaves aTODO and converts everything around it.
Linked PR/Issues
Part of #1766 · builds on #1771
Additional Context
Stacked on #1771, but stands on its own to review.
Pre-flight checklist