@@ -265,7 +265,9 @@ export function processDeclRules(ctx: DeclProcessingState): void {
265265
266266 // Component selector patterns that have special handling below:
267267 // 1. `${Other}:pseudo &` - ancestor pseudo via descendant combinator (space only)
268+ // 1c: `${Other} &` - ancestor without pseudo (no-pseudo reverse)
268269 // 2. `&:pseudo ${Child}` or just `& ${Child}` - parent styling descendant child
270+ // 3. `${Link}:pseudo + &` or `~ &` - cross-component sibling combinator
269271 // Other component selector patterns (like `${Other} .child`) should bail.
270272 const selectorTrimmed = selectorForAnalysis . trim ( ) ;
271273 const isHandledComponentPattern =
@@ -274,10 +276,14 @@ export function processDeclRules(ctx: DeclProcessingState): void {
274276 // Pattern 1b: comma-separated reverse selectors where each part matches Pattern 1
275277 // e.g., `__SC_EXPR_0__:focus-visible &, __SC_EXPR_1__:active &`
276278 isCommaGroupedReverseSelectorPattern ( selectorTrimmed ) ||
279+ // Pattern 1c: no-pseudo reverse: `${Other} &` (component as ancestor, no pseudo condition)
280+ / ^ _ _ S C _ E X P R _ \d + _ _ \s + & \s * $ / . test ( selectorTrimmed ) ||
277281 // Pattern 2: starts with & (forward descendant/pseudo pattern)
278282 selectorTrimmed . startsWith ( "&" ) ||
279283 // Pattern 3: standalone component selector `${Child} { ... }`
280- / ^ _ _ S C _ E X P R _ \d + _ _ \s * \{ / . test ( selectorTrimmed ) ) ;
284+ / ^ _ _ S C _ E X P R _ \d + _ _ \s * \{ / . test ( selectorTrimmed ) ||
285+ // Pattern 4: cross-component sibling: `${Link}:pseudo + &` or `~ &`
286+ / ^ _ _ S C _ E X P R _ \d + _ _ : [ a - z ] [ a - z 0 - 9 ( ) - ] * \s * [ + ~ ] \s * & \s * $ / . test ( selectorTrimmed ) ) ;
281287
282288 // Use heuristic-based bail checks. We need to allow:
283289 // - Component selectors that have special handling
@@ -389,10 +395,17 @@ export function processDeclRules(ctx: DeclProcessingState): void {
389395 ! isReverseSelectorPattern &&
390396 selTrim2 . startsWith ( "__SC_EXPR_" ) &&
391397 isCommaGroupedReverseSelectorPattern ( selTrim2 ) ;
398+ // `${Other} &` — no pseudo, component as ancestor. Requires a scoped defineMarker()
399+ // since defaultMarker() would match ANY ancestor, not just Other.
400+ const isNoPseudoReversePattern =
401+ ! isReverseSelectorPattern &&
402+ ! isGroupedReverseSelectorPattern &&
403+ selTrim2 . startsWith ( "__SC_EXPR_" ) &&
404+ / ^ _ _ S C _ E X P R _ \d + _ _ \s + & \s * $ / . test ( selTrim2 ) ;
392405 if (
393406 otherLocal &&
394407 ! isCssHelperPlaceholder &&
395- ( isReverseSelectorPattern || isGroupedReverseSelectorPattern )
408+ ( isReverseSelectorPattern || isGroupedReverseSelectorPattern || isNoPseudoReversePattern )
396409 ) {
397410 // For grouped selectors, verify ALL slot IDs resolve to the same component.
398411 // Without this guard, `${Link}:focus &, ${Button}:active &` would silently
@@ -432,8 +445,12 @@ export function processDeclRules(ctx: DeclProcessingState): void {
432445 break ;
433446 }
434447
435- // Extract all ancestor pseudos (one per comma-separated part)
436- const ancestorPseudos = extractReverseSelectorPseudos ( rule . selector ) ;
448+ // Extract all ancestor pseudos (one per comma-separated part).
449+ // For no-pseudo reverse (`${Other} &`), use `:is(*)` as synthetic always-matching
450+ // pseudo so the style is conditional on the marker, not unconditional.
451+ const ancestorPseudos = isNoPseudoReversePattern
452+ ? [ ":is(*)" ]
453+ : extractReverseSelectorPseudos ( rule . selector ) ;
437454 if ( ancestorPseudos . length === 0 ) {
438455 state . markBail ( ) ;
439456 warnings . push ( {
@@ -453,8 +470,18 @@ export function processDeclRules(ctx: DeclProcessingState): void {
453470 const overrideStyleKey = `${ toStyleKey ( decl . localName ) } In${ jsxParentName } ` ;
454471 ancestorSelectorParents . add ( parentStyleKey ) ;
455472
456- // For cross-file reverse, register a defineMarker for the imported parent
457- const reverseMarkerVarName = crossFileParent ? `${ jsxParentName } Marker` : undefined ;
473+ // Register a defineMarker for the parent:
474+ // - Cross-file reverse always needs a marker
475+ // - No-pseudo reverse needs a scoped marker (defaultMarker() would be too broad)
476+ const needsScopedMarker = isNoPseudoReversePattern || ! ! crossFileParent ;
477+ const reverseMarkerVarName = needsScopedMarker ? `${ jsxParentName } Marker` : undefined ;
478+
479+ // For no-pseudo reverse with same-file parent, register the marker through
480+ // the sibling marker mechanism (feeds into crossFileMarkers → sidecar generation).
481+ if ( isNoPseudoReversePattern && ! crossFileParent && reverseMarkerVarName ) {
482+ state . siblingMarkerNames . set ( parentStyleKey , reverseMarkerVarName ) ;
483+ state . siblingMarkerParents . add ( parentStyleKey ) ;
484+ }
458485
459486 const overrideCountBeforeReverse = relationOverrides . length ;
460487 // Process declarations once, then register into each pseudo bucket
@@ -476,6 +503,18 @@ export function processDeclRules(ctx: DeclProcessingState): void {
476503 jsxParentName ,
477504 ) ;
478505
506+ // For same-file no-pseudo reverse, set markerVarName on the override so
507+ // finalizeRelationOverrides emits stylex.when.ancestor(":is(*)", Marker).
508+ const lastOverride = relationOverrides . at ( - 1 ) ;
509+ if (
510+ isNoPseudoReversePattern &&
511+ ! crossFileParent &&
512+ lastOverride &&
513+ relationOverrides . length > overrideCountBeforeReverse
514+ ) {
515+ lastOverride . markerVarName = reverseMarkerVarName ;
516+ }
517+
479518 const result = processDeclarationsIntoBucket (
480519 rule ,
481520 firstBucket ,
@@ -616,6 +655,85 @@ export function processDeclRules(ctx: DeclProcessingState): void {
616655 continue ;
617656 }
618657
658+ // Cross-component sibling: `${Link}:focus-visible + &` or `${Link}:active ~ &`
659+ // The declaring component reacts when the referenced component is its sibling.
660+ // Uses stylex.when.siblingBefore(":pseudo", ReferencedMarker).
661+ const crossComponentSiblingMatch = otherLocal
662+ ? selTrim2 . match ( / ^ _ _ S C _ E X P R _ \d + _ _ : ( [ a - z ] [ a - z 0 - 9 ( ) - ] * ) \s * ( [ + ~ ] ) \s * & \s * $ / )
663+ : null ;
664+ if ( otherLocal && ! isCssHelperPlaceholder && crossComponentSiblingMatch ) {
665+ const siblingPseudo = `:${ crossComponentSiblingMatch [ 1 ] } ` ;
666+ const combinator = crossComponentSiblingMatch [ 2 ] as "+" | "~" ;
667+
668+ const referencedDecl = declByLocalName . get ( otherLocal ) ;
669+ const crossFileRef = ! referencedDecl
670+ ? state . crossFileSelectorsByLocal . get ( otherLocal )
671+ : undefined ;
672+ if ( ! referencedDecl && ! crossFileRef ) {
673+ state . markBail ( ) ;
674+ warnings . push ( {
675+ severity : "warning" ,
676+ type : "Unsupported selector: unknown component selector" ,
677+ loc : computeSelectorWarningLoc ( decl . loc , decl . rawCss , rule . selector ) ,
678+ } ) ;
679+ break ;
680+ }
681+
682+ // Emit info warning for `+` since adjacent becomes general sibling
683+ if ( combinator === "+" ) {
684+ warnings . push ( {
685+ severity : "info" ,
686+ type : "Sibling selector broadened: + (adjacent) becomes general sibling (~) in StyleX — interleaved non-matching elements will no longer block the match" ,
687+ loc : computeSelectorWarningLoc ( decl . loc , decl . rawCss , rule . selector ) ,
688+ } ) ;
689+ }
690+
691+ // Register marker for the referenced component
692+ const jsxRefName = crossFileRef ?. bridgeComponentLocalName ?? otherLocal ;
693+ const refStyleKey = referencedDecl ? referencedDecl . styleKey : toStyleKey ( jsxRefName ) ;
694+ const refMarkerVarName = state . siblingMarkerNames . get ( refStyleKey ) ?? `${ jsxRefName } Marker` ;
695+ state . siblingMarkerNames . set ( refStyleKey , refMarkerVarName ) ;
696+ state . siblingMarkerParents . add ( refStyleKey ) ;
697+ ancestorSelectorParents . add ( refStyleKey ) ;
698+
699+ // Process declarations into a temporary bucket
700+ const sibBucket : Record < string , unknown > = { } ;
701+ const sibResult = processDeclarationsIntoBucket (
702+ rule ,
703+ sibBucket ,
704+ j ,
705+ decl ,
706+ resolveThemeValue ,
707+ resolveThemeValueFromFn ,
708+ { bailOnUnresolved : true } ,
709+ ) ;
710+ if ( sibResult === "bail" ) {
711+ state . markBail ( ) ;
712+ warnings . push ( {
713+ severity : "warning" ,
714+ type : "Unsupported selector: unresolved interpolation in cross-component sibling selector" ,
715+ loc : computeSelectorWarningLoc ( decl . loc , decl . rawCss , rule . selector ) ,
716+ } ) ;
717+ break ;
718+ }
719+
720+ // Build stylex.when.siblingBefore(':pseudo', Marker) per property
721+ const makeSiblingKeyExpr = ( ) =>
722+ j . callExpression (
723+ j . memberExpression (
724+ j . memberExpression ( j . identifier ( "stylex" ) , j . identifier ( "when" ) ) ,
725+ j . identifier ( "siblingBefore" ) ,
726+ ) ,
727+ [ j . literal ( siblingPseudo ) , j . identifier ( refMarkerVarName ) ] ,
728+ ) ;
729+
730+ for ( const [ prop , value ] of Object . entries ( sibBucket ) ) {
731+ const entry = getOrCreateComputedMediaEntry ( prop , ctx ) ;
732+ entry . entries . push ( { keyExpr : makeSiblingKeyExpr ( ) , value } ) ;
733+ }
734+ continue ;
735+ }
736+
619737 // Selector interpolation that's a MemberExpression (e.g., screenSize.phone)
620738 // Try to resolve it via the adapter as a media query helper.
621739 if ( ! otherLocal && slotExpr && isMemberExpression ( slotExpr ) ) {
@@ -644,7 +762,10 @@ export function processDeclRules(ctx: DeclProcessingState): void {
644762 // Store the resolved media expression for this rule
645763 const mediaExpr = parseExpr ( selectorResult . expr ) ;
646764 if ( mediaExpr ) {
647- resolvedSelectorMedia = { keyExpr : mediaExpr , exprSource : selectorResult . expr } ;
765+ resolvedSelectorMedia = {
766+ keyExpr : mediaExpr ,
767+ exprSource : selectorResult . expr ,
768+ } ;
648769 // Add required imports
649770 registerImports ( selectorResult . imports , resolverImports ) ;
650771 resolved = true ;
@@ -936,7 +1057,9 @@ export function processDeclRules(ctx: DeclProcessingState): void {
9361057 ( existingVal as Record < string , unknown > ) [ ps ] = value ;
9371058 }
9381059 } else {
939- const pseudoMap : Record < string , unknown > = { default : existingVal ?? null } ;
1060+ const pseudoMap : Record < string , unknown > = {
1061+ default : existingVal ?? null ,
1062+ } ;
9401063 for ( const ps of pseudos ) {
9411064 pseudoMap [ ps ] = value ;
9421065 }
@@ -1075,7 +1198,9 @@ function processDeclarationsIntoBucket(
10751198 * slot can't be resolved, or `null` if no slots are found.
10761199 */
10771200function resolveAllSlots (
1078- d : { value : { kind : string ; parts ?: Array < { kind : string ; slotId ?: number } > } } ,
1201+ d : {
1202+ value : { kind : string ; parts ?: Array < { kind : string ; slotId ?: number } > } ;
1203+ } ,
10791204 decl : { templateExpressions : unknown [ ] } ,
10801205 resolveThemeValue : ( expr : unknown ) => unknown ,
10811206 resolveThemeValueFromFn : ( expr : unknown ) => unknown ,
@@ -1123,7 +1248,12 @@ function resolveAllSlots(
11231248 */
11241249function buildInterpolatedValue (
11251250 j : DeclProcessingState [ "state" ] [ "j" ] ,
1126- d : { value : { kind : string ; parts ?: Array < { kind : string ; value ?: string ; slotId ?: number } > } } ,
1251+ d : {
1252+ value : {
1253+ kind : string ;
1254+ parts ?: Array < { kind : string ; value ?: string ; slotId ?: number } > ;
1255+ } ;
1256+ } ,
11271257 resolveSlot : ( slotId : number ) => unknown ,
11281258) : unknown {
11291259 const parts = d . value . parts ?? [ ] ;
@@ -1187,7 +1317,11 @@ function resolveElementSelectorTarget(
11871317 root : DeclProcessingState [ "state" ] [ "root" ] ,
11881318 j : JSCodeshift ,
11891319) :
1190- | { childDecl : StyledDecl ; ancestorPseudo : string | null ; childPseudo : string | null }
1320+ | {
1321+ childDecl : StyledDecl ;
1322+ ancestorPseudo : string | null ;
1323+ childPseudo : string | null ;
1324+ }
11911325 | ElementSelectorBailReason
11921326 | null {
11931327 const parsed = parseElementSelectorPattern ( selector ) ;
@@ -1651,7 +1785,11 @@ function tagCrossFileOverride(
16511785function recoverStandaloneInterpolationsInPseudoBlock (
16521786 rule : DeclProcessingState [ "decl" ] [ "rules" ] [ number ] ,
16531787 decl : DeclProcessingState [ "decl" ] ,
1654- ) : { when : string ; propName : string ; cssProps : Record < string , unknown > } | null {
1788+ ) : {
1789+ when : string ;
1790+ propName : string ;
1791+ cssProps : Record < string , unknown > ;
1792+ } | null {
16551793 const { rawCss, templateExpressions } = decl ;
16561794 if ( ! rawCss ) {
16571795 return null ;
0 commit comments