styled-components-to-stylex-codemod 0.0.9 → 0.0.11
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 +60 -34
- package/dist/index.d.mts +6 -1
- package/dist/index.mjs +51 -21
- package/dist/logger-BkySFMoy.d.mts +473 -0
- package/dist/logger-D3j-qxgZ.mjs +339 -0
- package/dist/transform.d.mts +1 -2
- package/dist/transform.mjs +19422 -10039
- package/package.json +34 -27
- package/dist/logger-CNmtK-uJ.d.mts +0 -250
- package/dist/logger-DKelw2HS.mjs +0 -177
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.
|
|
@@ -97,18 +92,20 @@ const adapter = defineAdapter({
|
|
|
97
92
|
// `calleeSource` tells you where it came from:
|
|
98
93
|
// - { kind: "absolutePath", value: "/abs/path" } for relative imports
|
|
99
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.
|
|
100
101
|
|
|
101
102
|
const arg0 = ctx.args[0];
|
|
102
|
-
const key =
|
|
103
|
-
arg0?.kind === "literal" && typeof arg0.value === "string"
|
|
104
|
-
? arg0.value
|
|
105
|
-
: null;
|
|
103
|
+
const key = arg0?.kind === "literal" && typeof arg0.value === "string" ? arg0.value : null;
|
|
106
104
|
if (ctx.calleeImportedName !== "transitionSpeed" || !key) {
|
|
107
105
|
return null;
|
|
108
106
|
}
|
|
109
107
|
|
|
110
108
|
return {
|
|
111
|
-
usage: "create",
|
|
112
109
|
expr: `transitionSpeedVars.${key}`,
|
|
113
110
|
imports: [
|
|
114
111
|
{
|
|
@@ -119,9 +116,11 @@ const adapter = defineAdapter({
|
|
|
119
116
|
};
|
|
120
117
|
},
|
|
121
118
|
|
|
122
|
-
|
|
123
|
-
return
|
|
119
|
+
externalInterface() {
|
|
120
|
+
return null;
|
|
124
121
|
},
|
|
122
|
+
|
|
123
|
+
styleMerger: null,
|
|
125
124
|
});
|
|
126
125
|
|
|
127
126
|
const result = await runTransform({
|
|
@@ -139,10 +138,10 @@ console.log(result);
|
|
|
139
138
|
|
|
140
139
|
Adapters are the main extension point. They let you control:
|
|
141
140
|
|
|
142
|
-
- how theme paths
|
|
141
|
+
- how theme paths, CSS variables, and imported values are turned into StyleX-compatible JS values (`resolveValue`)
|
|
143
142
|
- what extra imports to inject into transformed files (returned from `resolveValue`)
|
|
144
|
-
- how helper calls are resolved (via `resolveCall({ ... })` returning `
|
|
145
|
-
- 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`)
|
|
146
145
|
- how className/style merging is handled for components accepting external styling (`styleMerger`)
|
|
147
146
|
|
|
148
147
|
#### Style Merger
|
|
@@ -160,8 +159,15 @@ const adapter = defineAdapter({
|
|
|
160
159
|
return null;
|
|
161
160
|
},
|
|
162
161
|
|
|
163
|
-
|
|
164
|
-
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;
|
|
165
171
|
},
|
|
166
172
|
|
|
167
173
|
// Use a custom merger function for cleaner output
|
|
@@ -178,15 +184,15 @@ The merger function should have this signature:
|
|
|
178
184
|
function mergedSx(
|
|
179
185
|
styles: StyleXStyles,
|
|
180
186
|
className?: string,
|
|
181
|
-
style?: React.CSSProperties
|
|
187
|
+
style?: React.CSSProperties,
|
|
182
188
|
): ReturnType<typeof stylex.props>;
|
|
183
189
|
```
|
|
184
190
|
|
|
185
191
|
See [`test-cases/lib/mergedSx.ts`](./test-cases/lib/mergedSx.ts) for a reference implementation.
|
|
186
192
|
|
|
187
|
-
#### External Styles Support
|
|
193
|
+
#### External Interface (Styles and Polymorphic `as` Support)
|
|
188
194
|
|
|
189
|
-
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:
|
|
190
196
|
|
|
191
197
|
```ts
|
|
192
198
|
const adapter = defineAdapter({
|
|
@@ -195,39 +201,59 @@ const adapter = defineAdapter({
|
|
|
195
201
|
return null;
|
|
196
202
|
},
|
|
197
203
|
|
|
198
|
-
|
|
204
|
+
resolveCall() {
|
|
205
|
+
return null;
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
externalInterface(ctx) {
|
|
199
209
|
// ctx: { filePath, componentName, exportName, isDefaultExport }
|
|
200
210
|
|
|
201
|
-
// Example: Enable for all exports in shared components folder
|
|
211
|
+
// Example: Enable styles (and `as`) for all exports in shared components folder
|
|
202
212
|
if (ctx.filePath.includes("/shared/components/")) {
|
|
203
|
-
return true;
|
|
213
|
+
return { styles: true };
|
|
204
214
|
}
|
|
205
215
|
|
|
206
|
-
// Example: Enable
|
|
207
|
-
if (ctx.componentName === "
|
|
208
|
-
return true;
|
|
216
|
+
// Example: Enable only `as` prop (no style merging)
|
|
217
|
+
if (ctx.componentName === "Typography") {
|
|
218
|
+
return { styles: false, as: true };
|
|
209
219
|
}
|
|
210
220
|
|
|
211
|
-
|
|
221
|
+
// Disable both (default)
|
|
222
|
+
return null;
|
|
212
223
|
},
|
|
224
|
+
|
|
225
|
+
styleMerger: null,
|
|
213
226
|
});
|
|
214
227
|
```
|
|
215
228
|
|
|
216
|
-
|
|
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:
|
|
217
237
|
|
|
218
238
|
- Accept `className` and `style` props
|
|
219
239
|
- Merge them with the StyleX-generated styles
|
|
220
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.
|
|
221
244
|
|
|
222
245
|
#### Dynamic interpolations
|
|
223
246
|
|
|
224
247
|
When the codemod encounters an interpolation inside a styled template literal, it runs an internal dynamic resolution pipeline which covers common cases like:
|
|
225
248
|
|
|
226
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 })`
|
|
227
251
|
- prop access (`props.foo`) and conditionals (`props.foo ? "a" : "b"`, `props.foo && "color: red;"`)
|
|
228
|
-
-
|
|
229
|
-
-
|
|
230
|
-
-
|
|
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
|
|
231
257
|
- helper calls applied to prop values (e.g. `shadow(props.shadow)`) by emitting a StyleX style function that calls the helper at runtime
|
|
232
258
|
- conditional CSS blocks via ternary (e.g. `props.$dim ? "opacity: 0.5;" : ""`)
|
|
233
259
|
|
|
@@ -239,8 +265,8 @@ If the pipeline can’t resolve an interpolation:
|
|
|
239
265
|
### Limitations
|
|
240
266
|
|
|
241
267
|
- **Flow** type generation is non-existing, works best with TypeScript or plain JS right now. Contributions more than welcome!
|
|
242
|
-
- **ThemeProvider**: if a file imports and uses `ThemeProvider` from `styled-components`, the transform **skips the entire file** (theming strategy is project-specific).
|
|
243
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.
|
|
244
270
|
|
|
245
271
|
## License
|
|
246
272
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as defineAdapter, r as Adapter, t as CollectedWarning } from "./logger-
|
|
1
|
+
import { i as defineAdapter, r as Adapter, t as CollectedWarning } from "./logger-BkySFMoy.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/run.d.ts
|
|
4
4
|
interface RunTransformOptions {
|
|
@@ -33,6 +33,11 @@ interface RunTransformOptions {
|
|
|
33
33
|
* @example "pnpm prettier --write"
|
|
34
34
|
*/
|
|
35
35
|
formatterCommand?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Maximum number of examples shown per warning category in the summary.
|
|
38
|
+
* @default 15
|
|
39
|
+
*/
|
|
40
|
+
maxExamples?: number;
|
|
36
41
|
}
|
|
37
42
|
interface RunTransformResult {
|
|
38
43
|
/** Number of files that had errors */
|
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-D3j-qxgZ.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,7 +8,8 @@ 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.
|
|
@@ -27,23 +28,34 @@ import { spawn } from "node:child_process";
|
|
|
27
28
|
* ],
|
|
28
29
|
* };
|
|
29
30
|
* }
|
|
30
|
-
*
|
|
31
|
+
* // Return undefined to bail/skip the file
|
|
31
32
|
* },
|
|
32
33
|
*
|
|
33
34
|
* resolveCall(ctx) {
|
|
34
35
|
* // Resolve helper calls inside template interpolations.
|
|
35
|
-
* //
|
|
36
|
-
* // -
|
|
37
|
-
* // -
|
|
38
|
-
* //
|
|
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
|
|
39
40
|
* void ctx;
|
|
40
|
-
* return null;
|
|
41
41
|
* },
|
|
42
42
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* //
|
|
46
|
-
*
|
|
43
|
+
* resolveSelector(ctx) {
|
|
44
|
+
* // Resolve imported values used in selector position.
|
|
45
|
+
* // Return one of:
|
|
46
|
+
* // - { kind: "media", expr, imports } for media queries (e.g., breakpoints.phone)
|
|
47
|
+
* // - { kind: "pseudoAlias", values, styleSelectorExpr?, imports? } for pseudo-class expansion
|
|
48
|
+
* // - undefined to bail/skip the file
|
|
49
|
+
* void ctx;
|
|
50
|
+
* },
|
|
51
|
+
*
|
|
52
|
+
* // Configure external interface for exported components
|
|
53
|
+
* externalInterface(ctx) {
|
|
54
|
+
* // Example: Enable styles (and `as`) for shared components folder
|
|
55
|
+
* if (ctx.filePath.includes("/shared/components/")) {
|
|
56
|
+
* return { styles: true };
|
|
57
|
+
* }
|
|
58
|
+
* return null;
|
|
47
59
|
* },
|
|
48
60
|
*
|
|
49
61
|
* // Optional: provide a custom merger, or use `null` for the default verbose merge output
|
|
@@ -57,6 +69,10 @@ function defineAdapter(adapter) {
|
|
|
57
69
|
|
|
58
70
|
//#endregion
|
|
59
71
|
//#region src/run.ts
|
|
72
|
+
/**
|
|
73
|
+
* Runs the codemod over input files with an adapter.
|
|
74
|
+
* Core concepts: jscodeshift execution, globs, and adapter hooks.
|
|
75
|
+
*/
|
|
60
76
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
61
77
|
/**
|
|
62
78
|
* Run the styled-components to StyleX transform on files matching the glob pattern.
|
|
@@ -105,7 +121,8 @@ async function runTransform(options) {
|
|
|
105
121
|
if (filesValue.length === 0) throw new Error(["runTransform(options): `files` must not be an empty array.", "Example: files: [\"src/**/*.ts\", \"src/**/*.tsx\"]"].join("\n"));
|
|
106
122
|
if (filesValue.find((p) => typeof p !== "string" || p.trim() === "") !== void 0) throw new Error(["runTransform(options): `files` array must contain non-empty strings.", `Received: files=${describeValue(filesValue)}`].join("\n"));
|
|
107
123
|
}
|
|
108
|
-
const { files, dryRun = false, print = false, parser = "tsx", formatterCommand } = options;
|
|
124
|
+
const { files, dryRun = false, print = false, parser = "tsx", formatterCommand, maxExamples } = options;
|
|
125
|
+
if (maxExamples !== void 0) Logger.setMaxExamples(maxExamples);
|
|
109
126
|
const adapter = options.adapter;
|
|
110
127
|
assertValidAdapter(adapter, "runTransform(options)");
|
|
111
128
|
const resolveValueWithLogging = (ctx) => {
|
|
@@ -114,7 +131,7 @@ async function runTransform(options) {
|
|
|
114
131
|
} catch (e) {
|
|
115
132
|
const msg = `adapter.resolveValue threw an error: ${e instanceof Error ? e.message : String(e)}`;
|
|
116
133
|
const filePath = ctx.filePath ?? "<unknown>";
|
|
117
|
-
Logger.logError(msg, filePath,
|
|
134
|
+
Logger.logError(msg, filePath, ctx.loc, ctx);
|
|
118
135
|
throw e;
|
|
119
136
|
}
|
|
120
137
|
};
|
|
@@ -123,17 +140,27 @@ async function runTransform(options) {
|
|
|
123
140
|
return adapter.resolveCall(ctx);
|
|
124
141
|
} catch (e) {
|
|
125
142
|
const msg = `adapter.resolveCall threw an error: ${e instanceof Error ? e.message : String(e)}`;
|
|
126
|
-
Logger.logError(msg, ctx.callSiteFilePath,
|
|
143
|
+
Logger.logError(msg, ctx.callSiteFilePath, ctx.loc, ctx);
|
|
144
|
+
throw e;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const resolveSelectorWithLogging = (ctx) => {
|
|
148
|
+
try {
|
|
149
|
+
return adapter.resolveSelector(ctx);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
const msg = `adapter.resolveSelector threw an error: ${e instanceof Error ? e.message : String(e)}`;
|
|
152
|
+
Logger.logError(msg, ctx.filePath, ctx.loc, ctx);
|
|
127
153
|
throw e;
|
|
128
154
|
}
|
|
129
155
|
};
|
|
130
156
|
const adapterWithLogging = {
|
|
131
157
|
styleMerger: adapter.styleMerger,
|
|
132
|
-
|
|
133
|
-
return adapter.
|
|
158
|
+
externalInterface(ctx) {
|
|
159
|
+
return adapter.externalInterface(ctx);
|
|
134
160
|
},
|
|
135
161
|
resolveValue: resolveValueWithLogging,
|
|
136
|
-
resolveCall: resolveCallWithLogging
|
|
162
|
+
resolveCall: resolveCallWithLogging,
|
|
163
|
+
resolveSelector: resolveSelectorWithLogging
|
|
137
164
|
};
|
|
138
165
|
const patterns = Array.isArray(files) ? files : [files];
|
|
139
166
|
const filePaths = [];
|
|
@@ -150,6 +177,7 @@ async function runTransform(options) {
|
|
|
150
177
|
warnings: []
|
|
151
178
|
};
|
|
152
179
|
}
|
|
180
|
+
Logger.setFileCount(filePaths.length);
|
|
153
181
|
const result = await run((() => {
|
|
154
182
|
const adjacent = join(__dirname, "transform.mjs");
|
|
155
183
|
if (existsSync(adjacent)) return adjacent;
|
|
@@ -171,13 +199,13 @@ async function runTransform(options) {
|
|
|
171
199
|
if (formatterCommand && result.ok > 0 && !dryRun) {
|
|
172
200
|
const [cmd, ...cmdArgs] = formatterCommand.split(/\s+/);
|
|
173
201
|
if (cmd) try {
|
|
174
|
-
await new Promise((resolve
|
|
202
|
+
await new Promise((resolve, reject) => {
|
|
175
203
|
const proc = spawn(cmd, [...cmdArgs, ...filePaths], {
|
|
176
204
|
stdio: "inherit",
|
|
177
205
|
shell: true
|
|
178
206
|
});
|
|
179
207
|
proc.on("close", (code) => {
|
|
180
|
-
if (code === 0) resolve
|
|
208
|
+
if (code === 0) resolve();
|
|
181
209
|
else reject(/* @__PURE__ */ new Error(`Formatter command exited with code ${code}`));
|
|
182
210
|
});
|
|
183
211
|
proc.on("error", reject);
|
|
@@ -186,13 +214,15 @@ async function runTransform(options) {
|
|
|
186
214
|
Logger.warn(`Formatter command failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
187
215
|
}
|
|
188
216
|
}
|
|
217
|
+
const report = Logger.createReport();
|
|
218
|
+
report.print();
|
|
189
219
|
return {
|
|
190
220
|
errors: result.error,
|
|
191
221
|
unchanged: result.nochange,
|
|
192
222
|
skipped: result.skip,
|
|
193
223
|
transformed: result.ok,
|
|
194
224
|
timeElapsed: parseFloat(result.timeElapsed) || 0,
|
|
195
|
-
warnings:
|
|
225
|
+
warnings: report.getWarnings()
|
|
196
226
|
};
|
|
197
227
|
}
|
|
198
228
|
|