@@ -466,6 +466,43 @@ export function emitWrappers(args: {
466466 return interfaces . size ( ) > 0 ;
467467 } ;
468468
469+ /**
470+ * Extends an existing interface with a base type.
471+ * Returns true if the interface was found and extended, false otherwise.
472+ */
473+ const extendExistingInterface = ( typeName : string , baseTypeText : string ) : boolean => {
474+ if ( ! emitTypes ) {
475+ return false ;
476+ }
477+ const interfaces = root . find ( j . TSInterfaceDeclaration , {
478+ id : { type : "Identifier" , name : typeName } ,
479+ } as any ) ;
480+ if ( interfaces . size ( ) === 0 ) {
481+ return false ;
482+ }
483+ // Parse the base type into a TSExpressionWithTypeArguments node
484+ const parsed = j ( `interface X extends ${ baseTypeText } {}` ) . get ( ) . node . program . body [ 0 ] as any ;
485+ const extendsClause = parsed ?. extends ?. [ 0 ] ;
486+ if ( ! extendsClause ) {
487+ return false ;
488+ }
489+ interfaces . forEach ( ( path : any ) => {
490+ const iface = path . node ;
491+ // Don't add if already extends this type
492+ const existingExtends = iface . extends ?? [ ] ;
493+ const alreadyExtends = existingExtends . some ( ( ext : any ) => {
494+ const extStr = j ( ext ) . toSource ( ) ;
495+ return extStr === baseTypeText ;
496+ } ) ;
497+ if ( alreadyExtends ) {
498+ return ;
499+ }
500+ // Add the extends clause
501+ iface . extends = [ ...existingExtends , extendsClause ] ;
502+ } ) ;
503+ return true ;
504+ } ;
505+
469506 /**
470507 * Emits a named props type alias and returns whether it was emitted.
471508 * Returns false if the type would shadow an existing type with the same name.
@@ -1656,11 +1693,15 @@ export function emitWrappers(args: {
16561693 // If there's an explicit type, extend it with base component props
16571694 const typeText = explicit ? `${ baseTypeText } & ${ explicit } ` : baseTypeText ;
16581695 const typeAliasEmitted = emitNamedPropsType ( d . localName , typeText ) ;
1659- // If the type alias was not emitted (e.g., due to shadowing), store the type
1660- // for inline annotation in the function parameter
1696+ // If the type alias was not emitted (e.g., due to shadowing), try to extend
1697+ // the existing interface with the base component props
16611698 if ( ! typeAliasEmitted && explicit ) {
1662- // Use PropsWithChildren wrapper with style for inline annotation
1663- inlineTypeText = `React.PropsWithChildren<${ explicit } & { style?: React.CSSProperties }>` ;
1699+ const propsTypeName = propsTypeNameFor ( d . localName ) ;
1700+ const extended = extendExistingInterface ( propsTypeName , baseTypeText ) ;
1701+ if ( ! extended ) {
1702+ // Fallback: use inline type annotation
1703+ inlineTypeText = `React.PropsWithChildren<${ explicit } & { style?: React.CSSProperties }>` ;
1704+ }
16641705 }
16651706 needsReactTypeImport = true ;
16661707 }
@@ -1720,6 +1761,34 @@ export function emitWrappers(args: {
17201761 }
17211762 }
17221763
1764+ // Add style function calls for dynamic prop-based styles (e.g., color: ${props => props.color})
1765+ const styleFnPairs = d . styleFnFromProps ?? [ ] ;
1766+ for ( const p of styleFnPairs ) {
1767+ const propExpr = j . identifier ( p . jsxProp ) ;
1768+ const call = j . callExpression (
1769+ j . memberExpression ( j . identifier ( "styles" ) , j . identifier ( p . fnKey ) ) ,
1770+ [ propExpr as any ] ,
1771+ ) ;
1772+ // Add prop to destructure list
1773+ if ( ! destructureProps . includes ( p . jsxProp ) ) {
1774+ destructureProps . push ( p . jsxProp ) ;
1775+ }
1776+ // Check if prop is required in the props type
1777+ const required = isPropRequiredInPropsTypeLiteral ( d . propsType , p . jsxProp ) ;
1778+ if ( required ) {
1779+ styleArgs . push ( call ) ;
1780+ } else {
1781+ // Use `!= null` so `0` / empty strings still count as "provided".
1782+ styleArgs . push (
1783+ j . logicalExpression (
1784+ "&&" ,
1785+ j . binaryExpression ( "!=" , propExpr as any , j . nullLiteral ( ) ) ,
1786+ call ,
1787+ ) ,
1788+ ) ;
1789+ }
1790+ }
1791+
17231792 // When supportsExternalStyles is true, generate wrapper with className/style merging
17241793 if ( supportsExternalStyles ) {
17251794 const isVoidTag = VOID_TAGS . has ( tagName ) ;
@@ -1735,6 +1804,8 @@ export function emitWrappers(args: {
17351804 patternProp ( "className" , classNameId ) ,
17361805 ...( isVoidTag ? [ ] : [ patternProp ( "children" , childrenId ) ] ) ,
17371806 patternProp ( "style" , styleId ) ,
1807+ // Include variant props and style function props in destructuring
1808+ ...destructureProps . map ( ( name ) => patternProp ( name ) ) ,
17381809 j . restElement ( restId ) ,
17391810 ] ;
17401811 const declStmt = j . variableDeclaration ( "const" , [
@@ -1839,10 +1910,14 @@ export function emitWrappers(args: {
18391910 const wrappedComponent = d . base . ident ;
18401911 {
18411912 const explicit = stringifyTsType ( d . propsType ) ;
1842- emitNamedPropsType (
1843- d . localName ,
1844- explicit ?? withChildren ( `React.ComponentProps<typeof ${ wrappedComponent } >` ) ,
1845- ) ;
1913+ const baseTypeText = `React.ComponentProps<typeof ${ wrappedComponent } >` ;
1914+ const typeText = explicit ? `${ baseTypeText } & ${ explicit } ` : withChildren ( baseTypeText ) ;
1915+ const typeAliasEmitted = emitNamedPropsType ( d . localName , typeText ) ;
1916+ // If the type alias was not emitted (e.g., due to shadowing), try to extend
1917+ // the existing interface with the base component props
1918+ if ( ! typeAliasEmitted && explicit ) {
1919+ extendExistingInterface ( propsTypeNameFor ( d . localName ) , baseTypeText ) ;
1920+ }
18461921 needsReactTypeImport = true ;
18471922 }
18481923 const styleArgs : any [ ] = [
@@ -1852,6 +1927,80 @@ export function emitWrappers(args: {
18521927 j . memberExpression ( j . identifier ( "styles" ) , j . identifier ( d . styleKey ) ) ,
18531928 ] ;
18541929
1930+ // Track props that need to be destructured for conditional styles
1931+ const destructureProps : string [ ] = [ ] ;
1932+
1933+ // Add variant style arguments if this component has variants
1934+ if ( d . variantStyleKeys ) {
1935+ for ( const [ when , variantKey ] of Object . entries ( d . variantStyleKeys ) ) {
1936+ // Parse the supported expression subset into AST
1937+ let cond : any = null ;
1938+ const trimmed = when . trim ( ) ;
1939+ let propName = "" ;
1940+
1941+ if ( trimmed . startsWith ( "!(" ) && trimmed . endsWith ( ")" ) ) {
1942+ const inner = trimmed . slice ( 2 , - 1 ) . trim ( ) ;
1943+ cond = j . unaryExpression ( "!" , j . identifier ( inner ) ) ;
1944+ propName = inner ;
1945+ } else if ( trimmed . startsWith ( "!" ) ) {
1946+ propName = trimmed . slice ( 1 ) ;
1947+ cond = j . unaryExpression ( "!" , j . identifier ( propName ) ) ;
1948+ } else if ( trimmed . includes ( "===" ) || trimmed . includes ( "!==" ) ) {
1949+ const op = trimmed . includes ( "!==" ) ? "!==" : "===" ;
1950+ const [ lhs , rhsRaw0 ] = trimmed . split ( op ) . map ( ( s ) => s . trim ( ) ) ;
1951+ const rhsRaw = rhsRaw0 ?? "" ;
1952+ const rhs =
1953+ rhsRaw ?. startsWith ( '"' ) || rhsRaw ?. startsWith ( "'" )
1954+ ? j . literal ( JSON . parse ( rhsRaw . replace ( / ^ ' / , '"' ) . replace ( / ' $ / , '"' ) ) )
1955+ : / ^ - ? \d + ( \. \d + ) ? $ / . test ( rhsRaw )
1956+ ? j . literal ( Number ( rhsRaw ) )
1957+ : j . identifier ( rhsRaw ) ;
1958+ propName = lhs ?? "" ;
1959+ cond = j . binaryExpression ( op , j . identifier ( propName ) , rhs ) ;
1960+ } else {
1961+ propName = trimmed ;
1962+ cond = j . identifier ( trimmed ) ;
1963+ }
1964+
1965+ if ( propName && ! destructureProps . includes ( propName ) ) {
1966+ destructureProps . push ( propName ) ;
1967+ }
1968+
1969+ styleArgs . push (
1970+ j . logicalExpression (
1971+ "&&" ,
1972+ cond ,
1973+ j . memberExpression ( j . identifier ( "styles" ) , j . identifier ( variantKey ) ) ,
1974+ ) ,
1975+ ) ;
1976+ }
1977+ }
1978+
1979+ // Add style function calls for dynamic prop-based styles
1980+ const styleFnPairs = d . styleFnFromProps ?? [ ] ;
1981+ for ( const p of styleFnPairs ) {
1982+ const propExpr = j . identifier ( p . jsxProp ) ;
1983+ const call = j . callExpression (
1984+ j . memberExpression ( j . identifier ( "styles" ) , j . identifier ( p . fnKey ) ) ,
1985+ [ propExpr as any ] ,
1986+ ) ;
1987+ if ( ! destructureProps . includes ( p . jsxProp ) ) {
1988+ destructureProps . push ( p . jsxProp ) ;
1989+ }
1990+ const required = isPropRequiredInPropsTypeLiteral ( d . propsType , p . jsxProp ) ;
1991+ if ( required ) {
1992+ styleArgs . push ( call ) ;
1993+ } else {
1994+ styleArgs . push (
1995+ j . logicalExpression (
1996+ "&&" ,
1997+ j . binaryExpression ( "!=" , propExpr as any , j . nullLiteral ( ) ) ,
1998+ call ,
1999+ ) ,
2000+ ) ;
2001+ }
2002+ }
2003+
18552004 const propsParamId = j . identifier ( "props" ) ;
18562005 annotatePropsParam ( propsParamId , d . localName ) ;
18572006 const propsId = j . identifier ( "props" ) ;
@@ -1861,6 +2010,9 @@ export function emitWrappers(args: {
18612010 ) ;
18622011
18632012 // Create: <WrappedComponent {...props} {...stylex.props(styles.key)} />
2013+ // or with destructuring if we have conditional/dynamic props:
2014+ // const { prop, style, ...rest } = props;
2015+ // return <WrappedComponent {...rest} {...stylex.props(styles.key, prop && styles.keyProp)} style={style} />
18642016 // Handle both simple identifiers (Button) and member expressions (animated.div)
18652017 let jsxTagName : any ;
18662018 if ( wrappedComponent . includes ( "." ) ) {
@@ -1872,23 +2024,60 @@ export function emitWrappers(args: {
18722024 } else {
18732025 jsxTagName = j . jsxIdentifier ( wrappedComponent ) ;
18742026 }
1875- const jsx = j . jsxElement (
1876- j . jsxOpeningElement (
1877- jsxTagName ,
1878- [ j . jsxSpreadAttribute ( propsId ) , j . jsxSpreadAttribute ( stylexPropsCall ) ] ,
1879- true ,
1880- ) ,
1881- null ,
1882- [ ] ,
1883- ) ;
18842027
1885- emitted . push (
1886- j . functionDeclaration (
1887- j . identifier ( d . localName ) ,
1888- [ propsParamId ] ,
1889- j . blockStatement ( [ j . returnStatement ( jsx as any ) ] ) ,
1890- ) ,
1891- ) ;
2028+ // If we have props to destructure, create a proper destructuring pattern
2029+ if ( destructureProps . length > 0 ) {
2030+ const styleId = j . identifier ( "style" ) ;
2031+ const restId = j . identifier ( "rest" ) ;
2032+ const patternProps : any [ ] = destructureProps . map ( ( name ) => patternProp ( name ) ) ;
2033+ patternProps . push ( patternProp ( "style" , styleId ) ) ;
2034+ patternProps . push ( j . restElement ( restId ) ) ;
2035+
2036+ const declStmt = j . variableDeclaration ( "const" , [
2037+ j . variableDeclarator ( j . objectPattern ( patternProps as any ) , propsId ) ,
2038+ ] ) ;
2039+
2040+ const jsx = j . jsxElement (
2041+ j . jsxOpeningElement (
2042+ jsxTagName ,
2043+ [
2044+ j . jsxSpreadAttribute ( restId ) ,
2045+ j . jsxSpreadAttribute ( stylexPropsCall ) ,
2046+ j . jsxAttribute ( j . jsxIdentifier ( "style" ) , j . jsxExpressionContainer ( styleId ) ) ,
2047+ ] ,
2048+ true ,
2049+ ) ,
2050+ null ,
2051+ [ ] ,
2052+ ) ;
2053+
2054+ emitted . push (
2055+ j . functionDeclaration (
2056+ j . identifier ( d . localName ) ,
2057+ [ propsParamId ] ,
2058+ j . blockStatement ( [ declStmt , j . returnStatement ( jsx as any ) ] ) ,
2059+ ) ,
2060+ ) ;
2061+ } else {
2062+ // Simple case: just spread props and stylex.props
2063+ const jsx = j . jsxElement (
2064+ j . jsxOpeningElement (
2065+ jsxTagName ,
2066+ [ j . jsxSpreadAttribute ( propsId ) , j . jsxSpreadAttribute ( stylexPropsCall ) ] ,
2067+ true ,
2068+ ) ,
2069+ null ,
2070+ [ ] ,
2071+ ) ;
2072+
2073+ emitted . push (
2074+ j . functionDeclaration (
2075+ j . identifier ( d . localName ) ,
2076+ [ propsParamId ] ,
2077+ j . blockStatement ( [ j . returnStatement ( jsx as any ) ] ) ,
2078+ ) ,
2079+ ) ;
2080+ }
18922081 }
18932082
18942083 if ( emitted . length > 0 ) {
0 commit comments