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 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
- shouldSupportExternalStyling() {
123
- return false;
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 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`)
143
142
  - what extra imports to inject into transformed files (returned from `resolveValue`)
144
- - how helper calls are resolved (via `resolveCall({ ... })` returning `usage: "props" | "create"`; `null`/`undefined` now bails)
145
- - 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`)
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
- shouldSupportExternalStyling(ctx) {
164
- 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;
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 `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:
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
- shouldSupportExternalStyling(ctx) {
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 for specific component names
207
- if (ctx.componentName === "Button" || ctx.componentName === "Card") {
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
- return false;
221
+ // Disable both (default)
222
+ return null;
212
223
  },
224
+
225
+ styleMerger: null,
213
226
  });
214
227
  ```
215
228
 
216
- 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:
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
- - simple helper calls (`transitionSpeed("slowTransition")`) via `resolveCall({ ... })` returning `usage: "create"`
229
- - style helper calls (returning StyleX styles) via `resolveCall({ ... })` returning `usage: "props"`; these are emitted as extra `stylex.props(...)` args
230
- - if `resolveCall` returns `null` or `undefined`, the transform now **bails the file** and logs a warning
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-CNmtK-uJ.mjs";
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-DKelw2HS.mjs";
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 - 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.
@@ -27,23 +28,34 @@ import { spawn } from "node:child_process";
27
28
  * ],
28
29
  * };
29
30
  * }
30
- * return null;
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
- * // Return:
36
- * // - { usage: "props", expr, imports } for StyleX styles (usable in stylex.props)
37
- * // - { usage: "create", expr, imports } for a single value (usable in stylex.create)
38
- * // - null to leave the call unresolved
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
- * // Enable className/style/rest support for exported components
44
- * shouldSupportExternalStyling(ctx) {
45
- * // Example: Enable for all exported components in a shared components folder
46
- * return ctx.filePath.includes("/shared/components/");
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, void 0, ctx);
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, void 0, ctx);
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
- shouldSupportExternalStyling(ctx) {
133
- return adapter.shouldSupportExternalStyling(ctx);
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$1, reject) => {
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$1();
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: Logger.flushWarnings()
225
+ warnings: report.getWarnings()
196
226
  };
197
227
  }
198
228