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 +178 -56
- package/dist/{bridge-consumer-patcher-DbMPzKPE.mjs → bridge-consumer-patcher-BzAIO9pC.mjs} +3 -3
- package/dist/compute-leaf-set-Drcu2eju.mjs +239 -0
- package/dist/{logger-fIHHMZYO.mjs → extract-external-interface-CdHbvfxu.mjs} +124 -2
- package/dist/{forwarded-as-consumer-patcher-CXfXrvkk.mjs → forwarded-as-consumer-patcher-Cs0X-olz.mjs} +2 -2
- package/dist/index.d.mts +12 -1
- package/dist/index.mjs +204 -34
- package/dist/{merge-markers-B4EyveCx.mjs → merge-markers-BC5YNB7D.mjs} +86 -9
- package/dist/{run-prepass-DUxB6hxo.mjs → run-prepass-Us5SBTib.mjs} +71 -108
- package/dist/{string-utils-Bq7DbB2x.mjs → string-utils-KggM5TNH.mjs} +21 -1
- package/dist/styled-css-BVR82jN5.mjs +72 -0
- package/dist/{logger-BoGU2nCP.d.mts → transform-types--9qCqNSJ.d.mts} +181 -8
- package/dist/transform.d.mts +2 -83
- package/dist/transform.mjs +4046 -862
- package/dist/{transient-prop-consumer-patcher-C5KQ2iFe.mjs → transient-prop-consumer-patcher-DLsKxg1R.mjs} +26 -7
- package/package.json +1 -2
- package/dist/styled-css-Bu2bjAUW.mjs +0 -36
- /package/dist/{path-utils-GG-vEb5-.mjs → path-utils-BIpoL4Ue.mjs} +0 -0
- /package/dist/{selector-context-heuristic-DE3JAmpc.mjs → selector-context-heuristic-6_jSRGkZ.mjs} +0 -0
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
|
-
##
|
|
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
|
-
|
|
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
|
|
171
|
+
return undefined;
|
|
26
172
|
},
|
|
27
173
|
// Map helper function calls to StyleX expressions
|
|
28
174
|
resolveCall(ctx) {
|
|
29
|
-
return
|
|
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
|
|
181
|
+
// Control which components accept external className/style, polymorphic `as`, and refs
|
|
32
182
|
externalInterface(ctx) {
|
|
33
|
-
return {
|
|
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 `
|
|
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
|
|
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 `
|
|
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
|
|
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
|
-
*
|
|
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; `
|
|
232
|
-
-
|
|
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 `
|
|
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
|
|
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 `
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import { t as isSelectorContext } from "./selector-context-heuristic-
|
|
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 };
|