styled-components-to-stylex-codemod 0.0.39 → 0.0.41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "styled-components-to-stylex-codemod",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "description": "Codemod to transform styled-components to StyleX",
5
5
  "keywords": [
6
6
  "codemod",
@@ -82,9 +82,9 @@
82
82
  "scripts": {
83
83
  "build": "tsdown",
84
84
  "dev": "tsdown --watch",
85
- "test": "vitest",
86
- "test:run": "vitest run",
87
- "test:coverage": "vitest run --coverage",
85
+ "test": "vitest --configLoader runner",
86
+ "test:run": "vitest run --configLoader runner",
87
+ "test:coverage": "vitest run --coverage --configLoader runner",
88
88
  "format": "oxfmt",
89
89
  "format:check": "oxfmt --check",
90
90
  "lint": "concurrently 'pnpm:lint:oxlint' 'pnpm:lint:eslint'",
@@ -1,392 +0,0 @@
1
- //#region src/internal/public-api-validation.ts
2
- function describeValue(value) {
3
- if (value === null) return "null";
4
- if (value === void 0) return "undefined";
5
- if (Array.isArray(value)) return `Array(${value.length})`;
6
- if (typeof value === "string") return `"${value}"`;
7
- if (typeof value === "number" || typeof value === "boolean") return String(value);
8
- if (typeof value === "bigint") return value.toString();
9
- if (typeof value === "symbol") return value.description ? `Symbol(${value.description})` : "Symbol()";
10
- if (typeof value === "function") return "[Function]";
11
- if (typeof value === "object") {
12
- const ctor = value?.constructor?.name ?? "Object";
13
- let keys = [];
14
- try {
15
- keys = Object.keys(value);
16
- } catch {}
17
- const preview = keys.slice(0, 5).join(", ");
18
- const suffix = keys.length > 5 ? ", ..." : "";
19
- return keys.length ? `${ctor} { ${preview}${suffix} }` : ctor;
20
- }
21
- return "[Unknown]";
22
- }
23
- /** Validates that the candidate is a fully-resolved `Adapter` (externalInterface must be a function). */
24
- function assertValidAdapter(candidate, where) {
25
- assertAdapterShape(candidate, where, false);
26
- }
27
- /** Validates that the candidate is a valid `AdapterInput` (externalInterface may be `"auto"` or a function). */
28
- function assertValidAdapterInput(candidate, where) {
29
- assertAdapterShape(candidate, where, true);
30
- }
31
- function assertAdapterShape(candidate, where, allowAutoExtIf) {
32
- const obj = candidate;
33
- const resolveValue = obj?.resolveValue;
34
- const resolveCall = obj?.resolveCall;
35
- const resolveSelector = obj?.resolveSelector;
36
- const resolveBaseComponent = obj?.resolveBaseComponent;
37
- const externalInterface = obj?.externalInterface;
38
- if (!candidate || typeof candidate !== "object") throw new Error([
39
- `${where}: expected an adapter object.`,
40
- `Received: ${describeValue(candidate)}`,
41
- "",
42
- "Adapter requirements:",
43
- " - adapter.resolveValue(context) is required",
44
- " - adapter.resolveCall(context) is required",
45
- " - adapter.resolveSelector(context) is required",
46
- " - adapter.externalInterface(context) is required",
47
- "",
48
- "resolveValue(context) is called with one of these shapes:",
49
- " - { kind: \"theme\", path }",
50
- " - { kind: \"cssVariable\", name, fallback?, definedValue? }",
51
- " - { kind: \"importedValue\", importedName, source, path? }",
52
- "",
53
- "resolveCall(context) is called with:",
54
- " - { callSiteFilePath, calleeImportedName, calleeSource, args }",
55
- "",
56
- "resolveSelector(context) is called with:",
57
- " - { kind: \"selectorInterpolation\", importedName, source, path? }",
58
- " - { kind: \"mediaQueryInterpolation\", importedName, source, path?, mediaQuery }",
59
- "",
60
- `Docs/examples: ${ADAPTER_DOCS_URL}`
61
- ].join("\n"));
62
- if (typeof resolveValue !== "function") throw new Error([
63
- `${where}: adapter.resolveValue must be a function.`,
64
- `Received: resolveValue=${describeValue(resolveValue)}`,
65
- "",
66
- "Adapter shape:",
67
- " {",
68
- " resolveValue(context) {",
69
- " // theme/cssVariable -> { expr, imports, dropDefinition? } | null",
70
- " }",
71
- " resolveCall(context) { return { expr, imports } | null }",
72
- " }",
73
- "",
74
- `Docs/examples: ${ADAPTER_DOCS_URL}`
75
- ].join("\n"));
76
- if (typeof resolveCall !== "function") throw new Error([
77
- `${where}: adapter.resolveCall must be a function.`,
78
- `Received: resolveCall=${describeValue(resolveCall)}`,
79
- "",
80
- "Adapter shape:",
81
- " {",
82
- " resolveCall(context) { return { expr: string, imports: ImportSpec[] } | null }",
83
- " }",
84
- "",
85
- `Docs/examples: ${ADAPTER_DOCS_URL}`
86
- ].join("\n"));
87
- if (typeof resolveSelector !== "function") throw new Error([
88
- `${where}: adapter.resolveSelector must be a function.`,
89
- `Received: resolveSelector=${describeValue(resolveSelector)}`,
90
- "",
91
- "Adapter shape:",
92
- " {",
93
- " resolveSelector(context) { return { kind: \"media\" | \"pseudoAlias\" | \"pseudoExpand\", ... } | undefined }",
94
- " }",
95
- "",
96
- `Docs/examples: ${ADAPTER_DOCS_URL}`
97
- ].join("\n"));
98
- if (resolveBaseComponent !== void 0 && typeof resolveBaseComponent !== "function") throw new Error([
99
- `${where}: adapter.resolveBaseComponent must be a function when provided.`,
100
- `Received: resolveBaseComponent=${describeValue(resolveBaseComponent)}`,
101
- "",
102
- "Adapter shape:",
103
- " {",
104
- " resolveBaseComponent(context) {",
105
- " return { tagName, consumedProps, sx?, mixins? } | undefined",
106
- " }",
107
- " }",
108
- "",
109
- `Docs/examples: ${ADAPTER_DOCS_URL}`
110
- ].join("\n"));
111
- if (!(typeof externalInterface === "function" || allowAutoExtIf && externalInterface === "auto")) throw new Error([`${where}: ${allowAutoExtIf ? "adapter.externalInterface must be a function or \"auto\"." : "adapter.externalInterface must be a function."}`, `Received: externalInterface=${describeValue(externalInterface)}`].join("\n"));
112
- const styleMerger = obj?.styleMerger;
113
- if (styleMerger !== null && styleMerger !== void 0) {
114
- if (typeof styleMerger !== "object") throw new Error([
115
- `${where}: adapter.styleMerger must be null or an object.`,
116
- `Received: styleMerger=${describeValue(styleMerger)}`,
117
- "",
118
- "Expected shape:",
119
- " {",
120
- " functionName: \"stylexProps\",",
121
- " importSource: { kind: \"specifier\", value: \"@company/ui-utils\" }",
122
- " }"
123
- ].join("\n"));
124
- const { functionName, importSource } = styleMerger;
125
- assertFunctionNameAndImportSource({
126
- where,
127
- configPath: "adapter.styleMerger",
128
- functionName,
129
- importSource,
130
- specifierExample: "@company/ui-utils",
131
- absolutePathExample: "/path/to/module.ts"
132
- });
133
- }
134
- const markerFile = obj?.markerFile;
135
- if (markerFile !== void 0 && markerFile !== null && typeof markerFile !== "function") throw new Error([
136
- `${where}: adapter.markerFile must be a function when provided.`,
137
- `Received: markerFile=${describeValue(markerFile)}`,
138
- "",
139
- "Expected signature:",
140
- " markerFile(ctx: { filePath: string }) => { kind: \"specifier\" | \"absolutePath\", value: string }"
141
- ].join("\n"));
142
- const wrappedComponentInterface = obj?.wrappedComponentInterface;
143
- if (wrappedComponentInterface !== void 0 && wrappedComponentInterface !== null && typeof wrappedComponentInterface !== "function") throw new Error([
144
- `${where}: adapter.wrappedComponentInterface must be a function when provided.`,
145
- `Received: wrappedComponentInterface=${describeValue(wrappedComponentInterface)}`,
146
- "",
147
- "Expected signature:",
148
- " wrappedComponentInterface(ctx: { localName: string; importSource: string; importedName: string; filePath: string })",
149
- " => { acceptsSx: boolean } | undefined"
150
- ].join("\n"));
151
- const themeHook = obj?.themeHook;
152
- if (themeHook !== null && themeHook !== void 0) {
153
- if (typeof themeHook !== "object") throw new Error([
154
- `${where}: adapter.themeHook must be an object when provided.`,
155
- `Received: themeHook=${describeValue(themeHook)}`,
156
- "",
157
- "Expected shape:",
158
- " {",
159
- " functionName: \"useTheme\",",
160
- " importSource: { kind: \"specifier\", value: \"@company/theme-hooks\" }",
161
- " }"
162
- ].join("\n"));
163
- const { functionName, importSource } = themeHook;
164
- assertFunctionNameAndImportSource({
165
- where,
166
- configPath: "adapter.themeHook",
167
- functionName,
168
- importSource,
169
- specifierExample: "@company/theme-hooks",
170
- absolutePathExample: "/path/to/theme-hooks.ts"
171
- });
172
- }
173
- }
174
- function assertFunctionNameAndImportSource(args) {
175
- const { where, configPath, functionName, importSource, specifierExample, absolutePathExample } = args;
176
- if (typeof functionName !== "string" || !functionName.trim()) throw new Error([`${where}: ${configPath}.functionName must be a non-empty string.`, `Received: functionName=${describeValue(functionName)}`].join("\n"));
177
- if (!importSource || typeof importSource !== "object") throw new Error([
178
- `${where}: ${configPath}.importSource must be an object.`,
179
- `Received: importSource=${describeValue(importSource)}`,
180
- "",
181
- "Expected shape:",
182
- ` { kind: "specifier", value: "${specifierExample}" }`,
183
- " or",
184
- ` { kind: "absolutePath", value: "${absolutePathExample}" }`
185
- ].join("\n"));
186
- const { kind, value } = importSource;
187
- if (kind !== "specifier" && kind !== "absolutePath") throw new Error([`${where}: ${configPath}.importSource.kind must be "specifier" or "absolutePath".`, `Received: kind=${describeValue(kind)}`].join("\n"));
188
- if (typeof value !== "string" || !value.trim()) throw new Error([`${where}: ${configPath}.importSource.value must be a non-empty string.`, `Received: value=${describeValue(value)}`].join("\n"));
189
- }
190
- const ADAPTER_DOCS_URL = `https://github.com/skovhus/styled-components-to-stylex-codemod#adapter`;
191
- //#endregion
192
- //#region src/adapter.ts
193
- /**
194
- * Adapter entry point for customizing the codemod.
195
- * Core concepts: value resolution hooks and adapter validation.
196
- */
197
- /**
198
- * Type guard: checks whether a resolve result is a directional expansion.
199
- */
200
- function isDirectionalResult(r) {
201
- return "directional" in r;
202
- }
203
- const DEFAULT_THEME_HOOK = {
204
- functionName: "useTheme",
205
- importSource: {
206
- kind: "specifier",
207
- value: "styled-components"
208
- }
209
- };
210
- /**
211
- * Helper for nicer user authoring + type inference.
212
- *
213
- * `defineAdapter(...)` also performs runtime validation (helpful for JS consumers)
214
- * and will throw a descriptive error message if the adapter shape is invalid.
215
- *
216
- * Usage:
217
- * export default defineAdapter({
218
- * resolveValue(ctx) {
219
- * if (ctx.kind === "theme") {
220
- * // For shorthand properties with multi-value tokens, return directional entries
221
- * if (ctx.cssProperty === "padding" && ctx.path === "input.padding") {
222
- * return {
223
- * directional: [
224
- * { prop: "paddingBlock", expr: "$input.paddingBlock", imports: [{ from: { kind: "specifier", value: "./tokens" }, names: [{ imported: "$input" }] }] },
225
- * { prop: "paddingInline", expr: "$input.paddingInline", imports: [{ from: { kind: "specifier", value: "./tokens" }, names: [{ imported: "$input" }] }] },
226
- * ],
227
- * };
228
- * }
229
- * return {
230
- * expr: `tokens.${ctx.path}`,
231
- * imports: [
232
- * { from: { kind: "specifier", value: "./tokens" }, names: [{ imported: "tokens" }] },
233
- * ],
234
- * };
235
- * }
236
- * // Return undefined to bail/skip the file
237
- * },
238
- *
239
- * resolveCall(ctx) {
240
- * // Resolve helper calls inside template interpolations.
241
- * // Use ctx.cssProperty to determine context:
242
- * // - If ctx.cssProperty exists → return a CSS value expression
243
- * // - If ctx.cssProperty is undefined → return a StyleX style object reference
244
- * // Return { expr, imports } or undefined to bail/skip the file
245
- * void ctx;
246
- * },
247
- *
248
- * resolveSelector(ctx) {
249
- * // Resolve imported values used in selector position.
250
- * // Return one of:
251
- * // - { kind: "media", expr, imports } for media queries (e.g., breakpoints.phone)
252
- * // - { kind: "pseudoAlias", values, styleSelectorExpr?, imports? } for pseudo-class expansion
253
- * // - undefined to bail/skip the file
254
- * // For @media placeholders, check ctx.kind === "mediaQueryInterpolation" and
255
- * // use ctx.mediaQuery.feature to choose the correct defineConsts media key.
256
- * void ctx;
257
- * },
258
- *
259
- * // Configure external interface for exported components
260
- * externalInterface(ctx) {
261
- * // Example: Enable styles, `as`, and `ref` for shared components folder
262
- * if (ctx.filePath.includes("/shared/components/")) {
263
- * return { styles: true, as: true, ref: true };
264
- * }
265
- * return { styles: false, as: false, ref: false };
266
- * },
267
- *
268
- * // Optional: provide a custom merger, or use `null` for the default verbose merge output
269
- * styleMerger: null,
270
- *
271
- * // Emit sx={} JSX attributes instead of {...stylex.props()} spreads (requires StyleX ≥0.18)
272
- * useSxProp: false,
273
- *
274
- * // Opt out of logical properties — use paddingTop/Right/Bottom/Left instead of Block/Inline
275
- * // usePhysicalProperties: true,
276
- *
277
- * // Optional: customize runtime theme hook import/call used by emitted wrappers
278
- * themeHook: {
279
- * functionName: "useTheme",
280
- * importSource: { kind: "specifier", value: "styled-components" },
281
- * },
282
- * });
283
- */
284
- function defineAdapter(adapter) {
285
- assertValidAdapterInput(adapter, "defineAdapter(adapter)");
286
- return adapter;
287
- }
288
- //#endregion
289
- //#region src/internal/merge-markers.ts
290
- /**
291
- * Shared utility for merging marker sidecar content.
292
- * Core concepts: deduplication of defineMarker declarations across files.
293
- */
294
- /** Regex matching a marker block: optional JSDoc comment followed by the export line. */
295
- const MARKER_BLOCK_RE = /(?:\/\*\*[^]*?\*\/\n)?export const \w+ = stylex\.defineMarker\(\);/gm;
296
- /** Regex matching just the export line (used for dedup checks). */
297
- const MARKER_EXPORT_RE = /^export const \w+ = stylex\.defineMarker\(\);$/gm;
298
- /** Regex matching a generated defineVars export block. */
299
- const DEFINE_VARS_BLOCK_RE = /export const \w+ = stylex\.defineVars\(\{\n[\s\S]*?\n\}\);/gm;
300
- /** Regex matching just the defineVars export line (used for dedup checks). */
301
- const DEFINE_VARS_EXPORT_RE = /^export const \w+ = stylex\.defineVars\(\{$/gm;
302
- /**
303
- * Merge marker declarations from `incoming` into `base`, appending only new
304
- * marker blocks (JSDoc + export). Returns `base` unchanged if all markers already exist.
305
- */
306
- function mergeMarkerDeclarations(base, incoming) {
307
- const mergedDefineVars = mergeDefineVarsBlocks(base, incoming);
308
- const markerBlocks = getNewBlocks({
309
- base: mergedDefineVars,
310
- incoming,
311
- blockRegex: MARKER_BLOCK_RE,
312
- exportRegex: MARKER_EXPORT_RE
313
- });
314
- const defineVarsBlocks = getNewBlocks({
315
- base: mergedDefineVars,
316
- incoming,
317
- blockRegex: DEFINE_VARS_BLOCK_RE,
318
- exportRegex: DEFINE_VARS_EXPORT_RE
319
- });
320
- const blocksToAdd = [...markerBlocks, ...defineVarsBlocks];
321
- if (blocksToAdd.length === 0) return mergedDefineVars;
322
- let merged = mergedDefineVars;
323
- if (!merged.includes("@stylexjs/stylex")) merged = `import * as stylex from "@stylexjs/stylex";\n\n${merged}`;
324
- return merged.trimEnd() + "\n\n" + blocksToAdd.join("\n\n") + "\n";
325
- }
326
- function getNewBlocks(args) {
327
- const { base, incoming, blockRegex, exportRegex } = args;
328
- const incomingExports = [...incoming.matchAll(exportRegex)].map((m) => m[0]);
329
- if (incomingExports.length === 0) return [];
330
- const newExportLines = incomingExports.filter((line) => !base.includes(line));
331
- if (newExportLines.length === 0) return [];
332
- const newExportSet = new Set(newExportLines);
333
- return [...incoming.matchAll(blockRegex)].map((m) => m[0]).filter((block) => {
334
- const exportLine = block.match(exportRegex);
335
- return exportLine && newExportSet.has(exportLine[0]);
336
- });
337
- }
338
- function mergeDefineVarsBlocks(base, incoming) {
339
- let merged = base;
340
- for (const incomingBlock of incoming.matchAll(DEFINE_VARS_BLOCK_RE)) {
341
- const incomingText = incomingBlock[0];
342
- const exportName = readDefineVarsExportName(incomingText);
343
- if (!exportName) continue;
344
- const existingBlock = findDefineVarsBlockByExportName(merged, exportName);
345
- if (!existingBlock) continue;
346
- const entriesToAdd = getMissingDefineVarsEntries({
347
- existingBlock: existingBlock.text,
348
- incomingBlock: incomingText
349
- });
350
- if (entriesToAdd.length === 0) continue;
351
- const insertionPoint = existingBlock.start + existingBlock.text.lastIndexOf("\n});");
352
- const linesToAdd = entriesToAdd.map((entry) => entry.line);
353
- merged = `${merged.slice(0, insertionPoint)}\n${linesToAdd.join("\n")}${merged.slice(insertionPoint)}`;
354
- }
355
- return merged;
356
- }
357
- function readDefineVarsExportName(block) {
358
- return /^export const ([A-Za-z_$][\w$]*) = stylex\.defineVars\(\{/m.exec(block)?.[1] ?? null;
359
- }
360
- function findDefineVarsBlockByExportName(source, exportName) {
361
- for (const match of source.matchAll(DEFINE_VARS_BLOCK_RE)) {
362
- const text = match[0];
363
- if (readDefineVarsExportName(text) === exportName && match.index !== void 0) return {
364
- text,
365
- start: match.index
366
- };
367
- }
368
- return null;
369
- }
370
- function getMissingDefineVarsEntries(args) {
371
- const { existingBlock, incomingBlock } = args;
372
- const existingKeys = new Set(readDefineVarsEntryKeys(existingBlock));
373
- return readDefineVarsEntries(incomingBlock).filter((entry) => !existingKeys.has(entry.key));
374
- }
375
- function readDefineVarsEntryKeys(block) {
376
- return readDefineVarsEntries(block).map((entry) => entry.key);
377
- }
378
- function readDefineVarsEntries(block) {
379
- return block.split("\n").map((line) => ({
380
- line,
381
- match: /^\s*(?:(["']--[^"']+["'])|([A-Za-z_$][\w$]*))\s*:/.exec(line)
382
- })).filter((entry) => Boolean(entry.match)).map(({ line, match }) => ({
383
- key: normalizeDefineVarsEntryKey(match[1] ?? match[2]),
384
- line
385
- }));
386
- }
387
- function normalizeDefineVarsEntryKey(key) {
388
- if (key.length >= 2 && (key.startsWith("\"") && key.endsWith("\"") || key.startsWith("'") && key.endsWith("'"))) return key.slice(1, -1);
389
- return key;
390
- }
391
- //#endregion
392
- export { assertValidAdapter as a, isDirectionalResult as i, DEFAULT_THEME_HOOK as n, assertValidAdapterInput as o, defineAdapter as r, describeValue as s, mergeMarkerDeclarations as t };