11import type { API } from "jscodeshift" ;
22import { resolveDynamicNode } from "./builtin-handlers.js" ;
3- import { cssDeclarationToStylexDeclarations } from "./css-prop-mapping.js" ;
3+ import { cssDeclarationToStylexDeclarations , cssPropertyToStylexProp } from "./css-prop-mapping.js" ;
44import { getMemberPathFromIdentifier , getNodeLocStart } from "./jscodeshift-utils.js" ;
55import type { ImportSource } from "../adapter.js" ;
66import {
@@ -674,6 +674,54 @@ export function lowerRules(args: {
674674
675675 // Now treat the interpolated portion as `borderColor`.
676676 const expr = decl . templateExpressions [ slotId ] as any ;
677+
678+ // Helper to parse a border shorthand string and return expanded properties
679+ const parseBorderShorthand = (
680+ value : string ,
681+ ) : { borderWidth ?: string ; borderStyle ?: string ; borderColor ?: string } | null => {
682+ const tokens = value . trim ( ) . split ( / \s + / ) ;
683+ const borderStylesSet = new Set ( [
684+ "none" ,
685+ "solid" ,
686+ "dashed" ,
687+ "dotted" ,
688+ "double" ,
689+ "groove" ,
690+ "ridge" ,
691+ "inset" ,
692+ "outset" ,
693+ ] ) ;
694+ const looksLikeLengthLocal = ( t : string ) =>
695+ / ^ - ? \d * \. ? \d + ( p x | r e m | e m | v h | v w | v m i n | v m a x | % ) ? $ / . test ( t ) ;
696+
697+ let bWidth : string | undefined ;
698+ let bStyle : string | undefined ;
699+ const colorParts : string [ ] = [ ] ;
700+ for ( const token of tokens ) {
701+ if ( ! bWidth && looksLikeLengthLocal ( token ) ) {
702+ bWidth = token ;
703+ } else if ( ! bStyle && borderStylesSet . has ( token ) ) {
704+ bStyle = token ;
705+ } else {
706+ colorParts . push ( token ) ;
707+ }
708+ }
709+ const bColor = colorParts . join ( " " ) . trim ( ) ;
710+ // If we found at least width or style, this is a border shorthand
711+ if ( bWidth || bStyle ) {
712+ return {
713+ ...( bWidth ? { borderWidth : bWidth } : { } ) ,
714+ ...( bStyle ? { borderStyle : bStyle } : { } ) ,
715+ ...( bColor ? { borderColor : bColor } : { } ) ,
716+ } ;
717+ }
718+ // Just a color value
719+ if ( bColor ) {
720+ return { borderColor : bColor } ;
721+ }
722+ return null ;
723+ } ;
724+
677725 if ( expr ?. type === "ArrowFunctionExpression" && expr . body ?. type === "ConditionalExpression" ) {
678726 const test = expr . body . test as any ;
679727 const cons = expr . body . consequent as any ;
@@ -684,14 +732,45 @@ export function lowerRules(args: {
684732 cons ?. type === "StringLiteral" &&
685733 alt ?. type === "StringLiteral"
686734 ) {
687- // Default to alternate; conditionally apply consequent.
688- styleObj . borderColor = alt . value ;
735+ const altParsed = parseBorderShorthand ( alt . value ) ;
736+ const consParsed = parseBorderShorthand ( cons . value ) ;
689737 const when = test . property . name ;
690- variantBuckets . set ( when , {
691- ...variantBuckets . get ( when ) ,
692- borderColor : cons . value ,
693- } ) ;
694- variantStyleKeys [ when ] ??= `${ decl . styleKey } ${ toSuffixFromProp ( when ) } ` ;
738+ const notWhen = `!${ when } ` ;
739+
740+ // Check if either value is a full border shorthand (has width or style)
741+ const isFullShorthand =
742+ ( altParsed && ( altParsed . borderWidth || altParsed . borderStyle ) ) ||
743+ ( consParsed && ( consParsed . borderWidth || consParsed . borderStyle ) ) ;
744+
745+ if ( isFullShorthand ) {
746+ // Both branches should become variants (neither goes to base style)
747+ if ( altParsed ) {
748+ variantBuckets . set ( notWhen , {
749+ ...variantBuckets . get ( notWhen ) ,
750+ ...altParsed ,
751+ } ) ;
752+ variantStyleKeys [ notWhen ] ??= `${ decl . styleKey } ${ toSuffixFromProp ( notWhen ) } ` ;
753+ }
754+ if ( consParsed ) {
755+ variantBuckets . set ( when , {
756+ ...variantBuckets . get ( when ) ,
757+ ...consParsed ,
758+ } ) ;
759+ variantStyleKeys [ when ] ??= `${ decl . styleKey } ${ toSuffixFromProp ( when ) } ` ;
760+ }
761+ } else {
762+ // Original behavior: default to alternate, conditionally apply consequent
763+ if ( altParsed ?. borderColor ) {
764+ styleObj . borderColor = altParsed . borderColor ;
765+ }
766+ if ( consParsed ?. borderColor ) {
767+ variantBuckets . set ( when , {
768+ ...variantBuckets . get ( when ) ,
769+ borderColor : consParsed . borderColor ,
770+ } ) ;
771+ variantStyleKeys [ when ] ??= `${ decl . styleKey } ${ toSuffixFromProp ( when ) } ` ;
772+ }
773+ }
695774 return true ;
696775 }
697776 }
@@ -702,6 +781,20 @@ export function lowerRules(args: {
702781 return true ;
703782 }
704783
784+ // Handle arrow functions that are simple member expressions (like theme access):
785+ // border: 1px solid ${(props) => props.theme.colors.primary}
786+ // In this case, we modify the declaration's property to be "borderColor" so that
787+ // the generic dynamic handler (resolveDynamicNode) outputs borderColor instead of border.
788+ if ( expr ?. type === "ArrowFunctionExpression" ) {
789+ const body = expr . body as any ;
790+ // Simple arrow function returning a member expression: (p) => p.theme.colors.X
791+ if ( body ?. type === "MemberExpression" ) {
792+ // Mutate the declaration's property so fallback handlers use borderColor
793+ d . property = "border-color" ;
794+ return false ; // Let the generic handler resolve the theme value
795+ }
796+ }
797+
705798 // fallback to inline style via wrapper
706799 if ( decl . shouldForwardProp ) {
707800 inlineStyleProps . push ( {
@@ -1129,7 +1222,7 @@ export function lowerRules(args: {
11291222 }
11301223
11311224 // Handle computed theme object access keyed by a prop:
1132- // background-color: ${(props) => props.theme.color [props.bg]}
1225+ // background-color: ${(props) => props.theme.colors [props.bg]}
11331226 //
11341227 // If the adapter can resolve `theme.color` as an object expression, we can emit a StyleX
11351228 // dynamic style function that indexes into that resolved object at runtime:
@@ -1382,7 +1475,10 @@ export function lowerRules(args: {
13821475 const neg = res . variants . find ( ( v : any ) => v . when . startsWith ( "!" ) ) ;
13831476 const pos = res . variants . find ( ( v : any ) => ! v . when . startsWith ( "!" ) ) ;
13841477
1385- const outs = cssDeclarationToStylexDeclarations ( d ) ;
1478+ const cssProp = ( d . property ?? "" ) . trim ( ) ;
1479+ // Map CSS property to StyleX property (handle special cases like background → backgroundColor)
1480+ const stylexProp =
1481+ cssProp === "background" ? "backgroundColor" : cssPropertyToStylexProp ( cssProp ) ;
13861482
13871483 const parseResolved = (
13881484 expr : string ,
@@ -1401,17 +1497,81 @@ export function lowerRules(args: {
14011497 return { exprAst, imports : imports ?? [ ] } ;
14021498 } ;
14031499
1500+ // Helper to expand border shorthand from a string literal like "2px solid blue"
1501+ const expandBorderShorthand = (
1502+ target : Record < string , unknown > ,
1503+ exprAst : any ,
1504+ ) : boolean => {
1505+ // Handle various AST wrapper structures
1506+ let node = exprAst ;
1507+ // Unwrap ExpressionStatement if present
1508+ if ( node ?. type === "ExpressionStatement" ) {
1509+ node = node . expression ;
1510+ }
1511+ // Only expand if it's a string literal
1512+ if ( node ?. type !== "StringLiteral" && node ?. type !== "Literal" ) {
1513+ return false ;
1514+ }
1515+ const value = node . value ;
1516+ if ( typeof value !== "string" ) {
1517+ return false ;
1518+ }
1519+ const tokens = value . trim ( ) . split ( / \s + / ) ;
1520+ const BORDER_STYLES = new Set ( [
1521+ "none" ,
1522+ "solid" ,
1523+ "dashed" ,
1524+ "dotted" ,
1525+ "double" ,
1526+ "groove" ,
1527+ "ridge" ,
1528+ "inset" ,
1529+ "outset" ,
1530+ ] ) ;
1531+ const looksLikeLength = ( t : string ) =>
1532+ / ^ - ? \d * \. ? \d + ( p x | r e m | e m | v h | v w | v m i n | v m a x | c h | e x | l h | % ) ? $ / . test ( t ) ;
1533+
1534+ let width : string | undefined ;
1535+ let style : string | undefined ;
1536+ const colorParts : string [ ] = [ ] ;
1537+ for ( const token of tokens ) {
1538+ if ( ! width && looksLikeLength ( token ) ) {
1539+ width = token ;
1540+ } else if ( ! style && BORDER_STYLES . has ( token ) ) {
1541+ style = token ;
1542+ } else {
1543+ colorParts . push ( token ) ;
1544+ }
1545+ }
1546+ const color = colorParts . join ( " " ) . trim ( ) ;
1547+ if ( ! width && ! style && ! color ) {
1548+ return false ;
1549+ }
1550+ if ( width ) {
1551+ target [ "borderWidth" ] = j . literal ( width ) ;
1552+ }
1553+ if ( style ) {
1554+ target [ "borderStyle" ] = j . literal ( style ) ;
1555+ }
1556+ if ( color ) {
1557+ target [ "borderColor" ] = j . literal ( color ) ;
1558+ }
1559+ return true ;
1560+ } ;
1561+
14041562 const applyParsed = (
14051563 target : Record < string , unknown > ,
14061564 parsed : { exprAst : unknown ; imports : any [ ] } ,
14071565 ) : void => {
14081566 for ( const imp of parsed . imports ) {
14091567 resolverImports . set ( JSON . stringify ( imp ) , imp ) ;
14101568 }
1411- for ( let i = 0 ; i < outs . length ; i ++ ) {
1412- const out = outs [ i ] ! ;
1413- target [ out . prop ] = parsed . exprAst as any ;
1569+ // Special handling for border shorthand with string literal values
1570+ if ( cssProp === "border" && expandBorderShorthand ( target , parsed . exprAst ) ) {
1571+ return ;
14141572 }
1573+ // Default: use the property from cssDeclarationToStylexDeclarations
1574+ target [ stylexProp ] = parsed . exprAst as any ;
14151575 } ;
14161576
14171577 // IMPORTANT: stage parsing first. If either branch fails to parse, skip this declaration entirely
0 commit comments