styled-components-to-stylex-codemod 0.0.14 → 0.0.16
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 +75 -9
- package/dist/bridge-consumer-patcher-D3fRIEkZ.mjs +122 -0
- package/dist/index.d.mts +26 -6
- package/dist/index.mjs +179 -41
- package/dist/{adapter-DilRjT4L.d.mts → logger-B7SOfCti.d.mts} +56 -2
- package/dist/{logger-D3j-qxgZ.mjs → logger-D-R2KB6I.mjs} +29 -4
- package/dist/resolve-imports-BDk6Ms09.mjs +66 -0
- package/dist/run-prepass-DOMrzrA_.mjs +915 -0
- package/dist/selector-context-heuristic-CGwiJ3HL.mjs +39 -0
- package/dist/styled-css-DBryFqQM.mjs +38 -0
- package/dist/transform.d.mts +43 -3
- package/dist/transform.mjs +1475 -320
- package/package.json +17 -20
- package/dist/consumer-analyzer.d.mts +0 -32
- package/dist/consumer-analyzer.mjs +0 -243
- package/dist/logger-I_hbSbxa.d.mts +0 -17
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ const adapter = defineAdapter({
|
|
|
38
38
|
|
|
39
39
|
await runTransform({
|
|
40
40
|
files: "src/**/*.tsx",
|
|
41
|
+
consumerPaths: null, // set to a glob to enable cross-file selector support
|
|
41
42
|
adapter,
|
|
42
43
|
dryRun: false,
|
|
43
44
|
parser: "tsx",
|
|
@@ -136,6 +137,7 @@ const adapter = defineAdapter({
|
|
|
136
137
|
|
|
137
138
|
await runTransform({
|
|
138
139
|
files: "src/**/*.tsx",
|
|
140
|
+
consumerPaths: null,
|
|
139
141
|
adapter,
|
|
140
142
|
dryRun: false,
|
|
141
143
|
parser: "tsx",
|
|
@@ -155,25 +157,56 @@ Adapters are the main extension point, see full example above. They let you cont
|
|
|
155
157
|
- which exported components should support external className/style extension and/or polymorphic `as` prop (`externalInterface`)
|
|
156
158
|
- how className/style merging is handled for components accepting external styling (`styleMerger`)
|
|
157
159
|
|
|
160
|
+
#### Cross-file selectors (`consumerPaths`)
|
|
161
|
+
|
|
162
|
+
`consumerPaths` is required. Pass `null` to opt out, or a glob pattern to enable cross-file selector scanning.
|
|
163
|
+
|
|
164
|
+
When transforming a subset of files, other files may reference your styled components as CSS selectors (e.g. `${Icon} { fill: red }`). Pass `consumerPaths` to scan those files and wire up cross-file selectors automatically:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
await runTransform({
|
|
168
|
+
files: "src/components/**/*.tsx", // files to transform
|
|
169
|
+
consumerPaths: "src/**/*.tsx", // additional files to scan for cross-file usage
|
|
170
|
+
adapter,
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- Files in **both** `files` and `consumerPaths` use the **marker sidecar** strategy (both consumer and target are transformed, using `stylex.defineMarker()`).
|
|
175
|
+
- Files in `consumerPaths` but **not** in `files` use the **bridge** strategy (a stable `className` is added to the converted component so unconverted consumers' selectors still work).
|
|
176
|
+
|
|
158
177
|
#### Auto-detecting external interface usage (experimental)
|
|
159
178
|
|
|
160
|
-
Instead of manually specifying which components need `styles` or `as` support,
|
|
179
|
+
Instead of manually specifying which components need `styles` or `as` support, set `externalInterface: "auto"` to auto-detect usage by scanning consumer code.
|
|
161
180
|
|
|
162
181
|
> [!NOTE]
|
|
163
|
-
> Experimental. Requires `
|
|
182
|
+
> Experimental. Requires `consumerPaths` and a successful prepass scan.
|
|
183
|
+
> If prepass fails, `runTransform()` throws (fail-fast) when `externalInterface: "auto"` is used.
|
|
164
184
|
|
|
165
185
|
```ts
|
|
166
|
-
import {
|
|
167
|
-
|
|
168
|
-
const externalInterface = createExternalInterface({ searchDirs: ["src/"] });
|
|
186
|
+
import { runTransform, defineAdapter } from "styled-components-to-stylex-codemod";
|
|
169
187
|
|
|
170
|
-
|
|
188
|
+
const adapter = defineAdapter({
|
|
171
189
|
// ...
|
|
172
|
-
externalInterface:
|
|
190
|
+
externalInterface: "auto",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await runTransform({
|
|
194
|
+
files: "src/**/*.tsx",
|
|
195
|
+
consumerPaths: "src/**/*.tsx", // required for auto-detection
|
|
196
|
+
adapter,
|
|
173
197
|
});
|
|
174
198
|
```
|
|
175
199
|
|
|
176
|
-
|
|
200
|
+
When `externalInterface: "auto"` is set, `runTransform()` scans `files` and `consumerPaths` for `styled(Component)` calls and `<Component as={...}>` JSX usage, resolves imports back to the component definition files, and returns the appropriate `{ styles, as }` flags automatically.
|
|
201
|
+
|
|
202
|
+
If that prepass scan fails, `runTransform()` stops and throws an actionable error rather than silently falling back to non-auto behavior.
|
|
203
|
+
|
|
204
|
+
Troubleshooting prepass failures with `"auto"`:
|
|
205
|
+
|
|
206
|
+
- verify `consumerPaths` globs match the files you expect
|
|
207
|
+
- confirm the selected parser matches your source syntax (`parser: "tsx"`, `parser: "ts"`, etc.)
|
|
208
|
+
- check resolver inputs (import paths, tsconfig path aliases, and related module resolution config)
|
|
209
|
+
- if needed, switch to a manual `externalInterface(ctx)` function to continue migration while you fix prepass inputs
|
|
177
210
|
|
|
178
211
|
#### Dynamic interpolations
|
|
179
212
|
|
|
@@ -190,7 +223,7 @@ When the codemod encounters an interpolation inside a styled template literal, i
|
|
|
190
223
|
- helper calls applied to prop values (e.g. `shadow(props.shadow)`) by emitting a StyleX style function that calls the helper at runtime
|
|
191
224
|
- conditional CSS blocks via ternary (e.g. `props.$dim ? "opacity: 0.5;" : ""`)
|
|
192
225
|
|
|
193
|
-
If the pipeline can
|
|
226
|
+
If the pipeline can't resolve an interpolation:
|
|
194
227
|
|
|
195
228
|
- for some dynamic value cases, the transform preserves the value as a wrapper inline style so output keeps visual parity (at the cost of using `style={...}` for that prop)
|
|
196
229
|
- otherwise, the declaration containing that interpolation is **dropped** and a warning is produced (manual follow-up required)
|
|
@@ -201,6 +234,39 @@ If the pipeline can’t resolve an interpolation:
|
|
|
201
234
|
- **createGlobalStyle**: detected usage is reported as an **unsupported-feature** warning (StyleX does not support global styles in the same way).
|
|
202
235
|
- **Theme prop overrides**: passing a `theme` prop directly to styled components (e.g. `<Button theme={...} />`) is not supported and will bail with a warning.
|
|
203
236
|
|
|
237
|
+
## Migration game plan
|
|
238
|
+
|
|
239
|
+
### 1. Define your theme and mixins as StyleX
|
|
240
|
+
|
|
241
|
+
Before running the codemod, convert your theme object and shared style helpers into StyleX equivalents:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// tokens.stylex.ts — theme variables
|
|
245
|
+
import * as stylex from "@stylexjs/stylex";
|
|
246
|
+
|
|
247
|
+
// Before: { colors: { primary: "#0066cc" }, spacing: { sm: "8px" } }
|
|
248
|
+
export const colors = stylex.defineVars({ primary: "#0066cc" });
|
|
249
|
+
export const spacing = stylex.defineVars({ sm: "8px" });
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
// helpers.stylex.ts — shared mixins
|
|
254
|
+
import * as stylex from "@stylexjs/stylex";
|
|
255
|
+
|
|
256
|
+
// Before: export const truncate = () => `white-space: nowrap; overflow: hidden; ...`
|
|
257
|
+
export const truncate = stylex.create({
|
|
258
|
+
base: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" },
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### 2. Write an adapter and run the codemod
|
|
263
|
+
|
|
264
|
+
The adapter maps your project's `props.theme.*` access, CSS variables, and helper calls to the StyleX equivalents from step 1. See [Usage](#usage) for the full API.
|
|
265
|
+
|
|
266
|
+
### 3. Verify, iterate, clean up
|
|
267
|
+
|
|
268
|
+
Build and test your project. Review warnings — they tell you which files were skipped and why. Fix adapter gaps, re-run on remaining files, and repeat until done. [Report issues](https://github.com/skovhus/styled-components-to-stylex-codemod/issues) with input/output examples if the codemod produces incorrect results.
|
|
269
|
+
|
|
204
270
|
## License
|
|
205
271
|
|
|
206
272
|
MIT
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { t as isSelectorContext } from "./selector-context-heuristic-CGwiJ3HL.mjs";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/bridge-consumer-patcher.ts
|
|
5
|
+
/**
|
|
6
|
+
* Post-transform consumer patching for global selector bridges.
|
|
7
|
+
*
|
|
8
|
+
* After the target component is transformed and gets a bridge className,
|
|
9
|
+
* patch unconverted consumer files to:
|
|
10
|
+
* 1. Import the bridge's GlobalSelector variable from the target module
|
|
11
|
+
* 2. Replace `${Component}` selector references with `${ComponentGlobalSelector}`
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Build a mapping from consumer file paths to their required replacements,
|
|
15
|
+
* cross-referencing prepass selector usages with successful bridge results.
|
|
16
|
+
*/
|
|
17
|
+
function buildConsumerReplacements(selectorUsages, bridgeResults) {
|
|
18
|
+
const consumerReplacements = /* @__PURE__ */ new Map();
|
|
19
|
+
const bridgeLookup = /* @__PURE__ */ new Map();
|
|
20
|
+
for (const [targetPath, results] of bridgeResults) for (const result of results) {
|
|
21
|
+
bridgeLookup.set(`${targetPath}:${result.componentName}`, result);
|
|
22
|
+
if (result.exportName && result.exportName !== result.componentName) bridgeLookup.set(`${targetPath}:${result.exportName}`, result);
|
|
23
|
+
}
|
|
24
|
+
for (const [consumerPath, usages] of selectorUsages) for (const usage of usages) {
|
|
25
|
+
if (usage.consumerIsTransformed) continue;
|
|
26
|
+
const bridge = bridgeLookup.get(`${usage.resolvedPath}:${usage.importedName}`);
|
|
27
|
+
if (!bridge) continue;
|
|
28
|
+
let replacements = consumerReplacements.get(consumerPath);
|
|
29
|
+
if (!replacements) {
|
|
30
|
+
replacements = [];
|
|
31
|
+
consumerReplacements.set(consumerPath, replacements);
|
|
32
|
+
}
|
|
33
|
+
replacements.push({
|
|
34
|
+
localName: usage.localName,
|
|
35
|
+
importSource: usage.importSource,
|
|
36
|
+
globalSelectorVarName: bridge.globalSelectorVarName,
|
|
37
|
+
importedName: usage.importedName
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return consumerReplacements;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Patch a single consumer file:
|
|
44
|
+
* 1. Add import for each GlobalSelector variable
|
|
45
|
+
* 2. Replace `${Component}` in styled template selectors with `${ComponentGlobalSelector}`
|
|
46
|
+
*
|
|
47
|
+
* Returns the patched source or null if no changes were made.
|
|
48
|
+
*/
|
|
49
|
+
function patchConsumerFile(filePath, replacements) {
|
|
50
|
+
let source;
|
|
51
|
+
try {
|
|
52
|
+
source = readFileSync(filePath, "utf-8");
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (replacements.length === 0) return null;
|
|
57
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
58
|
+
for (const r of replacements) {
|
|
59
|
+
let list = bySource.get(r.importSource);
|
|
60
|
+
if (!list) {
|
|
61
|
+
list = [];
|
|
62
|
+
bySource.set(r.importSource, list);
|
|
63
|
+
}
|
|
64
|
+
list.push(r);
|
|
65
|
+
}
|
|
66
|
+
let modified = source;
|
|
67
|
+
for (const [importSource, reps] of bySource) {
|
|
68
|
+
const varNames = reps.map((r) => r.globalSelectorVarName);
|
|
69
|
+
const importRegex = new RegExp(`(import\\s+(?:(?:\\{[^}]*\\}|[^;{]+)\\s+from\\s+['"]${escapeRegExp(importSource)}['"])\\s*;?)`);
|
|
70
|
+
if (modified.match(importRegex)) {
|
|
71
|
+
const namedImportRegex = new RegExp(`(import\\s+(?:[\\w$]+\\s*,\\s*)?\\{)([^}]*)(\\}\\s+from\\s+['"]${escapeRegExp(importSource)}['"]\\s*;?)`);
|
|
72
|
+
const namedMatch = modified.match(namedImportRegex);
|
|
73
|
+
if (namedMatch) {
|
|
74
|
+
const existingNames = namedMatch[2];
|
|
75
|
+
const newNames = varNames.filter((name) => !hasExactImportName(existingNames, name));
|
|
76
|
+
if (newNames.length > 0) {
|
|
77
|
+
const separator = existingNames.trimEnd().endsWith(",") ? " " : ", ";
|
|
78
|
+
modified = modified.replace(namedImportRegex, `$1${existingNames.trimEnd()}${separator}${newNames.join(", ")} $3`);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
const newImport = `import { ${varNames.join(", ")} } from "${importSource}";`;
|
|
82
|
+
modified = modified.replace(importRegex, `$1\n${newImport}`);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
const newImport = `import { ${varNames.join(", ")} } from "${importSource}";`;
|
|
86
|
+
const lastImportIdx = modified.lastIndexOf("\nimport ");
|
|
87
|
+
if (lastImportIdx !== -1) {
|
|
88
|
+
const importEnd = findImportEnd(modified, lastImportIdx + 1);
|
|
89
|
+
modified = modified.slice(0, importEnd) + "\n" + newImport + modified.slice(importEnd);
|
|
90
|
+
} else modified = newImport + "\n" + modified;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const r of replacements) {
|
|
94
|
+
const templateExprRegex = new RegExp(`(\\$\\{\\s*)${escapeRegExp(r.localName)}(\\s*\\})`, "g");
|
|
95
|
+
modified = modified.replace(templateExprRegex, (match, prefix, suffix, offset) => {
|
|
96
|
+
if (isInStyledTemplateSelectorContext(modified, offset, match.length)) return `${prefix}${r.globalSelectorVarName}${suffix}`;
|
|
97
|
+
return match;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return modified !== source ? modified : null;
|
|
101
|
+
}
|
|
102
|
+
/** Find the end position of an import statement starting at startIdx (handles multi-line imports). */
|
|
103
|
+
function findImportEnd(source, startIdx) {
|
|
104
|
+
const semiIdx = source.indexOf(";", startIdx);
|
|
105
|
+
if (semiIdx === -1) return source.indexOf("\n", startIdx);
|
|
106
|
+
return semiIdx + 1;
|
|
107
|
+
}
|
|
108
|
+
/** Check if a template expression at the given position is in a CSS selector context. */
|
|
109
|
+
function isInStyledTemplateSelectorContext(source, offset, length) {
|
|
110
|
+
const after = source.slice(offset + length).trimStart();
|
|
111
|
+
return isSelectorContext(source.slice(Math.max(0, offset - 200), offset).trimEnd(), after);
|
|
112
|
+
}
|
|
113
|
+
/** Check if a name appears as a distinct identifier in an import specifier list string. */
|
|
114
|
+
function hasExactImportName(importSpecifiers, name) {
|
|
115
|
+
return new RegExp(`(?:^|[^A-Za-z0-9_$])${escapeRegExp(name)}(?:$|[^A-Za-z0-9_$])`).test(importSpecifiers);
|
|
116
|
+
}
|
|
117
|
+
function escapeRegExp(s) {
|
|
118
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
export { buildConsumerReplacements, patchConsumerFile };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createExternalInterface } from "./consumer-analyzer.mjs";
|
|
3
|
-
import { t as CollectedWarning } from "./logger-I_hbSbxa.mjs";
|
|
1
|
+
import { a as defineAdapter, i as AdapterInput, t as CollectedWarning } from "./logger-B7SOfCti.mjs";
|
|
4
2
|
|
|
5
3
|
//#region src/run.d.ts
|
|
6
4
|
interface RunTransformOptions {
|
|
@@ -9,11 +7,32 @@ interface RunTransformOptions {
|
|
|
9
7
|
* @example "src/**\/*.tsx" or ["src/**\/*.ts", "src/**\/*.tsx"]
|
|
10
8
|
*/
|
|
11
9
|
files: string | string[];
|
|
10
|
+
/**
|
|
11
|
+
* File glob(s) to scan for cross-file component selector usage, or `null` to opt out.
|
|
12
|
+
*
|
|
13
|
+
* When set to a glob pattern, files matching this glob that are NOT in `files` trigger
|
|
14
|
+
* the bridge strategy (stable bridge className for incremental migration when consumers
|
|
15
|
+
* are not transformed). Files in both globs use the marker sidecar strategy (both
|
|
16
|
+
* consumer and target are transformed).
|
|
17
|
+
*
|
|
18
|
+
* Required when `externalInterface` is `"auto"`.
|
|
19
|
+
*
|
|
20
|
+
* @example "src/**\/*.tsx"
|
|
21
|
+
* @example null // opt out of cross-file scanning
|
|
22
|
+
*/
|
|
23
|
+
consumerPaths: string | string[] | null;
|
|
12
24
|
/**
|
|
13
25
|
* Adapter for customizing the transform.
|
|
14
26
|
* Controls value resolution and resolver-provided imports.
|
|
27
|
+
*
|
|
28
|
+
* Use `externalInterface: "auto"` to auto-detect which exported components
|
|
29
|
+
* need external className/style and polymorphic `as` support by scanning
|
|
30
|
+
* consumer code specified via `consumerPaths` (or `files`).
|
|
31
|
+
*
|
|
32
|
+
* Note: `"auto"` requires prepass scanning to succeed. If prepass fails,
|
|
33
|
+
* runTransform throws instead of silently falling back.
|
|
15
34
|
*/
|
|
16
|
-
adapter:
|
|
35
|
+
adapter: AdapterInput;
|
|
17
36
|
/**
|
|
18
37
|
* Dry run - don't write changes to files
|
|
19
38
|
* @default false
|
|
@@ -37,7 +56,7 @@ interface RunTransformOptions {
|
|
|
37
56
|
formatterCommands?: string[];
|
|
38
57
|
/**
|
|
39
58
|
* Maximum number of examples shown per warning category in the summary.
|
|
40
|
-
* @default
|
|
59
|
+
* @default 3
|
|
41
60
|
*/
|
|
42
61
|
maxExamples?: number;
|
|
43
62
|
}
|
|
@@ -75,6 +94,7 @@ interface RunTransformResult {
|
|
|
75
94
|
*
|
|
76
95
|
* await runTransform({
|
|
77
96
|
* files: 'src/**\/*.tsx',
|
|
97
|
+
* consumerPaths: null,
|
|
78
98
|
* adapter,
|
|
79
99
|
* dryRun: true,
|
|
80
100
|
* });
|
|
@@ -82,4 +102,4 @@ interface RunTransformResult {
|
|
|
82
102
|
*/
|
|
83
103
|
declare function runTransform(options: RunTransformOptions): Promise<RunTransformResult>;
|
|
84
104
|
//#endregion
|
|
85
|
-
export {
|
|
105
|
+
export { type AdapterInput, defineAdapter, runTransform };
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createExternalInterface } from "./consumer-analyzer.mjs";
|
|
1
|
+
import { i as describeValue, r as assertValidAdapterInput, t as Logger } from "./logger-D-R2KB6I.mjs";
|
|
3
2
|
import { run } from "jscodeshift/src/Runner.js";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
7
|
-
import { glob } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
6
|
+
import { glob, writeFile } from "node:fs/promises";
|
|
8
7
|
import { spawn } from "node:child_process";
|
|
9
8
|
|
|
10
9
|
//#region src/adapter.ts
|
|
@@ -64,7 +63,7 @@ import { spawn } from "node:child_process";
|
|
|
64
63
|
* });
|
|
65
64
|
*/
|
|
66
65
|
function defineAdapter(adapter) {
|
|
67
|
-
|
|
66
|
+
assertValidAdapterInput(adapter, "defineAdapter(adapter)");
|
|
68
67
|
return adapter;
|
|
69
68
|
}
|
|
70
69
|
|
|
@@ -95,6 +94,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
95
94
|
*
|
|
96
95
|
* await runTransform({
|
|
97
96
|
* files: 'src/**\/*.tsx',
|
|
97
|
+
* consumerPaths: null,
|
|
98
98
|
* adapter,
|
|
99
99
|
* dryRun: true,
|
|
100
100
|
* });
|
|
@@ -109,7 +109,7 @@ async function runTransform(options) {
|
|
|
109
109
|
"Example (plain JS):",
|
|
110
110
|
" import { runTransform, defineAdapter } from \"styled-components-to-stylex-codemod\";",
|
|
111
111
|
" const adapter = defineAdapter({ resolveValue() { return null; } });",
|
|
112
|
-
" await runTransform({ files: \"src/**/*.tsx\", adapter });"
|
|
112
|
+
" await runTransform({ files: \"src/**/*.tsx\", consumerPaths: null, adapter });"
|
|
113
113
|
].join("\n"));
|
|
114
114
|
const filesValue = options.files;
|
|
115
115
|
if (typeof filesValue !== "string" && !Array.isArray(filesValue)) throw new Error([
|
|
@@ -122,47 +122,52 @@ async function runTransform(options) {
|
|
|
122
122
|
if (filesValue.length === 0) throw new Error(["runTransform(options): `files` must not be an empty array.", "Example: files: [\"src/**/*.ts\", \"src/**/*.tsx\"]"].join("\n"));
|
|
123
123
|
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"));
|
|
124
124
|
}
|
|
125
|
-
|
|
125
|
+
if (options.consumerPaths === void 0) throw new Error([
|
|
126
|
+
"runTransform(options): `consumerPaths` is required.",
|
|
127
|
+
"Pass a glob pattern to enable cross-file selector scanning, or `null` to opt out.",
|
|
128
|
+
"Example: consumerPaths: \"src/**/*.tsx\" // scan for cross-file usage",
|
|
129
|
+
"Example: consumerPaths: null // opt out"
|
|
130
|
+
].join("\n"));
|
|
131
|
+
const { files, consumerPaths: consumerPathsOption, dryRun = false, print = false, parser = "tsx", formatterCommands, maxExamples } = options;
|
|
126
132
|
if (maxExamples !== void 0) Logger.setMaxExamples(maxExamples);
|
|
127
|
-
const
|
|
128
|
-
|
|
133
|
+
const adapterInput = options.adapter;
|
|
134
|
+
assertValidAdapterInput(adapterInput, "runTransform(options)");
|
|
135
|
+
if (adapterInput.externalInterface === "auto" && consumerPathsOption === null) throw new Error([
|
|
136
|
+
"runTransform(options): externalInterface is \"auto\" but consumerPaths is null.",
|
|
137
|
+
"Auto-detection needs consumer file globs to scan for styled(Component) and as-prop usage.",
|
|
138
|
+
"Example: consumerPaths: \"src/**/*.tsx\""
|
|
139
|
+
].join("\n"));
|
|
129
140
|
const resolveValueWithLogging = (ctx) => {
|
|
130
141
|
try {
|
|
131
|
-
return
|
|
142
|
+
return adapterInput.resolveValue(ctx);
|
|
132
143
|
} catch (e) {
|
|
133
144
|
const msg = `adapter.resolveValue threw an error: ${e instanceof Error ? e.message : String(e)}`;
|
|
134
145
|
const filePath = ctx.filePath ?? "<unknown>";
|
|
135
146
|
Logger.logError(msg, filePath, ctx.loc, ctx);
|
|
147
|
+
Logger.markErrorAsLogged(e);
|
|
136
148
|
throw e;
|
|
137
149
|
}
|
|
138
150
|
};
|
|
139
151
|
const resolveCallWithLogging = (ctx) => {
|
|
140
152
|
try {
|
|
141
|
-
return
|
|
153
|
+
return adapterInput.resolveCall(ctx);
|
|
142
154
|
} catch (e) {
|
|
143
155
|
const msg = `adapter.resolveCall threw an error: ${e instanceof Error ? e.message : String(e)}`;
|
|
144
156
|
Logger.logError(msg, ctx.callSiteFilePath, ctx.loc, ctx);
|
|
157
|
+
Logger.markErrorAsLogged(e);
|
|
145
158
|
throw e;
|
|
146
159
|
}
|
|
147
160
|
};
|
|
148
161
|
const resolveSelectorWithLogging = (ctx) => {
|
|
149
162
|
try {
|
|
150
|
-
return
|
|
163
|
+
return adapterInput.resolveSelector(ctx);
|
|
151
164
|
} catch (e) {
|
|
152
165
|
const msg = `adapter.resolveSelector threw an error: ${e instanceof Error ? e.message : String(e)}`;
|
|
153
166
|
Logger.logError(msg, ctx.filePath, ctx.loc, ctx);
|
|
167
|
+
Logger.markErrorAsLogged(e);
|
|
154
168
|
throw e;
|
|
155
169
|
}
|
|
156
170
|
};
|
|
157
|
-
const adapterWithLogging = {
|
|
158
|
-
styleMerger: adapter.styleMerger,
|
|
159
|
-
externalInterface(ctx) {
|
|
160
|
-
return adapter.externalInterface(ctx);
|
|
161
|
-
},
|
|
162
|
-
resolveValue: resolveValueWithLogging,
|
|
163
|
-
resolveCall: resolveCallWithLogging,
|
|
164
|
-
resolveSelector: resolveSelectorWithLogging
|
|
165
|
-
};
|
|
166
171
|
const patterns = Array.isArray(files) ? files : [files];
|
|
167
172
|
const filePaths = [];
|
|
168
173
|
const cwd = process.cwd();
|
|
@@ -179,7 +184,73 @@ async function runTransform(options) {
|
|
|
179
184
|
};
|
|
180
185
|
}
|
|
181
186
|
Logger.setFileCount(filePaths.length);
|
|
182
|
-
const
|
|
187
|
+
const consumerPatterns = consumerPathsOption ? Array.isArray(consumerPathsOption) ? consumerPathsOption : [consumerPathsOption] : [];
|
|
188
|
+
const consumerFilePaths = [];
|
|
189
|
+
for (const pattern of consumerPatterns) for await (const file of glob(pattern, { cwd })) consumerFilePaths.push(file);
|
|
190
|
+
if (consumerPatterns.length > 0 && consumerFilePaths.length === 0) throw new Error([
|
|
191
|
+
"runTransform(options): consumerPaths matched no files.",
|
|
192
|
+
`Pattern(s): ${consumerPatterns.join(", ")}`,
|
|
193
|
+
"Check that the glob pattern is correct and files exist."
|
|
194
|
+
].join("\n"));
|
|
195
|
+
const { createModuleResolver } = await import("./resolve-imports-BDk6Ms09.mjs");
|
|
196
|
+
const sharedResolver = createModuleResolver();
|
|
197
|
+
const { runPrepass } = await import("./run-prepass-DOMrzrA_.mjs");
|
|
198
|
+
const absoluteFiles = filePaths.map((f) => resolve(f));
|
|
199
|
+
const absoluteConsumers = consumerFilePaths.map((f) => resolve(f));
|
|
200
|
+
let prepassResult;
|
|
201
|
+
try {
|
|
202
|
+
prepassResult = await runPrepass({
|
|
203
|
+
filesToTransform: absoluteFiles,
|
|
204
|
+
consumerPaths: absoluteConsumers,
|
|
205
|
+
resolver: sharedResolver,
|
|
206
|
+
parserName: parser,
|
|
207
|
+
createExternalInterface: adapterInput.externalInterface === "auto",
|
|
208
|
+
enableAstCache: true
|
|
209
|
+
});
|
|
210
|
+
} catch (err) {
|
|
211
|
+
if (adapterInput.externalInterface === "auto") throw createAutoPrepassFailureError(err, consumerPatterns, parser);
|
|
212
|
+
Logger.warn(`Prepass failed, continuing without cross-file analysis: ${err instanceof Error ? err.message : String(err)}`);
|
|
213
|
+
prepassResult = {
|
|
214
|
+
crossFileInfo: {
|
|
215
|
+
selectorUsages: /* @__PURE__ */ new Map(),
|
|
216
|
+
componentsNeedingMarkerSidecar: /* @__PURE__ */ new Map(),
|
|
217
|
+
componentsNeedingGlobalSelectorBridge: /* @__PURE__ */ new Map()
|
|
218
|
+
},
|
|
219
|
+
consumerAnalysis: void 0
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const crossFilePrepassResult = prepassResult.crossFileInfo;
|
|
223
|
+
const resolvedAdapter = (() => {
|
|
224
|
+
if (adapterInput.externalInterface === "auto" && prepassResult.consumerAnalysis) {
|
|
225
|
+
const analysisMap = prepassResult.consumerAnalysis;
|
|
226
|
+
return {
|
|
227
|
+
...adapterInput,
|
|
228
|
+
externalInterface: (ctx) => {
|
|
229
|
+
let realPath;
|
|
230
|
+
try {
|
|
231
|
+
realPath = realpathSync(resolve(ctx.filePath));
|
|
232
|
+
} catch {
|
|
233
|
+
realPath = resolve(ctx.filePath);
|
|
234
|
+
}
|
|
235
|
+
return analysisMap.get(`${realPath}:${ctx.componentName}`) ?? {
|
|
236
|
+
styles: false,
|
|
237
|
+
as: false
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return adapterInput;
|
|
243
|
+
})();
|
|
244
|
+
const adapterWithLogging = {
|
|
245
|
+
styleMerger: resolvedAdapter.styleMerger,
|
|
246
|
+
externalInterface(ctx) {
|
|
247
|
+
return resolvedAdapter.externalInterface(ctx);
|
|
248
|
+
},
|
|
249
|
+
resolveValue: resolveValueWithLogging,
|
|
250
|
+
resolveCall: resolveCallWithLogging,
|
|
251
|
+
resolveSelector: resolveSelectorWithLogging
|
|
252
|
+
};
|
|
253
|
+
const transformPath = (() => {
|
|
183
254
|
const adjacent = join(__dirname, "transform.mjs");
|
|
184
255
|
if (existsSync(adjacent)) return adjacent;
|
|
185
256
|
const distSibling = join(__dirname, "..", "dist", "transform.mjs");
|
|
@@ -190,31 +261,34 @@ async function runTransform(options) {
|
|
|
190
261
|
` ${distSibling}`,
|
|
191
262
|
"Run `pnpm build` to generate dist artifacts."
|
|
192
263
|
].join("\n"));
|
|
193
|
-
})()
|
|
264
|
+
})();
|
|
265
|
+
const sidecarFiles = /* @__PURE__ */ new Map();
|
|
266
|
+
const bridgeResults = /* @__PURE__ */ new Map();
|
|
267
|
+
const result = await run(transformPath, filePaths, {
|
|
194
268
|
parser,
|
|
195
269
|
dry: dryRun,
|
|
196
270
|
print,
|
|
197
271
|
adapter: adapterWithLogging,
|
|
272
|
+
crossFilePrepassResult,
|
|
273
|
+
sidecarFiles,
|
|
274
|
+
bridgeResults,
|
|
198
275
|
runInBand: true
|
|
199
276
|
});
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
});
|
|
212
|
-
proc.on("error", reject);
|
|
213
|
-
});
|
|
214
|
-
} catch (e) {
|
|
215
|
-
Logger.warn(`Formatter command failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
277
|
+
if (sidecarFiles.size > 0 && !dryRun) for (const [sidecarPath, content] of sidecarFiles) await writeFile(sidecarPath, mergeSidecarContent(sidecarPath, content), "utf-8");
|
|
278
|
+
if (bridgeResults.size > 0 && !dryRun) {
|
|
279
|
+
const { buildConsumerReplacements, patchConsumerFile } = await import("./bridge-consumer-patcher-D3fRIEkZ.mjs");
|
|
280
|
+
const consumerReplacements = buildConsumerReplacements(crossFilePrepassResult.selectorUsages, bridgeResults);
|
|
281
|
+
const patchedFiles = [];
|
|
282
|
+
for (const [consumerPath, replacements] of consumerReplacements) {
|
|
283
|
+
const patched = patchConsumerFile(consumerPath, replacements);
|
|
284
|
+
if (patched !== null) {
|
|
285
|
+
await writeFile(consumerPath, patched, "utf-8");
|
|
286
|
+
patchedFiles.push(consumerPath);
|
|
287
|
+
}
|
|
216
288
|
}
|
|
289
|
+
if (formatterCommands && patchedFiles.length > 0) await runFormatters(formatterCommands, patchedFiles);
|
|
217
290
|
}
|
|
291
|
+
if (formatterCommands && formatterCommands.length > 0 && result.ok > 0 && !dryRun) await runFormatters(formatterCommands, filePaths);
|
|
218
292
|
const report = Logger.createReport();
|
|
219
293
|
report.print();
|
|
220
294
|
return {
|
|
@@ -226,6 +300,70 @@ async function runTransform(options) {
|
|
|
226
300
|
warnings: report.getWarnings()
|
|
227
301
|
};
|
|
228
302
|
}
|
|
303
|
+
function createAutoPrepassFailureError(err, consumerPatterns, parser) {
|
|
304
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
305
|
+
return new Error([
|
|
306
|
+
"runTransform(options): prepass failed while using externalInterface: \"auto\".",
|
|
307
|
+
"\"auto\" depends on successful prepass scanning and cannot continue without it.",
|
|
308
|
+
`Underlying error: ${reason}`,
|
|
309
|
+
"",
|
|
310
|
+
"Troubleshooting:",
|
|
311
|
+
" - Verify `consumerPaths` glob(s) and file syntax.",
|
|
312
|
+
` - Confirm parser setting matches your code (current parser: ${JSON.stringify(parser)}).`,
|
|
313
|
+
" - Check module resolution inputs (tsconfig paths / imports).",
|
|
314
|
+
" - Use a manual `externalInterface(ctx)` function to continue without auto-detection.",
|
|
315
|
+
"",
|
|
316
|
+
`consumerPaths: ${consumerPatterns.length > 0 ? consumerPatterns.join(", ") : "(none)"}`
|
|
317
|
+
].join("\n"));
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Merge new sidecar marker content into an existing .stylex.ts file, preserving
|
|
321
|
+
* user-owned exports (e.g. defineVars). If the file doesn't exist, returns content as-is.
|
|
322
|
+
*
|
|
323
|
+
* New marker declarations (`export const XMarker = stylex.defineMarker()`) are
|
|
324
|
+
* appended only if they don't already exist in the file. The stylex import is
|
|
325
|
+
* ensured at the top.
|
|
326
|
+
*/
|
|
327
|
+
function mergeSidecarContent(sidecarPath, newContent) {
|
|
328
|
+
let existing;
|
|
329
|
+
try {
|
|
330
|
+
existing = readFileSync(sidecarPath, "utf-8");
|
|
331
|
+
} catch {
|
|
332
|
+
return newContent;
|
|
333
|
+
}
|
|
334
|
+
const markerLineRe = /^export const \w+ = stylex\.defineMarker\(\);$/gm;
|
|
335
|
+
const newMarkers = [];
|
|
336
|
+
for (const m of newContent.matchAll(markerLineRe)) newMarkers.push(m[0]);
|
|
337
|
+
if (newMarkers.length === 0) return newContent;
|
|
338
|
+
const markersToAdd = newMarkers.filter((line) => !existing.includes(line));
|
|
339
|
+
if (markersToAdd.length === 0) return existing;
|
|
340
|
+
let merged = existing;
|
|
341
|
+
if (!merged.includes("@stylexjs/stylex")) merged = `import * as stylex from "@stylexjs/stylex";\n\n${merged}`;
|
|
342
|
+
const trailingNewline = merged.endsWith("\n") ? "" : "\n";
|
|
343
|
+
merged = merged + trailingNewline + markersToAdd.join("\n") + "\n";
|
|
344
|
+
return merged;
|
|
345
|
+
}
|
|
346
|
+
/** Run formatter commands on a list of files, logging warnings on failure. */
|
|
347
|
+
async function runFormatters(commands, files) {
|
|
348
|
+
for (const formatterCommand of commands) {
|
|
349
|
+
const [cmd, ...cmdArgs] = formatterCommand.split(/\s+/);
|
|
350
|
+
if (cmd) try {
|
|
351
|
+
await new Promise((res, rej) => {
|
|
352
|
+
const proc = spawn(cmd, [...cmdArgs, ...files], {
|
|
353
|
+
stdio: "inherit",
|
|
354
|
+
shell: true
|
|
355
|
+
});
|
|
356
|
+
proc.on("close", (code) => {
|
|
357
|
+
if (code === 0) res();
|
|
358
|
+
else rej(/* @__PURE__ */ new Error(`Formatter command exited with code ${code}`));
|
|
359
|
+
});
|
|
360
|
+
proc.on("error", rej);
|
|
361
|
+
});
|
|
362
|
+
} catch (e) {
|
|
363
|
+
Logger.warn(`Formatter command failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
229
367
|
|
|
230
368
|
//#endregion
|
|
231
|
-
export {
|
|
369
|
+
export { defineAdapter, runTransform };
|