styled-components-to-stylex-codemod 0.0.7 → 0.0.10
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 +82 -58
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +76 -21
- package/dist/logger-BLeJjMzG.mjs +306 -0
- package/dist/logger-DC-1uogs.d.mts +452 -0
- package/dist/transform.d.mts +1 -2
- package/dist/transform.mjs +18194 -7713
- package/package.json +37 -29
- package/dist/logger-BS4Evg0n.d.mts +0 -175
- package/dist/logger-Dlnt1fYP.mjs +0 -156
package/README.md
CHANGED
|
@@ -21,10 +21,7 @@ pnpm add styled-components-to-stylex-codemod
|
|
|
21
21
|
Use `runTransform` to transform files matching a glob pattern:
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
|
-
import {
|
|
25
|
-
runTransform,
|
|
26
|
-
defineAdapter,
|
|
27
|
-
} from "styled-components-to-stylex-codemod";
|
|
24
|
+
import { runTransform, defineAdapter } from "styled-components-to-stylex-codemod";
|
|
28
25
|
|
|
29
26
|
const adapter = defineAdapter({
|
|
30
27
|
resolveValue(ctx) {
|
|
@@ -70,9 +67,7 @@ const adapter = defineAdapter({
|
|
|
70
67
|
.replace(/^--/, "")
|
|
71
68
|
.split("-")
|
|
72
69
|
.filter(Boolean)
|
|
73
|
-
.map((part, i) =>
|
|
74
|
-
i === 0 ? part : part[0]?.toUpperCase() + part.slice(1)
|
|
75
|
-
)
|
|
70
|
+
.map((part, i) => (i === 0 ? part : part[0]?.toUpperCase() + part.slice(1)))
|
|
76
71
|
.join("");
|
|
77
72
|
|
|
78
73
|
// If you care about fallbacks, you can use `fallback` here to decide whether to resolve or not.
|
|
@@ -88,44 +83,44 @@ const adapter = defineAdapter({
|
|
|
88
83
|
};
|
|
89
84
|
}
|
|
90
85
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// `calleeImportedName` is the imported symbol name (works even with aliasing).
|
|
94
|
-
// `calleeSource` tells you where it came from:
|
|
95
|
-
// - { kind: "absolutePath", value: "/abs/path" } for relative imports
|
|
96
|
-
// - { kind: "specifier", value: "some-package/foo" } for package imports
|
|
97
|
-
|
|
98
|
-
if (ctx.calleeImportedName !== "transitionSpeed") {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// If you need to scope resolution to a particular module, you can use:
|
|
103
|
-
// - ctx.calleeSource
|
|
104
|
-
|
|
105
|
-
const arg0 = ctx.args[0];
|
|
106
|
-
const key =
|
|
107
|
-
arg0?.kind === "literal" && typeof arg0.value === "string"
|
|
108
|
-
? arg0.value
|
|
109
|
-
: null;
|
|
110
|
-
if (!key) {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
86
|
+
return null;
|
|
87
|
+
},
|
|
113
88
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
89
|
+
resolveCall(ctx) {
|
|
90
|
+
// Called for template interpolations like: ${transitionSpeed("slowTransition")}
|
|
91
|
+
// `calleeImportedName` is the imported symbol name (works even with aliasing).
|
|
92
|
+
// `calleeSource` tells you where it came from:
|
|
93
|
+
// - { kind: "absolutePath", value: "/abs/path" } for relative imports
|
|
94
|
+
// - { kind: "specifier", value: "some-package/foo" } for package imports
|
|
95
|
+
//
|
|
96
|
+
// The codemod determines how to use the result based on context:
|
|
97
|
+
// - If `ctx.cssProperty` exists (e.g., `border: ${helper()}`) → result is used as a CSS value
|
|
98
|
+
// - If `ctx.cssProperty` is undefined (e.g., `${helper()}`) → result is used as a StyleX style object
|
|
99
|
+
//
|
|
100
|
+
// Use `ctx.cssProperty` to return the appropriate expression for the context.
|
|
101
|
+
|
|
102
|
+
const arg0 = ctx.args[0];
|
|
103
|
+
const key = arg0?.kind === "literal" && typeof arg0.value === "string" ? arg0.value : null;
|
|
104
|
+
if (ctx.calleeImportedName !== "transitionSpeed" || !key) {
|
|
105
|
+
return null;
|
|
125
106
|
}
|
|
126
107
|
|
|
108
|
+
return {
|
|
109
|
+
expr: `transitionSpeedVars.${key}`,
|
|
110
|
+
imports: [
|
|
111
|
+
{
|
|
112
|
+
from: { kind: "specifier", value: "./lib/helpers.stylex" },
|
|
113
|
+
names: [{ imported: "transitionSpeed", local: "transitionSpeedVars" }],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
externalInterface() {
|
|
127
120
|
return null;
|
|
128
121
|
},
|
|
122
|
+
|
|
123
|
+
styleMerger: null,
|
|
129
124
|
});
|
|
130
125
|
|
|
131
126
|
const result = await runTransform({
|
|
@@ -143,10 +138,10 @@ console.log(result);
|
|
|
143
138
|
|
|
144
139
|
Adapters are the main extension point. They let you control:
|
|
145
140
|
|
|
146
|
-
- how theme paths
|
|
141
|
+
- how theme paths, CSS variables, and imported values are turned into StyleX-compatible JS values (`resolveValue`)
|
|
147
142
|
- what extra imports to inject into transformed files (returned from `resolveValue`)
|
|
148
|
-
- how helper calls are resolved (via `
|
|
149
|
-
- which exported components should support external className/style extension (`
|
|
143
|
+
- how helper calls are resolved (via `resolveCall({ ... })` returning `{ expr, imports }`; `null`/`undefined` bails the file)
|
|
144
|
+
- which exported components should support external className/style extension and/or polymorphic `as` prop (`externalInterface`)
|
|
150
145
|
- how className/style merging is handled for components accepting external styling (`styleMerger`)
|
|
151
146
|
|
|
152
147
|
#### Style Merger
|
|
@@ -164,8 +159,15 @@ const adapter = defineAdapter({
|
|
|
164
159
|
return null;
|
|
165
160
|
},
|
|
166
161
|
|
|
167
|
-
|
|
168
|
-
return
|
|
162
|
+
resolveCall() {
|
|
163
|
+
return null;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
externalInterface(ctx) {
|
|
167
|
+
if (ctx.filePath.includes("/shared/components/")) {
|
|
168
|
+
return { styles: true };
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
169
171
|
},
|
|
170
172
|
|
|
171
173
|
// Use a custom merger function for cleaner output
|
|
@@ -182,15 +184,15 @@ The merger function should have this signature:
|
|
|
182
184
|
function mergedSx(
|
|
183
185
|
styles: StyleXStyles,
|
|
184
186
|
className?: string,
|
|
185
|
-
style?: React.CSSProperties
|
|
187
|
+
style?: React.CSSProperties,
|
|
186
188
|
): ReturnType<typeof stylex.props>;
|
|
187
189
|
```
|
|
188
190
|
|
|
189
191
|
See [`test-cases/lib/mergedSx.ts`](./test-cases/lib/mergedSx.ts) for a reference implementation.
|
|
190
192
|
|
|
191
|
-
#### External Styles Support
|
|
193
|
+
#### External Interface (Styles and Polymorphic `as` Support)
|
|
192
194
|
|
|
193
|
-
Transformed components are "closed" by default — they don't accept external `className` or `style` props. Use `
|
|
195
|
+
Transformed components are "closed" by default — they don't accept external `className` or `style` props, and exported components only get `as` support when it is used inside the file. Use `externalInterface` to control which exported components should support these features:
|
|
194
196
|
|
|
195
197
|
```ts
|
|
196
198
|
const adapter = defineAdapter({
|
|
@@ -199,37 +201,59 @@ const adapter = defineAdapter({
|
|
|
199
201
|
return null;
|
|
200
202
|
},
|
|
201
203
|
|
|
202
|
-
|
|
204
|
+
resolveCall() {
|
|
205
|
+
return null;
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
externalInterface(ctx) {
|
|
203
209
|
// ctx: { filePath, componentName, exportName, isDefaultExport }
|
|
204
210
|
|
|
205
|
-
// Example: Enable for all exports in shared components folder
|
|
211
|
+
// Example: Enable styles (and `as`) for all exports in shared components folder
|
|
206
212
|
if (ctx.filePath.includes("/shared/components/")) {
|
|
207
|
-
return true;
|
|
213
|
+
return { styles: true };
|
|
208
214
|
}
|
|
209
215
|
|
|
210
|
-
// Example: Enable
|
|
211
|
-
if (ctx.componentName === "
|
|
212
|
-
return true;
|
|
216
|
+
// Example: Enable only `as` prop (no style merging)
|
|
217
|
+
if (ctx.componentName === "Typography") {
|
|
218
|
+
return { styles: false, as: true };
|
|
213
219
|
}
|
|
214
220
|
|
|
215
|
-
|
|
221
|
+
// Disable both (default)
|
|
222
|
+
return null;
|
|
216
223
|
},
|
|
224
|
+
|
|
225
|
+
styleMerger: null,
|
|
217
226
|
});
|
|
218
227
|
```
|
|
219
228
|
|
|
220
|
-
|
|
229
|
+
The `externalInterface` method returns:
|
|
230
|
+
|
|
231
|
+
- `null` — no external interface (neither className/style nor `as` prop)
|
|
232
|
+
- `{ styles: true }` — accept className/style props AND polymorphic `as` prop
|
|
233
|
+
- `{ styles: false, as: true }` — accept only polymorphic `as` prop (no style merging)
|
|
234
|
+
- `{ styles: false, as: false }` — equivalent to `null`
|
|
235
|
+
|
|
236
|
+
When `styles: true`, the generated component will:
|
|
221
237
|
|
|
222
238
|
- Accept `className` and `style` props
|
|
223
239
|
- Merge them with the StyleX-generated styles
|
|
224
240
|
- Forward remaining props via `...rest`
|
|
241
|
+
- Accept polymorphic `as` prop (required for style merging to work correctly)
|
|
242
|
+
|
|
243
|
+
When `{ styles: false, as: true }`, the generated component will accept a polymorphic `as` prop but won't include className/style merging.
|
|
225
244
|
|
|
226
245
|
#### Dynamic interpolations
|
|
227
246
|
|
|
228
247
|
When the codemod encounters an interpolation inside a styled template literal, it runs an internal dynamic resolution pipeline which covers common cases like:
|
|
229
248
|
|
|
230
249
|
- theme access (`props.theme...`) via `resolveValue({ kind: "theme", path })`
|
|
250
|
+
- imported value access (`import { zIndex } ...; ${zIndex.popover}`) via `resolveValue({ kind: "importedValue", importedName, source, path })`
|
|
231
251
|
- prop access (`props.foo`) and conditionals (`props.foo ? "a" : "b"`, `props.foo && "color: red;"`)
|
|
232
|
-
-
|
|
252
|
+
- helper calls (`transitionSpeed("slowTransition")`) via `resolveCall({ ... })` — the codemod infers usage from context:
|
|
253
|
+
- With `ctx.cssProperty` (e.g., `color: ${helper()}`) → result used as CSS value in `stylex.create()`
|
|
254
|
+
- Without `ctx.cssProperty` (e.g., `${helper()}`) → result used as StyleX styles in `stylex.props()`
|
|
255
|
+
- Use the optional `usage: "create" | "props"` field to override the default inference
|
|
256
|
+
- if `resolveCall` returns `null` or `undefined`, the transform **bails the file** and logs a warning
|
|
233
257
|
- helper calls applied to prop values (e.g. `shadow(props.shadow)`) by emitting a StyleX style function that calls the helper at runtime
|
|
234
258
|
- conditional CSS blocks via ternary (e.g. `props.$dim ? "opacity: 0.5;" : ""`)
|
|
235
259
|
|
|
@@ -241,8 +265,8 @@ If the pipeline can’t resolve an interpolation:
|
|
|
241
265
|
### Limitations
|
|
242
266
|
|
|
243
267
|
- **Flow** type generation is non-existing, works best with TypeScript or plain JS right now. Contributions more than welcome!
|
|
244
|
-
- **ThemeProvider**: if a file imports and uses `ThemeProvider` from `styled-components`, the transform **skips the entire file** (theming strategy is project-specific).
|
|
245
268
|
- **createGlobalStyle**: detected usage is reported as an **unsupported-feature** warning (StyleX does not support global styles in the same way).
|
|
269
|
+
- **Theme prop overrides**: passing a `theme` prop directly to styled components (e.g. `<Button theme={...} />`) is not supported and will bail with a warning.
|
|
246
270
|
|
|
247
271
|
## License
|
|
248
272
|
|
package/dist/index.d.mts
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as assertValidAdapter, r as describeValue, t as Logger } from "./logger-
|
|
1
|
+
import { n as assertValidAdapter, r as describeValue, t as Logger } from "./logger-BLeJjMzG.mjs";
|
|
2
2
|
import { run } from "jscodeshift/src/Runner.js";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
@@ -8,11 +8,15 @@ import { spawn } from "node:child_process";
|
|
|
8
8
|
|
|
9
9
|
//#region src/adapter.ts
|
|
10
10
|
/**
|
|
11
|
-
* Adapter
|
|
11
|
+
* Adapter entry point for customizing the codemod.
|
|
12
|
+
* Core concepts: value resolution hooks and adapter validation.
|
|
12
13
|
*/
|
|
13
14
|
/**
|
|
14
15
|
* Helper for nicer user authoring + type inference.
|
|
15
16
|
*
|
|
17
|
+
* `defineAdapter(...)` also performs runtime validation (helpful for JS consumers)
|
|
18
|
+
* and will throw a descriptive error message if the adapter shape is invalid.
|
|
19
|
+
*
|
|
16
20
|
* Usage:
|
|
17
21
|
* export default defineAdapter({
|
|
18
22
|
* resolveValue(ctx) {
|
|
@@ -24,14 +28,37 @@ import { spawn } from "node:child_process";
|
|
|
24
28
|
* ],
|
|
25
29
|
* };
|
|
26
30
|
* }
|
|
27
|
-
*
|
|
31
|
+
* // Return undefined to bail/skip the file
|
|
32
|
+
* },
|
|
33
|
+
*
|
|
34
|
+
* resolveCall(ctx) {
|
|
35
|
+
* // Resolve helper calls inside template interpolations.
|
|
36
|
+
* // Use ctx.cssProperty to determine context:
|
|
37
|
+
* // - If ctx.cssProperty exists → return a CSS value expression
|
|
38
|
+
* // - If ctx.cssProperty is undefined → return a StyleX style object reference
|
|
39
|
+
* // Return { expr, imports } or undefined to bail/skip the file
|
|
40
|
+
* void ctx;
|
|
28
41
|
* },
|
|
29
42
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* //
|
|
33
|
-
*
|
|
43
|
+
* resolveSelector(ctx) {
|
|
44
|
+
* // Resolve imported values used in selector position (e.g., media query helpers).
|
|
45
|
+
* // Return:
|
|
46
|
+
* // - { kind: "media", expr, imports } for media queries (e.g., breakpoints.phone)
|
|
47
|
+
* // - undefined to bail/skip the file
|
|
48
|
+
* void ctx;
|
|
34
49
|
* },
|
|
50
|
+
*
|
|
51
|
+
* // Configure external interface for exported components
|
|
52
|
+
* externalInterface(ctx) {
|
|
53
|
+
* // Example: Enable styles (and `as`) for shared components folder
|
|
54
|
+
* if (ctx.filePath.includes("/shared/components/")) {
|
|
55
|
+
* return { styles: true };
|
|
56
|
+
* }
|
|
57
|
+
* return null;
|
|
58
|
+
* },
|
|
59
|
+
*
|
|
60
|
+
* // Optional: provide a custom merger, or use `null` for the default verbose merge output
|
|
61
|
+
* styleMerger: null,
|
|
35
62
|
* });
|
|
36
63
|
*/
|
|
37
64
|
function defineAdapter(adapter) {
|
|
@@ -41,6 +68,10 @@ function defineAdapter(adapter) {
|
|
|
41
68
|
|
|
42
69
|
//#endregion
|
|
43
70
|
//#region src/run.ts
|
|
71
|
+
/**
|
|
72
|
+
* Runs the codemod over input files with an adapter.
|
|
73
|
+
* Core concepts: jscodeshift execution, globs, and adapter hooks.
|
|
74
|
+
*/
|
|
44
75
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
45
76
|
/**
|
|
46
77
|
* Run the styled-components to StyleX transform on files matching the glob pattern.
|
|
@@ -92,20 +123,42 @@ async function runTransform(options) {
|
|
|
92
123
|
const { files, dryRun = false, print = false, parser = "tsx", formatterCommand } = options;
|
|
93
124
|
const adapter = options.adapter;
|
|
94
125
|
assertValidAdapter(adapter, "runTransform(options)");
|
|
126
|
+
const resolveValueWithLogging = (ctx) => {
|
|
127
|
+
try {
|
|
128
|
+
return adapter.resolveValue(ctx);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
const msg = `adapter.resolveValue threw an error: ${e instanceof Error ? e.message : String(e)}`;
|
|
131
|
+
const filePath = ctx.filePath ?? "<unknown>";
|
|
132
|
+
Logger.logError(msg, filePath, ctx.loc, ctx);
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const resolveCallWithLogging = (ctx) => {
|
|
137
|
+
try {
|
|
138
|
+
return adapter.resolveCall(ctx);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
const msg = `adapter.resolveCall threw an error: ${e instanceof Error ? e.message : String(e)}`;
|
|
141
|
+
Logger.logError(msg, ctx.callSiteFilePath, ctx.loc, ctx);
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const resolveSelectorWithLogging = (ctx) => {
|
|
146
|
+
try {
|
|
147
|
+
return adapter.resolveSelector(ctx);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
const msg = `adapter.resolveSelector threw an error: ${e instanceof Error ? e.message : String(e)}`;
|
|
150
|
+
Logger.logError(msg, ctx.filePath, ctx.loc, ctx);
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
95
154
|
const adapterWithLogging = {
|
|
96
155
|
styleMerger: adapter.styleMerger,
|
|
97
|
-
|
|
98
|
-
return adapter.
|
|
156
|
+
externalInterface(ctx) {
|
|
157
|
+
return adapter.externalInterface(ctx);
|
|
99
158
|
},
|
|
100
|
-
resolveValue
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
} catch (e) {
|
|
104
|
-
const msg = `adapter.resolveValue threw an error: ${e instanceof Error ? e.message : String(e)}`;
|
|
105
|
-
Logger.error(msg, ctx);
|
|
106
|
-
throw e;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
159
|
+
resolveValue: resolveValueWithLogging,
|
|
160
|
+
resolveCall: resolveCallWithLogging,
|
|
161
|
+
resolveSelector: resolveSelectorWithLogging
|
|
109
162
|
};
|
|
110
163
|
const patterns = Array.isArray(files) ? files : [files];
|
|
111
164
|
const filePaths = [];
|
|
@@ -143,13 +196,13 @@ async function runTransform(options) {
|
|
|
143
196
|
if (formatterCommand && result.ok > 0 && !dryRun) {
|
|
144
197
|
const [cmd, ...cmdArgs] = formatterCommand.split(/\s+/);
|
|
145
198
|
if (cmd) try {
|
|
146
|
-
await new Promise((resolve
|
|
199
|
+
await new Promise((resolve, reject) => {
|
|
147
200
|
const proc = spawn(cmd, [...cmdArgs, ...filePaths], {
|
|
148
201
|
stdio: "inherit",
|
|
149
202
|
shell: true
|
|
150
203
|
});
|
|
151
204
|
proc.on("close", (code) => {
|
|
152
|
-
if (code === 0) resolve
|
|
205
|
+
if (code === 0) resolve();
|
|
153
206
|
else reject(/* @__PURE__ */ new Error(`Formatter command exited with code ${code}`));
|
|
154
207
|
});
|
|
155
208
|
proc.on("error", reject);
|
|
@@ -158,13 +211,15 @@ async function runTransform(options) {
|
|
|
158
211
|
Logger.warn(`Formatter command failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
159
212
|
}
|
|
160
213
|
}
|
|
214
|
+
const report = Logger.createReport();
|
|
215
|
+
report.print();
|
|
161
216
|
return {
|
|
162
217
|
errors: result.error,
|
|
163
218
|
unchanged: result.nochange,
|
|
164
219
|
skipped: result.skip,
|
|
165
220
|
transformed: result.ok,
|
|
166
221
|
timeElapsed: parseFloat(result.timeElapsed) || 0,
|
|
167
|
-
warnings:
|
|
222
|
+
warnings: report.getWarnings()
|
|
168
223
|
};
|
|
169
224
|
}
|
|
170
225
|
|