@@ -17,16 +17,73 @@ export type DynamicNode = {
1717} ;
1818
1919export type HandlerResult =
20- | { type : "resolvedValue" ; expr : string ; imports : ImportSpec [ ] }
21- | { type : "emitInlineStyle" ; style : string }
2220 | {
21+ /**
22+ * The node was resolved to a JS expression string that can be directly inlined into
23+ * generated output (typically for a single CSS property value).
24+ *
25+ * Example: `props.theme.colors.bgBase` -> `themeVars.bgBase`
26+ *
27+ * The caller is responsible for:
28+ * - parsing `expr` into an AST
29+ * - adding `imports`
30+ */
31+ type : "resolvedValue" ;
32+ expr : string ;
33+ imports : ImportSpec [ ] ;
34+ }
35+ | {
36+ /**
37+ * Emit a wrapper inline style from a raw CSS string snippet.
38+ *
39+ * This is intentionally narrow and primarily used for keeping runtime parity
40+ * when the codemod cannot safely lower to StyleX (e.g. complex dynamic blocks).
41+ */
42+ type : "emitInlineStyle" ;
43+ style : string ;
44+ }
45+ | {
46+ /**
47+ * Preserve the dynamic value by emitting a wrapper inline style:
48+ * style={{ ..., prop: expr(props) }}
49+ *
50+ * This is used for cases where we can't (or don't want to) lower into StyleX
51+ * buckets, but can safely keep parity with styled-components at runtime.
52+ */
53+ type : "emitInlineStyleValueFromProps" ;
54+ }
55+ | {
56+ /**
57+ * Emit a StyleX style function keyed off a single JSX prop.
58+ *
59+ * The caller uses this to generate a helper like:
60+ * const styles = stylex.create({
61+ * boxShadowFromProp: (shadow) => ({ boxShadow: shadow })
62+ * })
63+ *
64+ * And apply it conditionally in the wrapper:
65+ * shadow != null && styles.boxShadowFromProp(shadow)
66+ */
2367 type : "emitStyleFunction" ;
2468 nameHint : string ;
2569 params : string ;
2670 body : string ;
2771 call : string ;
72+ /**
73+ * Optional value transform to apply to the param before assigning to the style prop.
74+ * This allows supporting patterns like:
75+ * box-shadow: ${(props) => shadow(props.shadow)};
76+ * by emitting a style function that computes: `shadow(value)`.
77+ */
78+ valueTransform ?: { kind : "call" ; calleeIdent : string } ;
2879 }
2980 | {
81+ /**
82+ * Split a dynamic interpolation into one or more variant buckets.
83+ *
84+ * Each variant contains a static StyleX-style object. The caller is responsible for
85+ * wiring these into `stylex.create(...)` keys and applying them under the `when` condition.
86+ */
3087 type : "splitVariants" ;
3188 variants : Array < {
3289 nameHint : string ;
@@ -47,7 +104,16 @@ export type HandlerResult =
47104 imports : ImportSpec [ ] ;
48105 } > ;
49106 }
50- | { type : "keepOriginal" ; reason : string } ;
107+ | {
108+ /**
109+ * Signal that this handler does not know how to transform the node.
110+ *
111+ * The caller typically falls back to other strategies (or drops the declaration)
112+ * and may surface `reason` as a warning.
113+ */
114+ type : "keepOriginal" ;
115+ reason : string ;
116+ } ;
51117
52118export type InternalHandlerContext = {
53119 api : API ;
@@ -503,6 +569,138 @@ function tryResolveConditionalCssBlock(node: DynamicNode): HandlerResult | null
503569 return null ;
504570}
505571
572+ function tryResolveConditionalCssBlockTernary ( node : DynamicNode ) : HandlerResult | null {
573+ const expr = node . expr ;
574+ if ( ! isArrowFunctionExpression ( expr ) ) {
575+ return null ;
576+ }
577+ const paramName = getArrowFnSingleParamName ( expr ) ;
578+ if ( ! paramName ) {
579+ return null ;
580+ }
581+ if ( expr . body . type !== "ConditionalExpression" ) {
582+ return null ;
583+ }
584+
585+ // Support patterns like:
586+ // ${(props) => (props.$dim ? "opacity: 0.5;" : "")}
587+ const test = expr . body . test as any ;
588+ if ( ! test || test . type !== "MemberExpression" ) {
589+ return null ;
590+ }
591+ const testPath = getMemberPathFromIdentifier ( test , paramName ) ;
592+ if ( ! testPath || testPath . length !== 1 ) {
593+ return null ;
594+ }
595+ const when = testPath [ 0 ] ! ;
596+
597+ const consText = literalToString ( expr . body . consequent ) ;
598+ const altText = literalToString ( expr . body . alternate ) ;
599+ if ( consText === null || altText === null ) {
600+ return null ;
601+ }
602+
603+ const consStyle = consText . trim ( ) ? parseCssDeclarationBlock ( consText ) : null ;
604+ const altStyle = altText . trim ( ) ? parseCssDeclarationBlock ( altText ) : null ;
605+ if ( ! consStyle && ! altStyle ) {
606+ return null ;
607+ }
608+
609+ const variants : Array < { nameHint : string ; when : string ; style : Record < string , unknown > } > = [ ] ;
610+ if ( altStyle ) {
611+ variants . push ( { nameHint : "falsy" , when : `!${ when } ` , style : altStyle } ) ;
612+ }
613+ if ( consStyle ) {
614+ variants . push ( { nameHint : "truthy" , when, style : consStyle } ) ;
615+ }
616+ return { type : "splitVariants" , variants } ;
617+ }
618+
619+ function tryResolveArrowFnCallWithSinglePropArg ( node : DynamicNode ) : HandlerResult | null {
620+ if ( ! node . css . property ) {
621+ return null ;
622+ }
623+ const expr = node . expr as any ;
624+ if ( ! isArrowFunctionExpression ( expr ) ) {
625+ return null ;
626+ }
627+ const paramName = getArrowFnSingleParamName ( expr ) ;
628+ if ( ! paramName ) {
629+ return null ;
630+ }
631+ const body = expr . body as any ;
632+ if ( ! body || body . type !== "CallExpression" ) {
633+ return null ;
634+ }
635+ // Only support: helper(props.foo)
636+ if ( body . callee ?. type !== "Identifier" || typeof body . callee . name !== "string" ) {
637+ return null ;
638+ }
639+ const calleeIdent = body . callee . name as string ;
640+ const args = body . arguments ?? [ ] ;
641+ if ( args . length !== 1 ) {
642+ return null ;
643+ }
644+ const arg0 = args [ 0 ] as any ;
645+ if ( ! arg0 || arg0 . type !== "MemberExpression" ) {
646+ return null ;
647+ }
648+ const path = getMemberPathFromIdentifier ( arg0 , paramName ) ;
649+ if ( ! path || path . length !== 1 ) {
650+ return null ;
651+ }
652+ const propName = path [ 0 ] ! ;
653+
654+ return {
655+ type : "emitStyleFunction" ,
656+ nameHint : `${ sanitizeIdentifier ( node . css . property ) } FromProp` ,
657+ params : "value: any" ,
658+ body : `{ ${ Object . keys ( styleFromSingleDeclaration ( node . css . property , "value" ) ) [ 0 ] } : value }` ,
659+ call : propName ,
660+ valueTransform : { kind : "call" , calleeIdent } ,
661+ } ;
662+ }
663+
664+ function tryResolveInlineStyleValueForConditionalExpression (
665+ node : DynamicNode ,
666+ ) : HandlerResult | null {
667+ // Conservative fallback for value expressions we can't safely resolve into StyleX
668+ // buckets/functions, but can preserve via a wrapper inline style.
669+ if ( ! node . css . property ) {
670+ return null ;
671+ }
672+ const expr : any = node . expr as any ;
673+ if ( ! isArrowFunctionExpression ( expr ) ) {
674+ return null ;
675+ }
676+ if ( expr . body ?. type !== "ConditionalExpression" ) {
677+ return null ;
678+ }
679+ // IMPORTANT: do not attempt to preserve `props.theme.* ? ... : ...` via inline styles.
680+ // StyleX output does not have `props.theme` at runtime (styled-components injects theme via context),
681+ // so this would produce incorrect output unless a project-specific hook (e.g. useTheme()) is wired in.
682+ //
683+ // Treat these as unsupported so the caller can bail and surface a warning.
684+ {
685+ const paramName = getArrowFnSingleParamName ( expr ) ;
686+ const test = expr . body . test as any ;
687+ const testPath =
688+ paramName && test ?. type === "MemberExpression"
689+ ? getMemberPathFromIdentifier ( test , paramName )
690+ : null ;
691+ if ( testPath && testPath [ 0 ] === "theme" ) {
692+ return {
693+ type : "keepOriginal" ,
694+ reason :
695+ "Theme-dependent conditional values require a project-specific theme source (e.g. useTheme()); cannot safely preserve." ,
696+ } ;
697+ }
698+ }
699+ // Signal to the caller that we can preserve this declaration as an inline style
700+ // by calling the function with `props`.
701+ return { type : "emitInlineStyleValueFromProps" } ;
702+ }
703+
506704function tryResolvePropAccess ( node : DynamicNode ) : HandlerResult | null {
507705 if ( ! node . css . property ) {
508706 return null ;
@@ -549,8 +747,11 @@ export function resolveDynamicNode(
549747 tryResolveThemeAccess ( node , ctx ) ??
550748 tryResolveCallExpression ( node , ctx ) ??
551749 tryResolveConditionalValue ( node , ctx ) ??
750+ tryResolveConditionalCssBlockTernary ( node ) ??
552751 tryResolveConditionalCssBlock ( node ) ??
553- tryResolvePropAccess ( node )
752+ tryResolveArrowFnCallWithSinglePropArg ( node ) ??
753+ tryResolvePropAccess ( node ) ??
754+ tryResolveInlineStyleValueForConditionalExpression ( node )
554755 ) ;
555756}
556757
0 commit comments