Skip to content

Commit 93ff226

Browse files
committed
Replace sibling broadening warning with inline code comment
When CSS `+` (adjacent sibling) is transformed to StyleX's `siblingBefore()` (which emits `~`, general sibling), emit a `// NOTE:` comment on the computed key property instead of an info-level warning. This makes the semantic broadening visible directly in the generated code where it matters. https://claude.ai/code/session_01NACR4DTcJGrJ3xu7sWWQC9
1 parent 1c69ab8 commit 93ff226

10 files changed

Lines changed: 59 additions & 31 deletions

src/__tests__/transform.test.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,7 @@ export const App = () => <Box><span /></Box>;
667667
expect(warning?.loc?.line).toBe(7);
668668
});
669669

670-
it("should emit info warning for & + & (adjacent sibling broadens to general)", () => {
670+
it("should emit NOTE comment for & + & (adjacent sibling broadens to general)", () => {
671671
const source = `
672672
import styled from "styled-components";
673673
@@ -688,15 +688,18 @@ export const App = () => <Thing />;
688688
);
689689

690690
expect(result.code).not.toBeNull();
691-
const infoWarnings = result.warnings.filter((w) => w.severity === "info");
692-
expect(infoWarnings).toHaveLength(1);
693-
expect(infoWarnings[0]).toMatchObject({
694-
severity: "info",
695-
type: "Sibling selector broadened: & + & (adjacent) becomes general sibling (~) in StyleX — interleaved non-matching elements will no longer block the match",
696-
});
691+
// No info warning — the broadening note is emitted as a code comment instead
692+
const infoWarnings = result.warnings.filter(
693+
(w) => w.severity === "info" && w.type.includes("Sibling selector broadened"),
694+
);
695+
expect(infoWarnings).toHaveLength(0);
696+
// The output should contain a NOTE comment about the broadening
697+
expect(result.code).toContain(
698+
"// NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX",
699+
);
697700
});
698701

699-
it("should NOT emit info warning for & ~ & (general sibling is exact match)", () => {
702+
it("should NOT emit NOTE comment for & ~ & (general sibling is exact match)", () => {
700703
const source = `
701704
import styled from "styled-components";
702705
@@ -717,8 +720,12 @@ export const App = () => <Thing />;
717720
);
718721

719722
expect(result.code).not.toBeNull();
720-
const infoWarnings = result.warnings.filter((w) => w.severity === "info");
723+
const infoWarnings = result.warnings.filter(
724+
(w) => w.severity === "info" && w.type.includes("Sibling selector broadened"),
725+
);
721726
expect(infoWarnings).toHaveLength(0);
727+
// No NOTE comment for general sibling — it's an exact match
728+
expect(result.code).not.toContain("NOTE: CSS `+`");
722729
});
723730

724731
it("should emit info warning when transient props are renamed on exported component", () => {

src/internal/logger.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,6 @@ export type WarningType =
103103
| "Unsupported css`` mixin: nested contextual conditions in after-base mixin"
104104
| "Unsupported css`` mixin: cannot infer base default for after-base contextual override (base value is non-literal)"
105105
| "css`` helper function interpolation references closure variable that cannot be hoisted"
106-
| "Sibling selector broadened: & + & (adjacent) becomes general sibling (~) in StyleX — interleaved non-matching elements will no longer block the match"
107-
| "Sibling selector broadened: + (adjacent) becomes general sibling (~) in StyleX — interleaved non-matching elements will no longer block the match"
108106
| "Using styled-components components as mixins is not supported; use css`` mixins or strings instead"
109107
| "styled(ImportedComponent) wraps a component whose file contains internal styled-components — convert the base component's file first to avoid CSS cascade conflicts"
110108
| "Transient $-prefixed props renamed on exported component — update consumer call sites to use the new prop names"

src/internal/lower-rules/finalize-decl.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,15 @@ export function finalizeDeclProcessing(ctx: DeclProcessingState): void {
8484
(merged as Record<string, unknown>).__computedKeys = entry.entries.map((e) => ({
8585
keyExpr: e.keyExpr,
8686
value: e.value,
87+
leadingComment: e.leadingComment,
8788
}));
8889
} else {
8990
// No existing map, create a new nested object with default and __computedKeys
9091
const nested: Record<string, unknown> = { default: resolvedDefault };
9192
(nested as Record<string, unknown>).__computedKeys = entry.entries.map((e) => ({
9293
keyExpr: e.keyExpr,
9394
value: e.value,
95+
leadingComment: e.leadingComment,
9496
}));
9597
styleObj[prop] = nested;
9698
}

src/internal/lower-rules/process-rules.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -681,14 +681,8 @@ export function processDeclRules(ctx: DeclProcessingState): void {
681681
break;
682682
}
683683

684-
// Emit info warning for `+` since adjacent becomes general sibling
685-
if (combinator === "+") {
686-
warnings.push({
687-
severity: "info",
688-
type: "Sibling selector broadened: + (adjacent) becomes general sibling (~) in StyleX — interleaved non-matching elements will no longer block the match",
689-
loc: computeSelectorWarningLoc(decl.loc, decl.rawCss, rule.selector),
690-
});
691-
}
684+
// Track adjacent combinator so we can annotate the emitted computed keys
685+
const isAdjacentCombinator = combinator === "+";
692686

693687
// Register marker for the referenced component
694688
const refStyleKey = referencedDecl.styleKey;
@@ -769,7 +763,14 @@ export function processDeclRules(ctx: DeclProcessingState): void {
769763
for (const [prop, value] of Object.entries(sibBucket)) {
770764
const siblingValue = media ? { default: null, [media]: value } : value;
771765
const entry = getOrCreateComputedMediaEntry(prop, ctx);
772-
entry.entries.push({ keyExpr: makeSiblingKeyExpr(), value: siblingValue });
766+
entry.entries.push({
767+
keyExpr: makeSiblingKeyExpr(),
768+
value: siblingValue,
769+
...(isAdjacentCombinator && {
770+
leadingComment:
771+
"NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX",
772+
}),
773+
});
773774
}
774775
continue;
775776
}
@@ -2078,15 +2079,8 @@ function handleSiblingSelector(
20782079
const { j, warnings, resolveThemeValue, resolveThemeValueFromFn, ancestorSelectorParents } =
20792080
state;
20802081

2081-
// Emit info warning for `& + &` since adjacent becomes general sibling
2082+
// Track adjacent combinator so we can annotate the emitted computed keys
20822083
const isAdjacent = /\+/.test(selector);
2083-
if (isAdjacent) {
2084-
warnings.push({
2085-
severity: "info",
2086-
type: "Sibling selector broadened: & + & (adjacent) becomes general sibling (~) in StyleX — interleaved non-matching elements will no longer block the match",
2087-
loc: computeSelectorWarningLoc(decl.loc, decl.rawCss, rule.selector),
2088-
});
2089-
}
20902084

20912085
// Add to ancestorSelectorParents so the marker is injected into stylex.props() calls.
20922086
// Also add to siblingMarkerParents to distinguish from forward/reverse selectors —
@@ -2176,7 +2170,13 @@ function handleSiblingSelector(
21762170
for (const [prop, value] of Object.entries(bucket)) {
21772171
const siblingValue = media ? { default: null, [media]: value } : value;
21782172
const entry = getOrCreateComputedMediaEntry(prop, ctx);
2179-
entry.entries.push({ keyExpr: makeSiblingKeyExpr(), value: siblingValue });
2173+
entry.entries.push({
2174+
keyExpr: makeSiblingKeyExpr(),
2175+
value: siblingValue,
2176+
...(isAdjacent && {
2177+
leadingComment: "NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX",
2178+
}),
2179+
});
21802180
}
21812181
}
21822182

src/internal/transform/helpers.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export type ComputedKeyEntry = {
4646
keyExpr: unknown;
4747
/** The value (can be nested object, string, number, etc.) */
4848
value: unknown;
49+
/** Optional leading comment to attach to the emitted property */
50+
leadingComment?: string;
4951
};
5052

5153
export function objectToAst(j: API["jscodeshift"], obj: Record<string, unknown>): any {
@@ -145,6 +147,16 @@ export function objectToAst(j: API["jscodeshift"], obj: Record<string, unknown>)
145147
: literalToAst(j, entry.value);
146148
const prop = j.property("init", entry.keyExpr as any, valueAst);
147149
(prop as any).computed = true;
150+
if (entry.leadingComment) {
151+
(prop as any).comments = [
152+
{
153+
type: "CommentLine",
154+
value: ` ${entry.leadingComment}`,
155+
leading: true,
156+
trailing: false,
157+
},
158+
];
159+
}
148160
props.push(prop);
149161
}
150162

test-cases/selector-componentSiblingCombinator.output.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@ const styles = stylex.create({
3737
paddingInline: 8,
3838
color: {
3939
default: "gray",
40+
// NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX
4041
[stylex.when.siblingBefore(":focus-visible", LinkMarker)]: "blue",
4142
},
4243
backgroundColor: {
4344
default: null,
44-
45+
// NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX
4546
[stylex.when.siblingBefore(":hover", LinkMarker)]: {
4647
default: null,
4748
"@media (min-width: 768px)": "lightyellow",

test-cases/selector-sibling.output.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,28 @@ const styles = stylex.create({
3737
thing: {
3838
color: {
3939
default: "blue",
40+
// NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX
4041
[stylex.when.siblingBefore(":is(*)", ThingMarker)]: "red",
4142
},
4243
paddingBlock: 8,
4344
paddingInline: 16,
4445
backgroundColor: {
4546
default: null,
47+
// NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX
4648
[stylex.when.siblingBefore(":is(*)", ThingMarker)]: "lime",
4749
},
4850
},
4951
thingThemed: {
5052
color: {
5153
default: "blue",
54+
// NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX
5255
[stylex.when.siblingBefore(":is(*)", ThingThemedMarker)]: $colors.labelBase,
5356
},
5457
},
5558
row: {
5659
marginTop: {
5760
default: null,
61+
// NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX
5862
[stylex.when.siblingBefore(":is(*)", RowMarker)]: 16,
5963
},
6064
},

test-cases/selector-siblingBaseAfter.output.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const styles = stylex.create({
1919
thing: {
2020
color: {
2121
default: "blue",
22+
// NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX
2223
[stylex.when.siblingBefore(":is(*)", ThingMarker)]: "red",
2324
},
2425
paddingBlock: 8,

test-cases/selector-siblingMarkerScoping.output.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,17 @@ const styles = stylex.create({
2828
padding: 8,
2929
borderTopWidth: {
3030
default: null,
31+
// NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX
3132
[stylex.when.siblingBefore(":is(*)", RowMarker)]: 1,
3233
},
3334
borderTopStyle: {
3435
default: null,
36+
// NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX
3537
[stylex.when.siblingBefore(":is(*)", RowMarker)]: "solid",
3638
},
3739
borderTopColor: {
3840
default: null,
41+
// NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX
3942
[stylex.when.siblingBefore(":is(*)", RowMarker)]: "gray",
4043
},
4144
},

test-cases/selector-siblingMedia.output.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const styles = stylex.create({
1919
padding: 8,
2020
marginTop: {
2121
default: null,
22-
22+
// NOTE: CSS `+` (adjacent sibling) becomes `~` (general sibling) in StyleX
2323
[stylex.when.siblingBefore(":is(*)", ThingMarker)]: {
2424
default: null,
2525
"@media (min-width: 768px)": 16,

0 commit comments

Comments
 (0)