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 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
- if (ctx.kind === "call") {
92
- // Called for template interpolations like: ${transitionSpeed("slowTransition")}
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
- return {
115
- expr: `transitionSpeedVars.${key}`,
116
- imports: [
117
- {
118
- from: { kind: "specifier", value: "./lib/helpers.stylex" },
119
- names: [
120
- { imported: "transitionSpeed", local: "transitionSpeedVars" },
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 and CSS variables are turned into StyleX-compatible JS values (`resolveValue`)
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 `resolveValue({ kind: "call", ... })`)
149
- - which exported components should support external className/style extension (`shouldSupportExternalStyling`)
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
- shouldSupportExternalStyling(ctx) {
168
- return ctx.filePath.includes("/shared/components/");
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 `shouldSupportExternalStyling` to control which exported components should support external styling:
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
- shouldSupportExternalStyling(ctx) {
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 for specific component names
211
- if (ctx.componentName === "Button" || ctx.componentName === "Card") {
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
- return false;
221
+ // Disable both (default)
222
+ return null;
216
223
  },
224
+
225
+ styleMerger: null,
217
226
  });
218
227
  ```
219
228
 
220
- When `shouldSupportExternalStyling` returns `true`, the generated component will:
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
- - simple helper calls (`transitionSpeed("slowTransition")`) via `resolveValue({ kind: "call", calleeImportedName, calleeSource, args, ... })`
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
@@ -1,4 +1,4 @@
1
- import { i as defineAdapter, r as Adapter, t as CollectedWarning } from "./logger-BS4Evg0n.mjs";
1
+ import { i as defineAdapter, r as Adapter, t as CollectedWarning } from "./logger-DC-1uogs.mjs";
2
2
 
3
3
  //#region src/run.d.ts
4
4
  interface RunTransformOptions {
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as assertValidAdapter, r as describeValue, t as Logger } from "./logger-Dlnt1fYP.mjs";
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 - Single user entry point for customizing the codemod.
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
- * return null;
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
- * // Enable className/style/rest support for exported components
31
- * shouldSupportExternalStyling(ctx) {
32
- * // Example: Enable for all exported components in a shared components folder
33
- * return ctx.filePath.includes("/shared/components/");
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
- shouldSupportExternalStyling(ctx) {
98
- return adapter.shouldSupportExternalStyling(ctx);
156
+ externalInterface(ctx) {
157
+ return adapter.externalInterface(ctx);
99
158
  },
100
- resolveValue(ctx) {
101
- try {
102
- return adapter.resolveValue(ctx);
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$1, reject) => {
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$1();
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: Logger.flushWarnings()
222
+ warnings: report.getWarnings()
168
223
  };
169
224
  }
170
225