styled-components-to-stylex-codemod 0.0.36 → 0.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,153 @@ Transform styled-components to StyleX.
4
4
 
5
5
  **[Try it in the online playground](https://skovhus.github.io/styled-components-to-stylex-codemod/)** — experiment with the transform in your browser.
6
6
 
7
- ## Installation
7
+ ## Migration game plan
8
+
9
+ ### 1. Define your theme and mixins as StyleX
10
+
11
+ Before running the codemod, convert your theme object and shared style helpers into StyleX equivalents:
12
+
13
+ ```ts
14
+ // tokens.stylex.ts — theme variables
15
+ import * as stylex from "@stylexjs/stylex";
16
+
17
+ // Before: { colors: { primary: "#0066cc" }, spacing: { sm: "8px" } }
18
+ export const colors = stylex.defineVars({ primary: "#0066cc" });
19
+ export const spacing = stylex.defineVars({ sm: "8px" });
20
+ ```
21
+
22
+ ```ts
23
+ // helpers.stylex.ts — shared mixins
24
+ import * as stylex from "@stylexjs/stylex";
25
+
26
+ // Before: export const truncate = () => `white-space: nowrap; overflow: hidden; ...`
27
+ export const truncate = stylex.create({
28
+ base: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" },
29
+ });
30
+ ```
31
+
32
+ ### 2. Write an adapter and run the codemod
33
+
34
+ The adapter maps your project's `props.theme.*` access, CSS variables, and helper calls to the StyleX equivalents from step 1. See [Basic usage](#basic-usage) for the full API.
35
+
36
+ ### 3. Convert bottom-up (leaf components first)
37
+
38
+ When a component wraps another component that internally uses styled-components (e.g. `styled(GroupHeader)` where `GroupHeader` renders a `StyledHeader`), CSS cascade conflicts can arise after migration. Convert leaf files — the ones that don't wrap other styled-components — first, then work your way up. The codemod will bail with a warning if it detects this pattern.
39
+
40
+ ### 4. Verify, iterate, clean up
41
+
42
+ Build and test your project. Review warnings — they tell you which files were skipped and why. Fix adapter gaps, re-run on remaining files, and repeat until done. [Report issues](https://github.com/skovhus/styled-components-to-stylex-codemod/issues) with input/output examples if the codemod produces incorrect results.
43
+
44
+ ## Agent prompt for configuring a migration
45
+
46
+ Copy this into an agent working in the repository you want to migrate:
47
+
48
+ ````prompt
49
+ You are helping migrate this repository from styled-components to StyleX with
50
+ `styled-components-to-stylex-codemod`.
51
+
52
+ Work in small, reviewable steps:
53
+
54
+ 1. Inspect the project before changing files.
55
+ - Identify the package manager and install command.
56
+ - Find styled-components usage, theme access patterns, CSS variables, helper
57
+ functions used inside template interpolations, shared mixins, and existing
58
+ StyleX setup.
59
+ - Identify a leaf component/file glob to migrate first. Prefer components
60
+ that do not wrap other styled-components.
61
+
62
+ 2. Install the codemod and any missing StyleX runtime/build dependencies the
63
+ project needs.
64
+ - Use the repository's package manager.
65
+ - Keep dependency changes separate and explain why each package is needed.
66
+
67
+ 3. Create a local codemod runner, for example
68
+ `scripts/run-styled-components-to-stylex.mts`, using this shape:
69
+
70
+ ```ts
71
+ import { defineAdapter, runTransform } from "styled-components-to-stylex-codemod";
72
+
73
+ const adapter = defineAdapter({
74
+ resolveValue(ctx) {
75
+ // Map props.theme.*, CSS variables, and imported constants to StyleX
76
+ // variables or other static StyleX-compatible expressions.
77
+ return undefined;
78
+ },
79
+ resolveCall(ctx) {
80
+ // Map helper calls used in styled template interpolations to StyleX
81
+ // mixins/values, or return { preserveRuntimeCall: true } when safe.
82
+ return undefined;
83
+ },
84
+ resolveSelector(ctx) {
85
+ // Map imported selector helpers such as media query or pseudo aliases.
86
+ return undefined;
87
+ },
88
+ externalInterface(ctx) {
89
+ // Return { styles: true, as: true, ref: true } for exported components
90
+ // that must keep accepting className/style, polymorphic `as`, or refs.
91
+ return { styles: false, as: false, ref: false };
92
+ },
93
+ styleMerger: null,
94
+ useSxProp: false,
95
+ wrappedComponentInterface(ctx) {
96
+ return undefined;
97
+ },
98
+ themeHook: {
99
+ functionName: "useTheme",
100
+ importSource: { kind: "specifier", value: "styled-components" },
101
+ },
102
+ });
103
+
104
+ await runTransform({
105
+ files: "src/**/*.tsx",
106
+ consumerPaths: "src/**/*.tsx",
107
+ adapter,
108
+ dryRun: true,
109
+ parser: "tsx",
110
+ formatterCommands: ["pnpm prettier --write"],
111
+ });
112
+ ```
113
+
114
+ 4. Configure the adapter for this codebase.
115
+ - `resolveValue`: map theme paths (`props.theme.color.primary`), CSS
116
+ variables (`var(--token)`), and imported values to StyleX variables.
117
+ - `resolveCall`: map project style helpers to StyleX mixins or values.
118
+ - `resolveSelector`: map imported media-query or pseudo selector helpers.
119
+ - `externalInterface`: preserve `className`/`style`, `as`, and `ref` support
120
+ for public components. Use `externalInterface: "auto"` only when
121
+ `consumerPaths` covers the consumers and the prepass succeeds.
122
+ - `styleMerger`: provide the project's helper for combining StyleX styles
123
+ with external `className`/`style` when public components need it.
124
+ - `useSxProp` and `wrappedComponentInterface`: enable only if the project
125
+ uses StyleX `sx` props and the Babel plugin is configured for them.
126
+ - `themeHook`: point wrapper theme conditionals at the project's runtime
127
+ theme hook if it is not `useTheme` from styled-components.
128
+ - `resolveBaseComponent`: add this only for base UI primitives that can be
129
+ safely replaced with intrinsic elements and static StyleX styles.
130
+
131
+ 5. Run a dry run first.
132
+ - Keep `dryRun: true`.
133
+ - Run the runner against the smallest useful file glob.
134
+ - Read every warning. Update the adapter instead of hand-editing output
135
+ when the warning describes a repeatable project pattern.
136
+
137
+ 6. Run the real transform only after the dry run is clean enough to review.
138
+ - Set `dryRun: false`.
139
+ - Keep the migration scoped to the selected leaf files.
140
+ - Run the project's formatter, typecheck, lint, tests, and Storybook or
141
+ visual checks if available.
142
+ - Inspect the diff for dropped declarations, inline-style fallbacks, public
143
+ component API changes, and cross-file selector bridge/marker behavior.
144
+
145
+ 7. Iterate bottom-up.
146
+ - Commit the runner/adapter and each migrated slice separately.
147
+ - Expand the `files` glob only after the previous slice is reviewed.
148
+ - Preserve warnings or TODOs for any file that needs manual follow-up.
149
+ ````
150
+
151
+ ## API and configuration reference
152
+
153
+ ### Installation
8
154
 
9
155
  ```bash
10
156
  npm install styled-components-to-stylex-codemod
@@ -12,7 +158,7 @@ npm install styled-components-to-stylex-codemod
12
158
  pnpm add styled-components-to-stylex-codemod
13
159
  ```
14
160
 
15
- ## Usage
161
+ ### Basic usage
16
162
 
17
163
  Use `runTransform` to transform files matching a glob pattern:
18
164
 
@@ -22,15 +168,19 @@ import { runTransform, defineAdapter } from "styled-components-to-stylex-codemod
22
168
  const adapter = defineAdapter({
23
169
  // Map theme paths and CSS variables to StyleX expressions
24
170
  resolveValue(ctx) {
25
- return null;
171
+ return undefined;
26
172
  },
27
173
  // Map helper function calls to StyleX expressions
28
174
  resolveCall(ctx) {
29
- return null;
175
+ return undefined;
176
+ },
177
+ // Map imported selector helpers such as media query or pseudo aliases
178
+ resolveSelector(ctx) {
179
+ return undefined;
30
180
  },
31
- // Control which components accept external className/style and polymorphic `as`
181
+ // Control which components accept external className/style, polymorphic `as`, and refs
32
182
  externalInterface(ctx) {
33
- return { style: false, as: false };
183
+ return { styles: false, as: false, ref: false };
34
184
  },
35
185
  // Optional: use a helper for merging StyleX styles with external className/style
36
186
  styleMerger: null,
@@ -71,7 +221,7 @@ const adapter = defineAdapter({
71
221
  /**
72
222
  * Resolve dynamic values in styled template literals to StyleX expressions.
73
223
  * Called for theme access (`props.theme.x`), CSS variables (`var(--x)`),
74
- * and imported values. Return `{ expr, imports }` or `null` to skip.
224
+ * and imported values. Return `{ expr, imports }` or `undefined` to skip.
75
225
  */
76
226
  resolveValue(ctx) {
77
227
  if (ctx.kind === "theme") {
@@ -102,19 +252,19 @@ const adapter = defineAdapter({
102
252
  };
103
253
  }
104
254
 
105
- return null;
255
+ return undefined;
106
256
  },
107
257
 
108
258
  /**
109
259
  * Resolve helper function calls in template interpolations.
110
260
  * e.g. `${transitionSpeed("slow")}` → `transitionSpeedVars.slow`
111
- * Return `{ expr, imports }` or `null` to bail the file with a warning.
261
+ * Return `{ expr, imports }` or `undefined` to bail the file with a warning.
112
262
  */
113
263
  resolveCall(ctx) {
114
264
  const arg0 = ctx.args[0];
115
265
  const key = arg0?.kind === "literal" && typeof arg0.value === "string" ? arg0.value : null;
116
266
  if (ctx.calleeImportedName !== "transitionSpeed" || !key) {
117
- return null;
267
+ return undefined;
118
268
  }
119
269
 
120
270
  return {
@@ -128,6 +278,14 @@ const adapter = defineAdapter({
128
278
  };
129
279
  },
130
280
 
281
+ /**
282
+ * Resolve imported values used in selector position, such as media query
283
+ * helpers or pseudo-class aliases. Return `undefined` to bail the file.
284
+ */
285
+ resolveSelector(ctx) {
286
+ return undefined;
287
+ },
288
+
131
289
  /**
132
290
  * Optional: inline styled(ImportedComponent) into an intrinsic element.
133
291
  * When the base component can be resolved statically, return the target
@@ -153,14 +311,14 @@ const adapter = defineAdapter({
153
311
  },
154
312
 
155
313
  /**
156
- * Control which exported components accept external className/style
157
- * and/or polymorphic `as` prop. Return `{ styles, as }` flags.
314
+ * Control which exported components accept external className/style,
315
+ * polymorphic `as`, and/or refs. Return `{ styles, as, ref }` flags.
158
316
  */
159
317
  externalInterface(ctx) {
160
318
  if (ctx.filePath.includes("/shared/components/")) {
161
- return { styles: true, as: true };
319
+ return { styles: true, as: true, ref: true };
162
320
  }
163
- return { styles: false, as: false };
321
+ return { styles: false, as: false, ref: false };
164
322
  },
165
323
 
166
324
  /**
@@ -228,8 +386,9 @@ Adapters are the main extension point, see full example above. They let you cont
228
386
 
229
387
  - how theme paths, CSS variables, and imported values are turned into StyleX-compatible JS values (`resolveValue`)
230
388
  - what extra imports to inject into transformed files (returned from `resolveValue`)
231
- - how helper calls are resolved (via `resolveCall({ ... })` returning `{ expr, imports }`, or `{ preserveRuntimeCall: true }` to keep only the original helper runtime call; `null`/`undefined` bails the file)
232
- - which exported components should support external className/style extension and/or polymorphic `as` prop (`externalInterface`)
389
+ - how helper calls are resolved (via `resolveCall({ ... })` returning `{ expr, imports }`, or `{ preserveRuntimeCall: true }` to keep only the original helper runtime call; `undefined` bails the file)
390
+ - how imported media-query or pseudo selector helpers are resolved (`resolveSelector`)
391
+ - which exported components should support external className/style extension, polymorphic `as`, and/or refs (`externalInterface`)
233
392
  - how className/style merging is handled for components accepting external styling (`styleMerger`)
234
393
  - which imported components already accept a StyleX `sx` prop (auto-detected from the imported component's prop type when `useSxProp: true`; can be overridden via `wrappedComponentInterface`). When detected, the codemod emits `sx={styles.x}` on the wrapped component instead of `{...stylex.props(styles.x)}`.
235
394
  - which runtime theme hook import/call to use for emitted wrapper theme conditionals (`themeHook`)
@@ -254,7 +413,7 @@ await runTransform({
254
413
 
255
414
  #### Auto-detecting external interface usage (experimental)
256
415
 
257
- Instead of manually specifying which components need `styles` or `as` support, set `externalInterface: "auto"` to auto-detect usage by scanning consumer code.
416
+ Instead of manually specifying which components need `styles`, `as`, or `ref` support, set `externalInterface: "auto"` to auto-detect usage by scanning consumer code.
258
417
 
259
418
  > [!NOTE]
260
419
  > Experimental. Requires `consumerPaths` and a successful prepass scan.
@@ -275,7 +434,7 @@ await runTransform({
275
434
  });
276
435
  ```
277
436
 
278
- When `externalInterface: "auto"` is set, `runTransform()` scans `files` and `consumerPaths` for `styled(Component)` calls and `<Component as={...}>` JSX usage, resolves imports back to the component definition files, and returns the appropriate `{ styles, as }` flags automatically.
437
+ When `externalInterface: "auto"` is set, `runTransform()` scans `files` and `consumerPaths` for `styled(Component)` calls plus JSX usage such as `<Component as={...}>`, `ref`, `className`, and `style`, resolves imports back to the component definition files, and returns the appropriate `{ styles, as, ref }` flags automatically.
279
438
 
280
439
  If that prepass scan fails, `runTransform()` stops and throws an actionable error rather than silently falling back to non-auto behavior.
281
440
 
@@ -344,7 +503,7 @@ When the codemod encounters an interpolation inside a styled template literal, i
344
503
  - Use the optional `usage: "create" | "props"` field to override the default inference
345
504
  - Use `preserveRuntimeCall: true` to keep the original helper call as a runtime style-function
346
505
  override (with or without a static fallback from `expr`)
347
- - if `resolveCall` returns `null` or `undefined`, the transform **bails the file** and logs a warning
506
+ - if `resolveCall` returns `undefined`, the transform **bails the file** and logs a warning
348
507
  - helper calls applied to prop values (e.g. `shadow(props.shadow)`) by emitting a StyleX style function that calls the helper at runtime
349
508
  - conditional CSS blocks via ternary (e.g. `props.$dim ? "opacity: 0.5;" : ""`)
350
509
 
@@ -359,43 +518,6 @@ If the pipeline can't resolve an interpolation:
359
518
  - **createGlobalStyle**: detected usage is reported as an **unsupported-feature** warning (StyleX does not support global styles in the same way).
360
519
  - **Theme prop overrides**: passing a `theme` prop directly to styled components (e.g. `<Button theme={...} />`) is not supported and will bail with a warning.
361
520
 
362
- ## Migration game plan
363
-
364
- ### 1. Define your theme and mixins as StyleX
365
-
366
- Before running the codemod, convert your theme object and shared style helpers into StyleX equivalents:
367
-
368
- ```ts
369
- // tokens.stylex.ts — theme variables
370
- import * as stylex from "@stylexjs/stylex";
371
-
372
- // Before: { colors: { primary: "#0066cc" }, spacing: { sm: "8px" } }
373
- export const colors = stylex.defineVars({ primary: "#0066cc" });
374
- export const spacing = stylex.defineVars({ sm: "8px" });
375
- ```
376
-
377
- ```ts
378
- // helpers.stylex.ts — shared mixins
379
- import * as stylex from "@stylexjs/stylex";
380
-
381
- // Before: export const truncate = () => `white-space: nowrap; overflow: hidden; ...`
382
- export const truncate = stylex.create({
383
- base: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" },
384
- });
385
- ```
386
-
387
- ### 2. Write an adapter and run the codemod
388
-
389
- The adapter maps your project's `props.theme.*` access, CSS variables, and helper calls to the StyleX equivalents from step 1. See [Usage](#usage) for the full API.
390
-
391
- ### 3. Convert bottom-up (leaf components first)
392
-
393
- When a component wraps another component that internally uses styled-components (e.g. `styled(GroupHeader)` where `GroupHeader` renders a `StyledHeader`), CSS cascade conflicts can arise after migration. Convert leaf files — the ones that don't wrap other styled-components — first, then work your way up. The codemod will bail with a warning if it detects this pattern.
394
-
395
- ### 4. Verify, iterate, clean up
396
-
397
- Build and test your project. Review warnings — they tell you which files were skipped and why. Fix adapter gaps, re-run on remaining files, and repeat until done. [Report issues](https://github.com/skovhus/styled-components-to-stylex-codemod/issues) with input/output examples if the codemod produces incorrect results.
398
-
399
521
  ## License
400
522
 
401
523
  MIT
@@ -1,6 +1,6 @@
1
- import { r as escapeRegex } from "./string-utils-Bq7DbB2x.mjs";
2
- import { n as toRealPath } from "./path-utils-GG-vEb5-.mjs";
3
- import { t as isSelectorContext } from "./selector-context-heuristic-DE3JAmpc.mjs";
1
+ import { n as toRealPath } from "./path-utils-BIpoL4Ue.mjs";
2
+ import { r as escapeRegex } from "./string-utils-KggM5TNH.mjs";
3
+ import { t as isSelectorContext } from "./selector-context-heuristic-6_jSRGkZ.mjs";
4
4
  import { readFileSync } from "node:fs";
5
5
  //#region src/internal/bridge-consumer-patcher.ts
6
6
  /**
@@ -0,0 +1,239 @@
1
+ import { i as resolveBarrelReExport, r as findImportSource } from "./extract-external-interface-CdHbvfxu.mjs";
2
+ //#region src/internal/prepass/compute-leaf-set.ts
3
+ /**
4
+ * Computes which styled-component bindings are "leaves" for leaves-only mode:
5
+ * intrinsic bases (`styled.div`) or transitive wrappers around other leaf styled
6
+ * components in the transform set. Uses AST extraction (primary), regex fallback,
7
+ * fixed-point + import resolution (same helpers as consumer analysis).
8
+ */
9
+ const RX_EXPORT_DECL = String.raw`(?:export\s+)?(?:const|let|var)\s+`;
10
+ /** `const Name = styled.tag` — intrinsic HTML/SVG tag member. */
11
+ const STYLED_INTRINSIC_MEMBER_RE = new RegExp(String.raw`\b${RX_EXPORT_DECL}([A-Z][A-Za-z0-9]*)\b[^=]*=\s*styled\.([a-z][a-zA-Z0-9]*)\b`, "g");
12
+ /** `const Name = styled("tag")` — intrinsic string tag. */
13
+ const STYLED_INTRINSIC_STRING_RE = new RegExp(String.raw`\b${RX_EXPORT_DECL}([A-Z][A-Za-z0-9]*)\b[^=]*=\s*styled\s*\(\s*["']([^"']+)["']`, "g");
14
+ /** `const Name = styled(Component)` — wraps another component identifier. */
15
+ const STYLED_COMPONENT_RE = new RegExp(String.raw`\b${RX_EXPORT_DECL}([A-Z][A-Za-z0-9]*)\b[^=]*=\s*styled\s*\(\s*([A-Z][A-Za-z0-9]*)\s*\)`, "g");
16
+ /**
17
+ * Regex-derived styled definition bases for files in the transform set.
18
+ * Later entries for the same component name overwrite earlier ones (rare).
19
+ */
20
+ function extractStyledDefBasesFromSource(filePath, source, into) {
21
+ let map = into.get(filePath);
22
+ if (!map) {
23
+ map = /* @__PURE__ */ new Map();
24
+ into.set(filePath, map);
25
+ }
26
+ STYLED_INTRINSIC_MEMBER_RE.lastIndex = 0;
27
+ for (const m of source.matchAll(STYLED_INTRINSIC_MEMBER_RE)) {
28
+ const name = m[1];
29
+ if (name) map.set(name, { kind: "intrinsic" });
30
+ }
31
+ STYLED_INTRINSIC_STRING_RE.lastIndex = 0;
32
+ for (const m of source.matchAll(STYLED_INTRINSIC_STRING_RE)) {
33
+ const name = m[1];
34
+ if (name) map.set(name, { kind: "intrinsic" });
35
+ }
36
+ STYLED_COMPONENT_RE.lastIndex = 0;
37
+ for (const m of source.matchAll(STYLED_COMPONENT_RE)) {
38
+ const name = m[1];
39
+ const ident = m[2];
40
+ if (name && ident) map.set(name, {
41
+ kind: "component",
42
+ ident
43
+ });
44
+ }
45
+ }
46
+ /**
47
+ * AST-based extraction: understands `let`/`var`, export blocks, named `styled` imports,
48
+ * and `.attrs` / `.withConfig` chains before the tagged template.
49
+ * Results merge into `into`; bindings found here override regex entries for the same name.
50
+ */
51
+ function extractStyledDefBasesFromAstProgram(filePath, program, styledLocalNames, into) {
52
+ if (styledLocalNames.size === 0) return;
53
+ let map = into.get(filePath);
54
+ if (!map) {
55
+ map = /* @__PURE__ */ new Map();
56
+ into.set(filePath, map);
57
+ }
58
+ const body = program.body;
59
+ if (!body) return;
60
+ for (const stmt of body) walkStatement(stmt);
61
+ function walkStatement(stmt) {
62
+ if (stmt.type === "VariableDeclaration") {
63
+ for (const d of stmt.declarations ?? []) processDeclarator(d);
64
+ return;
65
+ }
66
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) walkStatement(stmt.declaration);
67
+ }
68
+ function processDeclarator(decl) {
69
+ if (decl.type !== "VariableDeclarator") return;
70
+ const id = decl.id;
71
+ if (id.type !== "Identifier" || typeof id.name !== "string") return;
72
+ const tpl = findTaggedTemplate(unwrapInitializer(decl.init));
73
+ if (!tpl || tpl.type !== "TaggedTemplateExpression") return;
74
+ const base = classifyStyledTemplateTag(tpl.tag, styledLocalNames);
75
+ if (base) map.set(id.name, base);
76
+ }
77
+ }
78
+ function unwrapInitializer(node) {
79
+ let cur = node ?? void 0;
80
+ while (cur) {
81
+ if (cur.type === "TSAsExpression" || cur.type === "AsExpression") {
82
+ cur = cur.expression;
83
+ continue;
84
+ }
85
+ if (cur.type === "ParenthesizedExpression") {
86
+ cur = cur.expression;
87
+ continue;
88
+ }
89
+ return cur;
90
+ }
91
+ }
92
+ function findTaggedTemplate(node) {
93
+ const n = unwrapInitializer(node);
94
+ if (!n) return;
95
+ if (n.type === "TaggedTemplateExpression") return n;
96
+ }
97
+ /** Peel `.attrs` / `.withConfig` / nested calls down to `styled.div` or `styled(X)`. */
98
+ function peelStyledApplication(tag, styledNames) {
99
+ let cur = tag;
100
+ while (cur) {
101
+ if (cur.type === "CallExpression") {
102
+ const callee = cur.callee;
103
+ if (callee?.type === "MemberExpression") {
104
+ cur = callee;
105
+ continue;
106
+ }
107
+ if (callee?.type === "Identifier" && typeof callee.name === "string" && styledNames.has(callee.name)) return cur;
108
+ return null;
109
+ }
110
+ if (cur.type === "MemberExpression") {
111
+ const obj = cur.object;
112
+ if (obj?.type === "Identifier" && typeof obj.name === "string" && styledNames.has(obj.name)) return cur;
113
+ cur = obj;
114
+ continue;
115
+ }
116
+ break;
117
+ }
118
+ return null;
119
+ }
120
+ function classifyStyledTemplateTag(tag, styledNames) {
121
+ const root = peelStyledApplication(tag, styledNames);
122
+ if (!root) return null;
123
+ if (root.type === "MemberExpression") {
124
+ const obj = root.object;
125
+ const prop = root.property;
126
+ const objName = obj?.type === "Identifier" ? obj.name : void 0;
127
+ if (obj?.type !== "Identifier" || typeof objName !== "string" || !styledNames.has(objName)) return null;
128
+ const isComputed = Boolean(root.computed);
129
+ if (isComputed && prop?.type === "StringLiteral" && typeof prop.value === "string") return { kind: "intrinsic" };
130
+ if (!isComputed && prop?.type === "Identifier" && typeof prop.name === "string") return { kind: "intrinsic" };
131
+ return null;
132
+ }
133
+ if (root.type === "CallExpression") {
134
+ const callee = root.callee;
135
+ const arg0 = root.arguments?.[0];
136
+ const calleeName = callee?.type === "Identifier" ? callee.name : void 0;
137
+ if (callee?.type !== "Identifier" || typeof calleeName !== "string" || !styledNames.has(calleeName) || !arg0) return null;
138
+ if (arg0.type === "Identifier" && typeof arg0.name === "string") return {
139
+ kind: "component",
140
+ ident: arg0.name
141
+ };
142
+ if (arg0.type === "StringLiteral" && typeof arg0.value === "string") return { kind: "intrinsic" };
143
+ return null;
144
+ }
145
+ return null;
146
+ }
147
+ /**
148
+ * Fixed-point: a styled binding is a leaf if its base is intrinsic, or its base
149
+ * component resolves (same-file or import) to another leaf binding in the transform set.
150
+ *
151
+ * @param transformSet - Absolute realpaths of files being transformed
152
+ * @param styledDefBases - From {@link extractStyledDefBasesFromSource}
153
+ * @param resolve - Module path resolver
154
+ * @param cachedRead - Read file source for import resolution
155
+ */
156
+ function computeGlobalLeafKeys(args) {
157
+ const { transformSet, styledDefBases, resolve, cachedRead, toRealPath, resolveBaseComponent } = args;
158
+ /** fileRealPath → Set of local binding names that are leaves */
159
+ const globalLeaves = /* @__PURE__ */ new Map();
160
+ const ensureSet = (file) => {
161
+ let s = globalLeaves.get(file);
162
+ if (!s) {
163
+ s = /* @__PURE__ */ new Set();
164
+ globalLeaves.set(file, s);
165
+ }
166
+ return s;
167
+ };
168
+ const isLeaf = (file, name) => globalLeaves.get(file)?.has(name) ?? false;
169
+ const tryResolveImportedLeaf = (file, ident) => {
170
+ const importInfo = findImportSource(cachedRead(file), ident);
171
+ if (!importInfo) return false;
172
+ const initialDefFile = resolve(importInfo.source, file);
173
+ if (!initialDefFile) return false;
174
+ const defReal = toRealPath(resolveBarrelReExport(initialDefFile, importInfo.isDefault ? "default" : importInfo.exportedName, resolve, cachedRead) ?? initialDefFile);
175
+ if (!transformSet.has(defReal)) return false;
176
+ return leafKeyExists(defReal, importInfo.exportedName, importInfo.isDefault, cachedRead, globalLeaves);
177
+ };
178
+ const tryResolveAdapterIntrinsic = (file, ident) => {
179
+ if (!resolveBaseComponent) return false;
180
+ const importInfo = findImportSource(cachedRead(file), ident);
181
+ if (!importInfo) return false;
182
+ try {
183
+ const result = resolveBaseComponent({
184
+ importSource: importInfo.source,
185
+ importedName: importInfo.exportedName,
186
+ staticProps: {},
187
+ filePath: file
188
+ });
189
+ return typeof result?.tagName === "string" && result.tagName.trim() !== "";
190
+ } catch {
191
+ return false;
192
+ }
193
+ };
194
+ let changed = true;
195
+ while (changed) {
196
+ changed = false;
197
+ for (const [filePath, nameMap] of styledDefBases) {
198
+ const fileReal = toRealPath(filePath);
199
+ if (!transformSet.has(fileReal)) continue;
200
+ for (const [name, base] of nameMap) {
201
+ if (isLeaf(fileReal, name)) continue;
202
+ if (base.kind === "intrinsic") {
203
+ ensureSet(fileReal).add(name);
204
+ changed = true;
205
+ continue;
206
+ }
207
+ const ident = base.ident;
208
+ if (isLeaf(fileReal, ident)) {
209
+ ensureSet(fileReal).add(name);
210
+ changed = true;
211
+ continue;
212
+ }
213
+ if (tryResolveAdapterIntrinsic(filePath, ident)) {
214
+ ensureSet(fileReal).add(name);
215
+ changed = true;
216
+ continue;
217
+ }
218
+ if (tryResolveImportedLeaf(filePath, ident)) {
219
+ ensureSet(fileReal).add(name);
220
+ changed = true;
221
+ }
222
+ }
223
+ }
224
+ }
225
+ const globalLeafKeys = /* @__PURE__ */ new Set();
226
+ for (const [file, names] of globalLeaves) for (const name of names) globalLeafKeys.add(`${file}:${name}`);
227
+ return globalLeafKeys;
228
+ }
229
+ function leafKeyExists(defFile, exportedName, allowDefaultFallback, cachedRead, globalLeaves) {
230
+ if (globalLeaves.get(defFile)?.has(exportedName) ?? false) return true;
231
+ if (!allowDefaultFallback) return false;
232
+ const defaultLocalName = findDefaultExportedLocalName(cachedRead(defFile));
233
+ return defaultLocalName ? globalLeaves.get(defFile)?.has(defaultLocalName) ?? false : false;
234
+ }
235
+ function findDefaultExportedLocalName(source) {
236
+ return source.match(/\bexport\s+default\s+([A-Z][A-Za-z0-9]*)\b/)?.[1] ?? source.match(/\bexport\s*\{[^}]*\b([A-Z][A-Za-z0-9]*)\s+as\s+default\b[^}]*\}/)?.[1];
237
+ }
238
+ //#endregion
239
+ export { extractStyledDefBasesFromAstProgram as n, extractStyledDefBasesFromSource as r, computeGlobalLeafKeys as t };