styled-components-to-stylex-codemod 0.0.19 → 0.0.21
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 +3 -1
- package/dist/{bridge-consumer-patcher-DhMoL4Od.mjs → bridge-consumer-patcher-C2mFaVmd.mjs} +1 -1
- package/dist/forwarded-as-consumer-patcher-GPnjkzc_.mjs +91 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +21 -6
- package/dist/{logger-D7X3KrX1.mjs → logger-Bi5-MGBs.mjs} +3 -3
- package/dist/{logger-DyMTX-U6.d.mts → logger-tKRY0yOA.d.mts} +54 -14
- package/dist/{run-prepass-ByjC18e6.mjs → run-prepass-BuYfEK4M.mjs} +67 -36
- package/dist/string-utils-Dmym5IkG.mjs +80 -0
- package/dist/transform.d.mts +1 -1
- package/dist/transform.mjs +2555 -705
- package/package.json +1 -1
- /package/dist/{resolve-imports-BDk6Ms09.mjs → resolve-imports-4bFqrkrQ.mjs} +0 -0
- /package/dist/{selector-context-heuristic-CGwiJ3HL.mjs → selector-context-heuristic-Cki9_tTH.mjs} +0 -0
package/README.md
CHANGED
|
@@ -192,7 +192,7 @@ Adapters are the main extension point, see full example above. They let you cont
|
|
|
192
192
|
|
|
193
193
|
- how theme paths, CSS variables, and imported values are turned into StyleX-compatible JS values (`resolveValue`)
|
|
194
194
|
- what extra imports to inject into transformed files (returned from `resolveValue`)
|
|
195
|
-
- how helper calls are resolved (via `resolveCall({ ... })` returning `{ expr, imports }
|
|
195
|
+
- how helper calls are resolved (via `resolveCall({ ... })` returning `{ expr, imports }`, or `{ preserveRuntimeCall: true }` to keep only the original helper runtime call; `null`/`undefined` bails the file)
|
|
196
196
|
- which exported components should support external className/style extension and/or polymorphic `as` prop (`externalInterface`)
|
|
197
197
|
- how className/style merging is handled for components accepting external styling (`styleMerger`)
|
|
198
198
|
- which runtime theme hook import/call to use for emitted wrapper theme conditionals (`themeHook`)
|
|
@@ -304,6 +304,8 @@ When the codemod encounters an interpolation inside a styled template literal, i
|
|
|
304
304
|
- With `ctx.cssProperty` (e.g., `color: ${helper()}`) → result used as CSS value in `stylex.create()`
|
|
305
305
|
- Without `ctx.cssProperty` (e.g., `${helper()}`) → result used as StyleX styles in `stylex.props()`
|
|
306
306
|
- Use the optional `usage: "create" | "props"` field to override the default inference
|
|
307
|
+
- Use `preserveRuntimeCall: true` to keep the original helper call as a runtime style-function
|
|
308
|
+
override (with or without a static fallback from `expr`)
|
|
307
309
|
- if `resolveCall` returns `null` or `undefined`, the transform **bails the file** and logs a warning
|
|
308
310
|
- helper calls applied to prop values (e.g. `shadow(props.shadow)`) by emitting a StyleX style function that calls the helper at runtime
|
|
309
311
|
- conditional CSS blocks via ternary (e.g. `props.$dim ? "opacity: 0.5;" : ""`)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { n as escapeRegex } from "./string-utils-Dmym5IkG.mjs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/forwarded-as-consumer-patcher.ts
|
|
6
|
+
/**
|
|
7
|
+
* Post-transform consumer patching for `as` → `forwardedAs`.
|
|
8
|
+
*
|
|
9
|
+
* When a component (e.g., `Flex`) is converted from styled-components to StyleX,
|
|
10
|
+
* unconverted consumer files that wrap it with `styled(Flex)` break when using
|
|
11
|
+
* the `as` prop — styled-components intercepts `as` and replaces `Flex` entirely,
|
|
12
|
+
* losing all StyleX styles.
|
|
13
|
+
*
|
|
14
|
+
* `forwardedAs` tells styled-components to pass the prop through to the wrapped
|
|
15
|
+
* component's own `as` prop, preserving StyleX styles.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Filter prepass consumers to exclude:
|
|
19
|
+
* - consumers that were actually transformed (no longer use styled-components)
|
|
20
|
+
* - entries whose wrapped target bailed and didn't actually transform
|
|
21
|
+
*
|
|
22
|
+
* Returns a map of consumer paths → entries to patch.
|
|
23
|
+
*/
|
|
24
|
+
function buildForwardedAsReplacements(prepassConsumers, transformedFiles) {
|
|
25
|
+
const result = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const [consumerPath, entries] of prepassConsumers) {
|
|
27
|
+
if (transformedFiles.has(toRealPath(consumerPath))) continue;
|
|
28
|
+
const surviving = entries.filter((e) => transformedFiles.has(e.targetPath));
|
|
29
|
+
if (surviving.length > 0) result.set(consumerPath, surviving);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Patch a single consumer file: replace `as` with `forwardedAs` in JSX props
|
|
35
|
+
* and `.attrs()` calls for the given styled wrapper components.
|
|
36
|
+
*
|
|
37
|
+
* Returns the patched source or `null` if no changes were made.
|
|
38
|
+
*/
|
|
39
|
+
function patchConsumerForwardedAs(filePath, entries) {
|
|
40
|
+
let source;
|
|
41
|
+
try {
|
|
42
|
+
source = readFileSync(filePath, "utf-8");
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (entries.length === 0) return null;
|
|
47
|
+
let modified = source;
|
|
48
|
+
for (const { localStyledName } of entries) {
|
|
49
|
+
modified = patchJsxAsProp(modified, localStyledName);
|
|
50
|
+
modified = patchAttrsAsProp(modified, localStyledName);
|
|
51
|
+
}
|
|
52
|
+
return modified !== source ? modified : null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Replace `as=` with `forwardedAs=` in JSX tags for the given component name.
|
|
56
|
+
* Handles both `as="span"` and `as={expr}` forms.
|
|
57
|
+
* Skips tags that already contain `forwardedAs`.
|
|
58
|
+
*/
|
|
59
|
+
function patchJsxAsProp(source, componentName) {
|
|
60
|
+
const tagRegex = new RegExp(`(<${escapeRegex(componentName)}\\b[^<>]*)\\bas(\\s*[={])`, "g");
|
|
61
|
+
return source.replace(tagRegex, (match, before, after) => {
|
|
62
|
+
if (before.includes("forwardedAs") || match.includes("forwardedAs")) return match;
|
|
63
|
+
return `${before}forwardedAs${after}`;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Replace `as:` with `forwardedAs:` in object-form `.attrs({...})` calls on
|
|
68
|
+
* the styled declaration for the given component name.
|
|
69
|
+
* Only matches object-form `.attrs({ as: ... })`, NOT function-form
|
|
70
|
+
* `.attrs(({ as }) => ...)` where the destructuring param would be incorrectly patched.
|
|
71
|
+
* Skips attrs blocks that already contain `forwardedAs`.
|
|
72
|
+
*/
|
|
73
|
+
function patchAttrsAsProp(source, componentName) {
|
|
74
|
+
const attrsRegex = new RegExp(`(const\\s+${escapeRegex(componentName)}\\b[^;]*\\.attrs\\s*\\(\\s*\\{[^)]*?)\\bas(\\s*:)`, "g");
|
|
75
|
+
return source.replace(attrsRegex, (match, before, after) => {
|
|
76
|
+
if (before.includes("forwardedAs")) return match;
|
|
77
|
+
return `${before}forwardedAs${after}`;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Resolve symlinks so paths match the keys in transformedFiles. */
|
|
81
|
+
function toRealPath(filePath) {
|
|
82
|
+
const resolved = resolve(filePath);
|
|
83
|
+
try {
|
|
84
|
+
return realpathSync(resolved);
|
|
85
|
+
} catch {
|
|
86
|
+
return resolved;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
export { buildForwardedAsReplacements, patchConsumerForwardedAs };
|
package/dist/index.d.mts
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as assertValidAdapterInput, o as describeValue, r as defineAdapter, t as Logger } from "./logger-
|
|
1
|
+
import { a as assertValidAdapterInput, o as describeValue, r as defineAdapter, t as Logger } from "./logger-Bi5-MGBs.mjs";
|
|
2
2
|
import { run } from "jscodeshift/src/Runner.js";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -141,9 +141,9 @@ async function runTransform(options) {
|
|
|
141
141
|
`Pattern(s): ${consumerPatterns.join(", ")}`,
|
|
142
142
|
"Check that the glob pattern is correct and files exist."
|
|
143
143
|
].join("\n"));
|
|
144
|
-
const { createModuleResolver } = await import("./resolve-imports-
|
|
144
|
+
const { createModuleResolver } = await import("./resolve-imports-4bFqrkrQ.mjs");
|
|
145
145
|
const sharedResolver = createModuleResolver();
|
|
146
|
-
const { runPrepass } = await import("./run-prepass-
|
|
146
|
+
const { runPrepass } = await import("./run-prepass-BuYfEK4M.mjs");
|
|
147
147
|
const absoluteFiles = filePaths.map((f) => resolve(f));
|
|
148
148
|
const absoluteConsumers = consumerFilePaths.map((f) => resolve(f));
|
|
149
149
|
let prepassResult;
|
|
@@ -165,7 +165,8 @@ async function runTransform(options) {
|
|
|
165
165
|
componentsNeedingMarkerSidecar: /* @__PURE__ */ new Map(),
|
|
166
166
|
componentsNeedingGlobalSelectorBridge: /* @__PURE__ */ new Map()
|
|
167
167
|
},
|
|
168
|
-
consumerAnalysis: void 0
|
|
168
|
+
consumerAnalysis: void 0,
|
|
169
|
+
forwardedAsConsumers: /* @__PURE__ */ new Map()
|
|
169
170
|
};
|
|
170
171
|
}
|
|
171
172
|
const crossFilePrepassResult = prepassResult.crossFileInfo;
|
|
@@ -183,7 +184,8 @@ async function runTransform(options) {
|
|
|
183
184
|
}
|
|
184
185
|
return analysisMap.get(`${realPath}:${ctx.componentName}`) ?? {
|
|
185
186
|
styles: false,
|
|
186
|
-
as: false
|
|
187
|
+
as: false,
|
|
188
|
+
ref: false
|
|
187
189
|
};
|
|
188
190
|
}
|
|
189
191
|
};
|
|
@@ -229,7 +231,7 @@ async function runTransform(options) {
|
|
|
229
231
|
});
|
|
230
232
|
if (sidecarFiles.size > 0 && !dryRun) for (const [sidecarPath, content] of sidecarFiles) await writeFile(sidecarPath, mergeSidecarContent(sidecarPath, content), "utf-8");
|
|
231
233
|
if (bridgeResults.size > 0 && !dryRun) {
|
|
232
|
-
const { buildConsumerReplacements, patchConsumerFile } = await import("./bridge-consumer-patcher-
|
|
234
|
+
const { buildConsumerReplacements, patchConsumerFile } = await import("./bridge-consumer-patcher-C2mFaVmd.mjs");
|
|
233
235
|
const consumerReplacements = buildConsumerReplacements(crossFilePrepassResult.selectorUsages, bridgeResults, transformedFiles);
|
|
234
236
|
const patchedFiles = [];
|
|
235
237
|
for (const [consumerPath, replacements] of consumerReplacements) {
|
|
@@ -241,6 +243,19 @@ async function runTransform(options) {
|
|
|
241
243
|
}
|
|
242
244
|
if (formatterCommands && patchedFiles.length > 0) await runFormatters(formatterCommands, patchedFiles);
|
|
243
245
|
}
|
|
246
|
+
if (prepassResult.forwardedAsConsumers.size > 0 && !dryRun) {
|
|
247
|
+
const { buildForwardedAsReplacements, patchConsumerForwardedAs } = await import("./forwarded-as-consumer-patcher-GPnjkzc_.mjs");
|
|
248
|
+
const forwardedAsReplacements = buildForwardedAsReplacements(prepassResult.forwardedAsConsumers, transformedFiles);
|
|
249
|
+
const patchedFiles = [];
|
|
250
|
+
for (const [consumerPath, entries] of forwardedAsReplacements) {
|
|
251
|
+
const patched = patchConsumerForwardedAs(consumerPath, entries);
|
|
252
|
+
if (patched !== null) {
|
|
253
|
+
await writeFile(consumerPath, patched, "utf-8");
|
|
254
|
+
patchedFiles.push(consumerPath);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (formatterCommands && patchedFiles.length > 0) await runFormatters(formatterCommands, patchedFiles);
|
|
258
|
+
}
|
|
244
259
|
if (formatterCommands && formatterCommands.length > 0 && result.ok > 0 && !dryRun) await runFormatters(formatterCommands, filePaths);
|
|
245
260
|
const report = Logger.createReport();
|
|
246
261
|
report.print();
|
|
@@ -229,11 +229,11 @@ const DEFAULT_THEME_HOOK = {
|
|
|
229
229
|
*
|
|
230
230
|
* // Configure external interface for exported components
|
|
231
231
|
* externalInterface(ctx) {
|
|
232
|
-
* // Example: Enable styles and `
|
|
232
|
+
* // Example: Enable styles, `as`, and `ref` for shared components folder
|
|
233
233
|
* if (ctx.filePath.includes("/shared/components/")) {
|
|
234
|
-
* return { styles: true, as: true };
|
|
234
|
+
* return { styles: true, as: true, ref: true };
|
|
235
235
|
* }
|
|
236
|
-
* return { styles: false, as: false };
|
|
236
|
+
* return { styles: false, as: false, ref: false };
|
|
237
237
|
* },
|
|
238
238
|
*
|
|
239
239
|
* // Optional: provide a custom merger, or use `null` for the default verbose merge output
|
|
@@ -164,7 +164,7 @@ type ResolveValueResult = {
|
|
|
164
164
|
*/
|
|
165
165
|
usage?: "props";
|
|
166
166
|
};
|
|
167
|
-
type
|
|
167
|
+
type CallResolveResultWithExpr = {
|
|
168
168
|
/**
|
|
169
169
|
* JS expression string to inline into generated output.
|
|
170
170
|
*
|
|
@@ -217,7 +217,38 @@ type CallResolveResult = {
|
|
|
217
217
|
* Example: `"white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"`
|
|
218
218
|
*/
|
|
219
219
|
cssText?: string;
|
|
220
|
+
/**
|
|
221
|
+
* When true, keeps the original helper call as a runtime style-function override
|
|
222
|
+
* in addition to the resolved static value.
|
|
223
|
+
*
|
|
224
|
+
* This is useful for incremental migrations where you still want to run an
|
|
225
|
+
* existing runtime helper (for example `ColorConverter.cssWithAlpha(...)`) while
|
|
226
|
+
* also emitting a static StyleX fallback.
|
|
227
|
+
*
|
|
228
|
+
* Behavior notes:
|
|
229
|
+
* - In `CallResolveResultWithExpr`, `expr`/`imports` are used as a static fallback in
|
|
230
|
+
* `stylex.create(...)`.
|
|
231
|
+
* - In `CallResolveRuntimeOnlyResult`, no static fallback is emitted.
|
|
232
|
+
* - The runtime override is only emitted for arrow-function helper call interpolations.
|
|
233
|
+
* - Theme access in the original call is rewritten to use the wrapper `useTheme()` value.
|
|
234
|
+
*/
|
|
235
|
+
preserveRuntimeCall?: boolean;
|
|
236
|
+
};
|
|
237
|
+
type CallResolveRuntimeOnlyResult = {
|
|
238
|
+
/**
|
|
239
|
+
* Keep the original helper call as a runtime style-function override, without
|
|
240
|
+
* requiring a static fallback expression.
|
|
241
|
+
*
|
|
242
|
+
* This mode is only supported for helper calls used as CSS values (not StyleX
|
|
243
|
+
* style-object references).
|
|
244
|
+
*/
|
|
245
|
+
preserveRuntimeCall: true;
|
|
246
|
+
/**
|
|
247
|
+
* Optional usage hint. Runtime-only results are treated as CSS-value usage.
|
|
248
|
+
*/
|
|
249
|
+
usage?: "create";
|
|
220
250
|
};
|
|
251
|
+
type CallResolveResult = CallResolveResultWithExpr | CallResolveRuntimeOnlyResult;
|
|
221
252
|
type ImportSource = {
|
|
222
253
|
kind: "absolutePath";
|
|
223
254
|
value: string;
|
|
@@ -361,14 +392,19 @@ interface ExternalInterfaceContext {
|
|
|
361
392
|
/**
|
|
362
393
|
* Result type for `adapter.externalInterface(...)`.
|
|
363
394
|
*
|
|
364
|
-
* - `
|
|
365
|
-
* - `
|
|
366
|
-
* - `
|
|
367
|
-
*
|
|
395
|
+
* - `styles` — accept external className/style props
|
|
396
|
+
* - `as` — accept polymorphic `as` prop
|
|
397
|
+
* - `ref` — include `ref` in the component's public type
|
|
398
|
+
*
|
|
399
|
+
* Examples:
|
|
400
|
+
* - `{ styles: true, as: false, ref: false }` → className/style support only
|
|
401
|
+
* - `{ styles: true, as: true, ref: true }` → full external interface
|
|
402
|
+
* - `{ styles: false, as: false, ref: false }` → no external interface support
|
|
368
403
|
*/
|
|
369
404
|
type ExternalInterfaceResult = {
|
|
370
405
|
styles: boolean;
|
|
371
406
|
as: boolean;
|
|
407
|
+
ref: boolean;
|
|
372
408
|
};
|
|
373
409
|
/**
|
|
374
410
|
* Configuration for a custom style merger function that combines stylex.props()
|
|
@@ -430,6 +466,10 @@ interface Adapter {
|
|
|
430
466
|
*
|
|
431
467
|
* Return:
|
|
432
468
|
* - `{ expr, imports }` with the resolved expression
|
|
469
|
+
* - `{ preserveRuntimeCall: true }` to keep only the original runtime helper call
|
|
470
|
+
* (no static fallback)
|
|
471
|
+
* - Optional: add `preserveRuntimeCall: true` to also keep the original helper
|
|
472
|
+
* call at runtime as a wrapper style-function override
|
|
433
473
|
* - `undefined` to bail/skip the file
|
|
434
474
|
*/
|
|
435
475
|
resolveCall: (context: CallResolveContext) => CallResolveResult | undefined;
|
|
@@ -458,10 +498,10 @@ interface Adapter {
|
|
|
458
498
|
* Called for exported styled components to determine their external interface.
|
|
459
499
|
*
|
|
460
500
|
* Return:
|
|
461
|
-
* - `{ styles: false, as: false }` → no external interface
|
|
462
|
-
* - `{ styles: true, as: false }` → accept className/style props only
|
|
463
|
-
* - `{ styles: true, as: true }` →
|
|
464
|
-
* - `{ styles: false, as: true }` → accept only polymorphic `as` prop
|
|
501
|
+
* - `{ styles: false, as: false, ref: false }` → no external interface
|
|
502
|
+
* - `{ styles: true, as: false, ref: false }` → accept className/style props only
|
|
503
|
+
* - `{ styles: true, as: true, ref: true }` → full external interface
|
|
504
|
+
* - `{ styles: false, as: true, ref: false }` → accept only polymorphic `as` prop
|
|
465
505
|
*/
|
|
466
506
|
externalInterface: (context: ExternalInterfaceContext) => ExternalInterfaceResult;
|
|
467
507
|
/**
|
|
@@ -474,7 +514,7 @@ interface Adapter {
|
|
|
474
514
|
* ```typescript
|
|
475
515
|
* function merger(
|
|
476
516
|
* styles: StyleXStyles | StyleXStyles[],
|
|
477
|
-
* className?: string,
|
|
517
|
+
* className?: string | (string | undefined | false | null)[],
|
|
478
518
|
* style?: React.CSSProperties
|
|
479
519
|
* ): { className?: string; style?: React.CSSProperties }
|
|
480
520
|
* ```
|
|
@@ -552,11 +592,11 @@ interface AdapterInput {
|
|
|
552
592
|
*
|
|
553
593
|
* // Configure external interface for exported components
|
|
554
594
|
* externalInterface(ctx) {
|
|
555
|
-
* // Example: Enable styles and `
|
|
595
|
+
* // Example: Enable styles, `as`, and `ref` for shared components folder
|
|
556
596
|
* if (ctx.filePath.includes("/shared/components/")) {
|
|
557
|
-
* return { styles: true, as: true };
|
|
597
|
+
* return { styles: true, as: true, ref: true };
|
|
558
598
|
* }
|
|
559
|
-
* return { styles: false, as: false };
|
|
599
|
+
* return { styles: false, as: false, ref: false };
|
|
560
600
|
* },
|
|
561
601
|
*
|
|
562
602
|
* // Optional: provide a custom merger, or use `null` for the default verbose merge output
|
|
@@ -573,7 +613,7 @@ declare function defineAdapter<T extends AdapterInput>(adapter: T): T;
|
|
|
573
613
|
//#endregion
|
|
574
614
|
//#region src/internal/logger.d.ts
|
|
575
615
|
type Severity = "info" | "warning" | "error";
|
|
576
|
-
type WarningType = "`css` helper function switch must return css templates in all branches" | "`css` helper usage as a function call (css(...)) is not supported" | "`css` helper used outside of a styled component template cannot be statically transformed" | "Adapter helper call in border interpolation did not resolve to a single CSS value" | "Adapter resolveCall returned an unparseable styles expression" | "Adapter resolveCall returned an unparseable value expression" | "Adapter resolveCall returned StyleX styles for helper call where a CSS value was expected" | "Adapter resolveCall returned undefined for helper call" | "Adapter resolveBaseComponent threw an error" | "Adapter resolved StyleX styles cannot be applied under nested selectors/at-rules" | "Adapter resolved StyleX styles inside pseudo selector but did not provide cssText for property expansion — add cssText to resolveCall result to enable pseudo-wrapping" | 'Adapter resolveCall cssText could not be parsed as CSS declarations — expected semicolon-separated property: value pairs (e.g. "white-space: nowrap; overflow: hidden;")' | "Adapter resolveValue returned an unparseable value expression" | "Adapter resolveValue returned undefined for imported value" | "Arrow function: body is not a recognized pattern (expected ternary, logical, call, or member expression)" | "Arrow function: conditional branches could not be resolved to static or theme values" | "Arrow function: helper call body is not supported" | "Arrow function: indexed theme lookup pattern not matched" | "Arrow function: logical expression pattern not supported" | "Arrow function: prop access cannot be converted to style function for this CSS property" | "Arrow function: theme access path could not be resolved" | "Component selectors like `${OtherComponent}:hover &` are not directly representable in StyleX. Manual refactor is required" | "Conditional `css` block: !important is not supported in StyleX" | "Conditional `css` block: @-rules (e.g., @media, @supports) are not supported" | "CSS block contains unsupported at-rule (only @media
|
|
616
|
+
type WarningType = "`css` helper function switch must return css templates in all branches" | "`css` helper usage as a function call (css(...)) is not supported" | "`css` helper used outside of a styled component template cannot be statically transformed" | "Adapter helper call in border interpolation did not resolve to a single CSS value" | "Adapter resolveCall returned an unparseable styles expression" | "Adapter resolveCall returned an unparseable value expression" | "Adapter resolveCall returned StyleX styles for helper call where a CSS value was expected" | "Adapter resolveCall returned undefined for helper call" | "Adapter resolveBaseComponent threw an error" | "Adapter resolved StyleX styles cannot be applied under nested selectors/at-rules" | "Adapter resolved StyleX styles inside pseudo selector but did not provide cssText for property expansion — add cssText to resolveCall result to enable pseudo-wrapping" | 'Adapter resolveCall cssText could not be parsed as CSS declarations — expected semicolon-separated property: value pairs (e.g. "white-space: nowrap; overflow: hidden;")' | "Adapter resolveValue returned an unparseable value expression" | "Adapter resolveValue returned undefined for imported value" | "Arrow function: body is not a recognized pattern (expected ternary, logical, call, or member expression)" | "Arrow function: conditional branches could not be resolved to static or theme values" | "Arrow function: helper call body is not supported" | "Arrow function: indexed theme lookup pattern not matched" | "Arrow function: logical expression pattern not supported" | "Arrow function: prop access cannot be converted to style function for this CSS property" | "Arrow function: theme access path could not be resolved" | "Component selectors like `${OtherComponent}:hover &` are not directly representable in StyleX. Manual refactor is required" | "Conditional `css` block: !important is not supported in StyleX" | "Conditional `css` block: @-rules (e.g., @media, @supports) are not supported" | "CSS block contains unsupported at-rule (only @media and @container are supported; @supports, etc. require manual handling)" | "Conditional `css` block: dynamic interpolation could not be resolved to a single component prop" | "Conditional `css` block: failed to parse expression" | "Conditional `css` block: missing CSS property name" | "Conditional `css` block: missing interpolation expression" | "Conditional `css` block: mixed static/dynamic values with non-theme expressions cannot be safely transformed" | "Conditional `css` block: multiple interpolation slots in a single property value" | "Conditional `css` block: ternary branch value could not be resolved (imported values require adapter support)" | "Conditional `css` block: ternary expressions inside pseudo selectors are not supported" | "Conditional `css` block: media query contains unresolvable interpolation" | "Conditional `css` block: unsupported selector" | "Directional border helper styles are not supported" | "Multi-slot border interpolation could not be resolved" | "Resolved border helper value could not be expanded to longhand properties" | "createGlobalStyle is not supported in StyleX. Global styles should be handled separately (e.g., in a CSS file or using CSS reset libraries)" | "Dynamic styles inside pseudo elements (::before/::after) are not supported by StyleX. See https://github.com/facebook/stylex/issues/1396" | "Failed to parse theme expressions" | "Heterogeneous background values (mix of gradients and colors) not currently supported" | "Higher-order styled factory wrappers (e.g. hoc(styled)) are not supported" | "Imported CSS helper mixins: cannot determine inherited properties for correct pseudo selector handling" | "Local helper function returns CSS that cannot be decomposed into individual properties" | "Local helper function computes CSS values that cannot be statically traced to the component prop" | "Styled-components specificity hacks like `&&` / `&&&` are not representable in StyleX" | "Theme-dependent block-level conditional could not be fully resolved (branches may contain dynamic interpolations)" | "Theme-dependant call expression could not be resolved (e.g. theme helper calls like theme.highlight() are not supported)" | "Theme value with fallback (props.theme.X ?? / || default) cannot be resolved statically — use adapter.resolveValue to map theme paths to StyleX tokens" | "Theme-dependent nested prop access requires a project-specific theme source (e.g. useTheme())" | "Theme-dependent template literals require a project-specific theme source (e.g. useTheme())" | "Theme prop overrides on styled components are not supported" | "Universal selectors (`*`) are currently unsupported" | "Unsupported call expression (expected imported helper(...) or imported helper(...)(...))" | "Unsupported conditional test in shouldForwardProp" | "Unsupported shouldForwardProp pattern (only !prop.startsWith(), ![].includes(prop), and prop !== are supported)" | "Unsupported interpolation: arrow function" | "Unsupported interpolation: call expression" | "Unsupported interpolation: identifier" | "Unsupported interpolation: member expression" | "Unsupported interpolation: property" | "Unsupported interpolation: unknown" | "Unsupported nested conditional interpolation" | "Unsupported prop-based inline style expression cannot be safely inlined" | "Unsupported prop-based inline style props.theme access is not supported" | "Unsupported selector interpolation: imported value in selector position" | "Unsupported: media query contains unresolvable interpolation" | "Unsupported selector: class selector" | "Unsupported selector: comma-separated selectors must all be simple pseudos or pseudo-elements" | "Unsupported selector: descendant pseudo selector (space before pseudo)" | "Unsupported selector: descendant/child/sibling selector" | "Unsupported selector: interpolated pseudo selector" | "Unsupported selector: sibling combinator" | "Unsupported selector: unresolved interpolation in sibling selector" | "Unsupported selector: ambiguous element selector" | "Unsupported selector: attribute selector on unsupported element" | "Unsupported selector: element selector on exported component" | "Unsupported selector: element selector with combined ancestor and child pseudos" | "Unsupported selector: element selector with dynamic children" | "Unsupported selector: element selector with plain intrinsic children" | "Unsupported selector: element selector pseudo collision" | "Unsupported selector: unresolved interpolation in cross-file component selector" | "Unsupported selector: unresolved interpolation in descendant component selector" | "Unsupported selector: unresolved interpolation in element selector" | "Unsupported selector: unresolved interpolation in reverse component selector" | "Unsupported selector: grouped reverse selector references different components" | "Unsupported selector: unknown component selector" | "Unsupported css`` mixin: after-base mixin style is not a plain object" | "Unsupported css`` mixin: nested contextual conditions in after-base mixin" | "Unsupported css`` mixin: cannot infer base default for after-base contextual override (base value is non-literal)" | "css`` helper function interpolation references closure variable that cannot be hoisted" | "Sibling selector broadened: & + & (adjacent) becomes general sibling (~) in StyleX — interleaved non-matching elements will no longer block the match" | "Using styled-components components as mixins is not supported; use css`` mixins or strings instead" | "styled(ImportedComponent) wraps a component whose file contains internal styled-components — convert the base component's file first to avoid CSS cascade conflicts";
|
|
577
617
|
interface WarningLog {
|
|
578
618
|
severity: Severity;
|
|
579
619
|
type: WarningType;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as PLACEHOLDER_RE } from "./styled-css-DBryFqQM.mjs";
|
|
2
|
-
import { t as isSelectorContext } from "./selector-context-heuristic-
|
|
2
|
+
import { t as isSelectorContext } from "./selector-context-heuristic-Cki9_tTH.mjs";
|
|
3
3
|
import path, { relative, resolve } from "node:path";
|
|
4
4
|
import { readFileSync, realpathSync } from "node:fs";
|
|
5
5
|
import { execSync } from "node:child_process";
|
|
@@ -466,10 +466,15 @@ function deduplicateAndResolve(filesToTransform, consumerPaths) {
|
|
|
466
466
|
* and runs AST parsing + consumer analysis only on relevant files.
|
|
467
467
|
*/
|
|
468
468
|
const AS_PROP_RE = /\bas[={]/;
|
|
469
|
+
const REF_PROP_RE = /\bref[={]/;
|
|
469
470
|
const STYLED_CALL_RE = /styled\(([A-Z][A-Za-z0-9]+)/g;
|
|
470
471
|
const STYLED_DEF_RE = /const\s+([A-Z][A-Za-z0-9]*)\b[^=]*=\s*styled[.(]/g;
|
|
471
472
|
/** Matches <Component ...as= across lines. [^<>]* avoids crossing tag boundaries. */
|
|
472
473
|
const JSX_AS_COMPONENT_RE = /<([A-Z][A-Za-z0-9]*)\b[^<>]*\bas[={]/g;
|
|
474
|
+
/** Matches <Component ...ref= across lines. [^<>]* avoids crossing tag boundaries. */
|
|
475
|
+
const JSX_REF_COMPONENT_RE = /<([A-Z][A-Za-z0-9]*)\b[^<>]*\bref[={]/g;
|
|
476
|
+
/** Captures both the local styled name and the wrapped component: styled(Flex) → ["StyledFlex", "Flex"] */
|
|
477
|
+
const STYLED_COMPONENT_WRAPPER_RE = /const\s+([A-Z][A-Za-z0-9]*)\b[^=]*=\s*styled\(\s*([A-Z][A-Za-z0-9]*)\s*\)/g;
|
|
473
478
|
async function runPrepass(options) {
|
|
474
479
|
const { filesToTransform, consumerPaths, resolver, parserName, createExternalInterface, enableAstCache } = options;
|
|
475
480
|
const t0 = performance.now();
|
|
@@ -517,9 +522,11 @@ async function runPrepass(options) {
|
|
|
517
522
|
const componentsNeedingMarkerSidecar = /* @__PURE__ */ new Map();
|
|
518
523
|
const componentsNeedingGlobalSelectorBridge = /* @__PURE__ */ new Map();
|
|
519
524
|
const asUsages = /* @__PURE__ */ new Map();
|
|
525
|
+
const refUsages = /* @__PURE__ */ new Map();
|
|
520
526
|
const styledCallUsages = [];
|
|
521
527
|
const styledDefFiles = /* @__PURE__ */ new Map();
|
|
522
528
|
const classNameStyleUsages = /* @__PURE__ */ new Map();
|
|
529
|
+
const styledWrapperUsages = [];
|
|
523
530
|
const fileContents = /* @__PURE__ */ new Map();
|
|
524
531
|
const cachedRead = (filePath) => {
|
|
525
532
|
const content = fileContents.get(filePath);
|
|
@@ -543,7 +550,8 @@ async function runPrepass(options) {
|
|
|
543
550
|
}
|
|
544
551
|
const hasStyled = source.includes("styled-components");
|
|
545
552
|
const hasAsProp = createExternalInterface && AS_PROP_RE.test(source);
|
|
546
|
-
|
|
553
|
+
const hasRefProp = createExternalInterface && REF_PROP_RE.test(source);
|
|
554
|
+
if (!hasStyled && !hasAsProp && !hasRefProp) continue;
|
|
547
555
|
fileContents.set(filePath, source);
|
|
548
556
|
if (hasStyled && BARE_TEMPLATE_IDENTIFIER_RE.test(source) && hasRegexSelectorCandidate(source)) {
|
|
549
557
|
const usages = scanFileForSelectorsAst(filePath, source, transformSet, resolver, parser, toRealPath, cachedRead, astCache, createExternalInterface);
|
|
@@ -560,11 +568,21 @@ async function runPrepass(options) {
|
|
|
560
568
|
});
|
|
561
569
|
STYLED_DEF_RE.lastIndex = 0;
|
|
562
570
|
for (const m of source.matchAll(STYLED_DEF_RE)) if (m[1]) addToSetMap(styledDefFiles, filePath, m[1]);
|
|
571
|
+
STYLED_COMPONENT_WRAPPER_RE.lastIndex = 0;
|
|
572
|
+
for (const m of source.matchAll(STYLED_COMPONENT_WRAPPER_RE)) if (m[1] && m[2]) styledWrapperUsages.push({
|
|
573
|
+
file: filePath,
|
|
574
|
+
localStyledName: m[1],
|
|
575
|
+
wrappedName: m[2]
|
|
576
|
+
});
|
|
563
577
|
}
|
|
564
578
|
if (hasAsProp) {
|
|
565
579
|
JSX_AS_COMPONENT_RE.lastIndex = 0;
|
|
566
580
|
for (const m of source.matchAll(JSX_AS_COMPONENT_RE)) if (m[1]) addToSetMap(asUsages, m[1], filePath);
|
|
567
581
|
}
|
|
582
|
+
if (hasRefProp) {
|
|
583
|
+
JSX_REF_COMPONENT_RE.lastIndex = 0;
|
|
584
|
+
for (const m of source.matchAll(JSX_REF_COMPONENT_RE)) if (m[1]) addToSetMap(refUsages, m[1], filePath);
|
|
585
|
+
}
|
|
568
586
|
}
|
|
569
587
|
const styledFileCount = fileContents.size;
|
|
570
588
|
if (createExternalInterface && styledDefFiles.size > 0) {
|
|
@@ -590,44 +608,35 @@ async function runPrepass(options) {
|
|
|
590
608
|
if (!entry) {
|
|
591
609
|
entry = {
|
|
592
610
|
styles: false,
|
|
593
|
-
as: false
|
|
611
|
+
as: false,
|
|
612
|
+
ref: false
|
|
594
613
|
};
|
|
595
614
|
consumerAnalysis.set(key, entry);
|
|
596
615
|
}
|
|
597
616
|
return entry;
|
|
598
617
|
};
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
for (const
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
618
|
+
const matchUsagesToDefinitions = (usages, field) => {
|
|
619
|
+
if (usages.size === 0) return;
|
|
620
|
+
for (const [defFile, names] of styledDefFiles) {
|
|
621
|
+
const defSrc = cachedRead(defFile);
|
|
622
|
+
for (const name of names) {
|
|
623
|
+
const usageFiles = usages.get(name);
|
|
624
|
+
if (!usageFiles) continue;
|
|
625
|
+
if (!fileExports(defSrc, name)) continue;
|
|
626
|
+
if (usageFiles.has(defFile)) {
|
|
627
|
+
ensure(defFile, name)[field] = true;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
for (const usageFile of usageFiles) if (fileImportsFrom(cachedRead(usageFile), usageFile, name, defFile, resolve$1)) {
|
|
631
|
+
ensure(defFile, name)[field] = true;
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
612
634
|
}
|
|
613
635
|
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const usageFiles = classNameStyleUsages.get(name);
|
|
619
|
-
if (!usageFiles) continue;
|
|
620
|
-
if (!fileExports(defSrc, name)) continue;
|
|
621
|
-
if (usageFiles.has(defFile)) {
|
|
622
|
-
ensure(defFile, name).styles = true;
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
for (const usageFile of usageFiles) if (fileImportsFrom(cachedRead(usageFile), usageFile, name, defFile, resolve$1)) {
|
|
626
|
-
ensure(defFile, name).styles = true;
|
|
627
|
-
break;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
636
|
+
};
|
|
637
|
+
matchUsagesToDefinitions(asUsages, "as");
|
|
638
|
+
matchUsagesToDefinitions(refUsages, "ref");
|
|
639
|
+
matchUsagesToDefinitions(classNameStyleUsages, "styles");
|
|
631
640
|
{
|
|
632
641
|
const seen = /* @__PURE__ */ new Set();
|
|
633
642
|
for (const { file, name } of styledCallUsages) {
|
|
@@ -649,6 +658,26 @@ async function runPrepass(options) {
|
|
|
649
658
|
}
|
|
650
659
|
}
|
|
651
660
|
}
|
|
661
|
+
const forwardedAsConsumers = /* @__PURE__ */ new Map();
|
|
662
|
+
if (createExternalInterface && styledWrapperUsages.length > 0) for (const { file, localStyledName, wrappedName } of styledWrapperUsages) {
|
|
663
|
+
if (transformSet.has(file)) continue;
|
|
664
|
+
const importInfo = findImportSource(cachedRead(file), wrappedName);
|
|
665
|
+
if (!importInfo) continue;
|
|
666
|
+
const { source: importSource, exportedName } = importInfo;
|
|
667
|
+
let defFile = resolve$1(importSource, file);
|
|
668
|
+
if (!defFile) continue;
|
|
669
|
+
defFile = resolveBarrelReExport(defFile, exportedName, resolve$1, cachedRead) ?? defFile;
|
|
670
|
+
if (!transformSet.has(defFile)) continue;
|
|
671
|
+
let entries = forwardedAsConsumers.get(file);
|
|
672
|
+
if (!entries) {
|
|
673
|
+
entries = [];
|
|
674
|
+
forwardedAsConsumers.set(file, entries);
|
|
675
|
+
}
|
|
676
|
+
entries.push({
|
|
677
|
+
localStyledName,
|
|
678
|
+
targetPath: defFile
|
|
679
|
+
});
|
|
680
|
+
}
|
|
652
681
|
const crossFileInfo = {
|
|
653
682
|
selectorUsages,
|
|
654
683
|
componentsNeedingMarkerSidecar,
|
|
@@ -659,12 +688,14 @@ async function runPrepass(options) {
|
|
|
659
688
|
const elapsed = ((performance.now() - t0) / 1e3).toFixed(1);
|
|
660
689
|
const reStyled = consumerAnalysis ? [...consumerAnalysis.values()].filter((v) => v.styles).length : 0;
|
|
661
690
|
const asProp = consumerAnalysis ? [...consumerAnalysis.values()].filter((v) => v.as).length : 0;
|
|
662
|
-
|
|
691
|
+
const refProp = consumerAnalysis ? [...consumerAnalysis.values()].filter((v) => v.ref).length : 0;
|
|
692
|
+
process.stdout.write(`Prepass: scanned ${uniqueAllFiles.length} files in ${elapsed}s — ${styledFileCount} with styled-components, ${selectorUsages.size} cross-file selectors, ${reStyled} re-styled, ${asProp} as-prop, ${refProp} ref-prop, ${classNameStyleUsages.size} className/style, ${forwardedAsConsumers.size} forwardedAs\n`);
|
|
663
693
|
}
|
|
664
694
|
if (process.env.DEBUG_CODEMOD) logPrepassDebug(uniqueAllFiles, crossFileInfo, consumerAnalysis);
|
|
665
695
|
return {
|
|
666
696
|
crossFileInfo,
|
|
667
|
-
consumerAnalysis
|
|
697
|
+
consumerAnalysis,
|
|
698
|
+
forwardedAsConsumers
|
|
668
699
|
};
|
|
669
700
|
}
|
|
670
701
|
/** Matches `${Identifier}` in source — used to find potential selector expressions. */
|
|
@@ -850,7 +881,7 @@ function rgPreFilter(files) {
|
|
|
850
881
|
const dirs = deduplicateParentDirs(files);
|
|
851
882
|
if (dirs.length === 0) return;
|
|
852
883
|
try {
|
|
853
|
-
const pattern = String.raw`(styled-components|\bas[={])`;
|
|
884
|
+
const pattern = String.raw`(styled-components|\bas[={]|\bref[={])`;
|
|
854
885
|
const globArgs = [
|
|
855
886
|
"*.tsx",
|
|
856
887
|
"*.ts",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/internal/utilities/string-utils.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared string formatting utilities.
|
|
4
|
+
* Core concepts: casing conversions and whitespace normalization.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Capitalizes the first character of a string.
|
|
8
|
+
* @example capitalize("hello") => "Hello"
|
|
9
|
+
*/
|
|
10
|
+
function capitalize(s) {
|
|
11
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Converts a kebab-case string to camelCase.
|
|
15
|
+
* @example kebabToCamelCase("focus-visible") => "focusVisible"
|
|
16
|
+
* @example kebabToCamelCase("placeholder-shown") => "placeholderShown"
|
|
17
|
+
* @example kebabToCamelCase("hover") => "hover"
|
|
18
|
+
*/
|
|
19
|
+
function kebabToCamelCase(s) {
|
|
20
|
+
return s.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Lowercases the first character of a string.
|
|
24
|
+
* @example lowerFirst("Hello") => "hello"
|
|
25
|
+
*/
|
|
26
|
+
function lowerFirst(s) {
|
|
27
|
+
return s.charAt(0).toLowerCase() + s.slice(1);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Removes non-alphanumeric characters (except underscores) from a string.
|
|
31
|
+
* Useful for sanitizing strings to be valid JavaScript identifiers.
|
|
32
|
+
* @example sanitizeIdentifier("my-var!") => "my_var_"
|
|
33
|
+
*/
|
|
34
|
+
function sanitizeIdentifier(s) {
|
|
35
|
+
return s.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Checks if a CSS value looks like a length unit (px, rem, em, %, etc.).
|
|
39
|
+
* Matches numeric values with optional CSS length units.
|
|
40
|
+
* @example looksLikeLength("10px") => true
|
|
41
|
+
* @example looksLikeLength("1.5rem") => true
|
|
42
|
+
* @example looksLikeLength("auto") => false
|
|
43
|
+
*/
|
|
44
|
+
function looksLikeLength(token) {
|
|
45
|
+
return /^-?\d*\.?\d+(px|rem|em|vh|vw|vmin|vmax|ch|ex|lh|svh|svw|dvh|dvw|cqw|cqh|%)?$/.test(token);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Checks if a CSS value appears to be a background image (gradient or url).
|
|
49
|
+
* @example isBackgroundImageValue("linear-gradient(red, blue)") => true
|
|
50
|
+
* @example isBackgroundImageValue("url(image.png)") => true
|
|
51
|
+
* @example isBackgroundImageValue("#fff") => false
|
|
52
|
+
*/
|
|
53
|
+
function isBackgroundImageValue(value) {
|
|
54
|
+
return /\b(linear|radial|conic|repeating-linear|repeating-radial|repeating-conic)-gradient\b/.test(value) || /\burl\s*\(/.test(value);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Escapes special regex characters in a string so it can be safely used in a RegExp.
|
|
58
|
+
* @example escapeRegex("foo.bar") => "foo\\.bar"
|
|
59
|
+
* @example escapeRegex("$test") => "\\$test"
|
|
60
|
+
*/
|
|
61
|
+
function escapeRegex(s) {
|
|
62
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Normalizes whitespace in a CSS value string.
|
|
66
|
+
* Collapses all sequences of whitespace (including newlines) to single spaces
|
|
67
|
+
* and trims leading/trailing whitespace.
|
|
68
|
+
*
|
|
69
|
+
* This is useful for multiline template literals that are used for formatting
|
|
70
|
+
* convenience but should produce compact CSS values.
|
|
71
|
+
*
|
|
72
|
+
* @example normalizeWhitespace(" foo\n bar ") => "foo bar"
|
|
73
|
+
* @example normalizeWhitespace("\n value \n") => "value"
|
|
74
|
+
*/
|
|
75
|
+
function normalizeWhitespace(s) {
|
|
76
|
+
return s.replace(/\s+/g, " ").trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
export { looksLikeLength as a, sanitizeIdentifier as c, kebabToCamelCase as i, escapeRegex as n, lowerFirst as o, isBackgroundImageValue as r, normalizeWhitespace as s, capitalize as t };
|
package/dist/transform.d.mts
CHANGED