Skip to content

Commit 486afc7

Browse files
skovhuscursoragent
andauthored
Cross-file selector transform (#340)
* fix: infer cross-file selector info in playground browser transform The playground runs single-file transforms without cross-file prepass info, causing cross-file selector test cases (e.g. selector-crossFileTwoParents) to show 'Unsupported selector: unknown component selector' instead of the correct transformation. Add inferCrossFileInfo() that reuses the existing prepass helper functions to detect imported identifiers used as selectors in styled templates and synthesize CrossFileSelectorUsage entries. This allows the transform to handle cross-file selectors correctly in the browser-only playground. Co-authored-by: Kenneth Skovhus <skovhus@users.noreply.github.com> * fix: make converted-collapse-icon fixture include bridge class and merge styles The fixture simulating an already-converted StyleX component had two issues: 1. It did not include the bridge class (sc2sx-CollapseArrowIcon-a1b2c3d4) on its DOM element, so styled-components consumers targeting it via CollapseArrowIconGlobalSelector could never match. 2. It spread {...stylex.props(styles.base)} after {...props}, overwriting incoming className/style. This prevented StyleX consumers from passing override styles (like the ancestor-hover backgroundColor: rebeccapurple). Fix: extract the bridge class as a const, apply it to the DOM element, and properly merge incoming className/style with the component's base styles. Co-authored-by: Kenneth Skovhus <skovhus@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 39f71e3 commit 486afc7

File tree

2 files changed

+103
-5
lines changed

2 files changed

+103
-5
lines changed

playground/src/lib/browser-transform.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
import jscodeshift from "jscodeshift";
2+
import { parse } from "@babel/parser";
23
import { transformWithWarnings } from "../../../src/transform";
34
import type { Adapter } from "../../../src/adapter";
45
import type { WarningLog } from "../../../src/internal/logger";
6+
import type { CrossFileInfo, CrossFileSelectorUsage } from "../../../src/internal/transform-types";
7+
import {
8+
walkForImportsAndTemplates,
9+
buildImportMapFromNodes,
10+
findStyledImportNameFromNodes,
11+
findCssImportNamesFromNodes,
12+
findComponentSelectorLocalsFromNodes,
13+
BARE_TEMPLATE_IDENTIFIER_RE,
14+
} from "../../../src/internal/prepass/scan-cross-file-selectors";
15+
import type { AstNode } from "../../../src/internal/prepass/prepass-parser";
516

617
export type { WarningLog };
718

@@ -32,7 +43,80 @@ export function runTransform(
3243
report: () => {},
3344
};
3445

35-
const options = { adapter };
46+
const crossFileInfo = inferCrossFileInfo(source);
47+
const options = crossFileInfo ? { adapter, crossFileInfo } : { adapter };
3648

3749
return transformWithWarnings(file, api, options);
3850
}
51+
52+
/**
53+
* Infer cross-file selector info from source code without filesystem access.
54+
* Detects imported identifiers used as selectors in styled templates
55+
* (e.g. `${CrossFileIcon} { ... }`) and synthesizes CrossFileSelectorUsage
56+
* entries so the transform can handle them.
57+
*/
58+
function inferCrossFileInfo(source: string): CrossFileInfo | undefined {
59+
if (!source.includes("styled-components")) {
60+
return undefined;
61+
}
62+
if (!BARE_TEMPLATE_IDENTIFIER_RE.test(source)) {
63+
return undefined;
64+
}
65+
66+
let ast: AstNode;
67+
try {
68+
ast = parse(source, {
69+
sourceType: "module",
70+
plugins: ["jsx", "typescript"],
71+
}) as unknown as AstNode;
72+
} catch {
73+
return undefined;
74+
}
75+
76+
const program = (ast.program ?? ast) as AstNode;
77+
78+
const importNodes: AstNode[] = [];
79+
const templateNodes: AstNode[] = [];
80+
walkForImportsAndTemplates(program, importNodes, templateNodes);
81+
82+
const importMap = buildImportMapFromNodes(importNodes);
83+
if (importMap.size === 0) {
84+
return undefined;
85+
}
86+
87+
const styledImportName = findStyledImportNameFromNodes(importNodes);
88+
const cssImportNames = findCssImportNamesFromNodes(importNodes);
89+
if (!styledImportName && cssImportNames.size === 0) {
90+
return undefined;
91+
}
92+
93+
const selectorLocals = findComponentSelectorLocalsFromNodes(
94+
templateNodes,
95+
styledImportName ?? "",
96+
cssImportNames,
97+
);
98+
if (selectorLocals.size === 0) {
99+
return undefined;
100+
}
101+
102+
const usages: CrossFileSelectorUsage[] = [];
103+
for (const localName of selectorLocals) {
104+
const imp = importMap.get(localName);
105+
if (!imp || imp.source === "styled-components") {
106+
continue;
107+
}
108+
109+
usages.push({
110+
localName,
111+
importSource: imp.source,
112+
importedName: imp.importedName,
113+
resolvedPath: `/synthetic/${imp.source}`,
114+
});
115+
}
116+
117+
if (usages.length === 0) {
118+
return undefined;
119+
}
120+
121+
return { selectorUsages: usages };
122+
}

test-cases/lib/converted-collapse-icon.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import * as React from "react";
22
import * as stylex from "@stylexjs/stylex";
33

4-
// Already-converted StyleX component (simulates Run 1 output)
4+
// Already-converted StyleX component (simulates Run 1 output).
5+
// Includes bridge class + className/style merging so both unconverted
6+
// styled-components consumers and converted StyleX consumers can
7+
// override styles on this component's DOM element.
58
export interface CollapseArrowIconProps extends React.ComponentProps<"div"> {}
69

710
const styles = stylex.create({
@@ -14,9 +17,20 @@ const styles = stylex.create({
1417
},
1518
});
1619

17-
export function CollapseArrowIcon(props: CollapseArrowIconProps) {
18-
return <div {...props} {...stylex.props(styles.base)} />;
20+
const collapseArrowIconBridgeClass = "sc2sx-CollapseArrowIcon-a1b2c3d4";
21+
22+
export function CollapseArrowIcon({ className, style, ...props }: CollapseArrowIconProps) {
23+
const base = stylex.props(styles.base);
24+
return (
25+
<div
26+
{...props}
27+
className={[collapseArrowIconBridgeClass, base.className, className]
28+
.filter(Boolean)
29+
.join(" ")}
30+
style={{ ...base.style, ...style }}
31+
/>
32+
);
1933
}
2034

2135
/** @deprecated Bridge selector for unconverted consumers — will be removed once all files are migrated. */
22-
export const CollapseArrowIconGlobalSelector = ".sc2sx-CollapseArrowIcon-a1b2c3d4";
36+
export const CollapseArrowIconGlobalSelector = `.${collapseArrowIconBridgeClass}`;

0 commit comments

Comments
 (0)