@@ -28,6 +28,8 @@ export function postProcessTransformedAst(args: {
2828 crossFileMarkers ?: Map < string , string > ;
2929 /** Parent style keys that need defaultMarker() (have at least one override without a scoped marker) */
3030 parentsNeedingDefaultMarker ?: Set < string > ;
31+ /** Maps style key → set of CSS attribute selector strings used in ancestor attribute conditions */
32+ ancestorAttrsByStyleKey ?: Map < string , Set < string > > ;
3133} ) : { changed : boolean ; needsReactImport : boolean } {
3234 const {
3335 root,
@@ -42,6 +44,7 @@ export function postProcessTransformedAst(args: {
4244 stylesIdentifier = "styles" ,
4345 crossFileMarkers,
4446 parentsNeedingDefaultMarker,
47+ ancestorAttrsByStyleKey,
4548 } = args ;
4649 let changed = false ;
4750
@@ -57,7 +60,11 @@ export function postProcessTransformedAst(args: {
5760 // - Add `stylex.defaultMarker()` to elements that need markers (ancestor selectors).
5861 // - Add override style keys to descendant/child elements' `stylex.props(...)` calls.
5962 // - For cross-file selectors: use `defineMarker()` and add overrides to imported child JSX.
60- if ( relationOverrides . length > 0 || ancestorSelectorParents . size > 0 ) {
63+ if (
64+ relationOverrides . length > 0 ||
65+ ancestorSelectorParents . size > 0 ||
66+ ( ancestorAttrsByStyleKey && ancestorAttrsByStyleKey . size > 0 )
67+ ) {
6168 // IMPORTANT: Do not reuse the same AST node instance across multiple insertion points.
6269 // Recast/jscodeshift expect a tree (no shared references); reuse can corrupt printing.
6370 const makeDefaultMarkerCall = ( ) =>
@@ -376,9 +383,67 @@ export function postProcessTransformedAst(args: {
376383 }
377384 }
378385
386+ // When a child element uses styles with ancestor attribute conditions,
387+ // walk up the ancestor JSX tree and add defaultMarker() to any element
388+ // whose JSX attributes match the CSS attribute selector patterns.
389+ if ( ancestorAttrsByStyleKey && ancestorAttrsByStyleKey . size > 0 ) {
390+ const usedStyleKeys = getUsedStyleKeys ( call , sxAttr , stylesIdentifier ) ;
391+ for ( const key of usedStyleKeys ) {
392+ const attrSelectors = ancestorAttrsByStyleKey . get ( key ) ;
393+ if ( ! attrSelectors ) {
394+ continue ;
395+ }
396+ const attrNames = extractAttrNamesFromSelectors ( attrSelectors ) ;
397+ for ( const anc of ancestors ) {
398+ if ( ! anc . opening ) {
399+ continue ;
400+ }
401+ const jsxAttrs = ( anc . opening . attributes ?? [ ] ) as any [ ] ;
402+ const hasMatchingAttr = jsxAttrs . some (
403+ ( a : any ) =>
404+ a . type === "JSXAttribute" &&
405+ a . name ?. type === "JSXIdentifier" &&
406+ attrNames . has ( a . name . name ) ,
407+ ) ;
408+ if ( ! hasMatchingAttr ) {
409+ continue ;
410+ }
411+ // Add defaultMarker() via sx attribute on the ancestor element
412+ const existingSx = getSxAttrFromAttrs ( jsxAttrs ) ;
413+ if ( existingSx ) {
414+ if ( ! hasDefaultMarkerInSxArgs ( existingSx ) ) {
415+ addArgsToSxAttr ( existingSx , [ makeDefaultMarkerCall ( ) ] ) ;
416+ changed = true ;
417+ }
418+ } else {
419+ const existingCall = getStylexPropsCallFromAttrs ( jsxAttrs ) ;
420+ if ( existingCall ) {
421+ if ( ! hasDefaultMarker ( existingCall ) ) {
422+ existingCall . arguments = [
423+ ...( existingCall . arguments ?? [ ] ) ,
424+ makeDefaultMarkerCall ( ) ,
425+ ] ;
426+ changed = true ;
427+ }
428+ } else {
429+ // Plain element with no sx or stylex.props — add sx={stylex.defaultMarker()}
430+ anc . opening . attributes = [
431+ ...jsxAttrs ,
432+ j . jsxAttribute (
433+ j . jsxIdentifier ( "sx" ) ,
434+ j . jsxExpressionContainer ( makeDefaultMarkerCall ( ) ) ,
435+ ) ,
436+ ] ;
437+ changed = true ;
438+ }
439+ }
440+ }
441+ }
442+ }
443+
379444 const nextAncestors = [
380445 ...ancestors ,
381- { call, sxAttr, elementStyleKey, markerVarName : addedMarkerVarName } ,
446+ { call, sxAttr, elementStyleKey, markerVarName : addedMarkerVarName , opening } ,
382447 ] ;
383448 for ( const child of node . children ?? [ ] ) {
384449 visitJsxChild ( child , nextAncestors ) ;
@@ -741,6 +806,56 @@ export function postProcessTransformedAst(args: {
741806
742807// --- Non-exported helpers ---
743808
809+ /** Extracts style key names referenced in a stylex.props() call or sx attribute. */
810+ function getUsedStyleKeys ( call : any , sxAttr : any , stylesIdentifier : string ) : string [ ] {
811+ const keys : string [ ] = [ ] ;
812+ const extractKey = ( node : any ) : void => {
813+ if (
814+ node ?. type === "MemberExpression" &&
815+ node . object ?. type === "Identifier" &&
816+ node . object . name === stylesIdentifier &&
817+ node . property ?. type === "Identifier"
818+ ) {
819+ keys . push ( node . property . name ) ;
820+ }
821+ // Also match styleFn calls: styles.key(...)
822+ if ( node ?. type === "CallExpression" ) {
823+ extractKey ( node . callee ) ;
824+ }
825+ } ;
826+ if ( call ) {
827+ for ( const arg of call . arguments ?? [ ] ) {
828+ extractKey ( arg ) ;
829+ }
830+ }
831+ if ( sxAttr ) {
832+ const expr = sxAttr . value ?. expression ;
833+ if ( expr ?. type === "ArrayExpression" ) {
834+ for ( const el of expr . elements ?? [ ] ) {
835+ extractKey ( el ) ;
836+ }
837+ } else {
838+ extractKey ( expr ) ;
839+ }
840+ }
841+ return keys ;
842+ }
843+
844+ /** Extracts attribute names from CSS attribute selector strings like '[aria-checked="true"]'. */
845+ function extractAttrNamesFromSelectors ( selectors : Set < string > ) : Set < string > {
846+ const names = new Set < string > ( ) ;
847+ // Match attribute names inside [...] brackets
848+ const re = / \[ ( [ a - z A - Z ] [ \w - ] * ) / g;
849+ for ( const sel of selectors ) {
850+ let m ;
851+ while ( ( m = re . exec ( sel ) ) !== null ) {
852+ names . add ( m [ 1 ] ! ) ;
853+ }
854+ re . lastIndex = 0 ;
855+ }
856+ return names ;
857+ }
858+
744859function appendToMapList < K , V > ( map : Map < K , V [ ] > , key : K , value : V ) : void {
745860 const list = map . get ( key ) ;
746861 if ( list ) {
0 commit comments