styled-components-to-stylex-codemod 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -34
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +46 -19
- package/dist/{logger-DKelw2HS.mjs → logger-BLeJjMzG.mjs} +149 -20
- package/dist/logger-DC-1uogs.d.mts +452 -0
- package/dist/transform.d.mts +1 -2
- package/dist/transform.mjs +18014 -9848
- package/package.json +30 -27
- package/dist/logger-CNmtK-uJ.d.mts +0 -250
package/README.md
CHANGED
|
@@ -21,10 +21,7 @@ pnpm add styled-components-to-stylex-codemod
|
|
|
21
21
|
Use `runTransform` to transform files matching a glob pattern:
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
|
-
import {
|
|
25
|
-
runTransform,
|
|
26
|
-
defineAdapter,
|
|
27
|
-
} from "styled-components-to-stylex-codemod";
|
|
24
|
+
import { runTransform, defineAdapter } from "styled-components-to-stylex-codemod";
|
|
28
25
|
|
|
29
26
|
const adapter = defineAdapter({
|
|
30
27
|
resolveValue(ctx) {
|
|
@@ -70,9 +67,7 @@ const adapter = defineAdapter({
|
|
|
70
67
|
.replace(/^--/, "")
|
|
71
68
|
.split("-")
|
|
72
69
|
.filter(Boolean)
|
|
73
|
-
.map((part, i) =>
|
|
74
|
-
i === 0 ? part : part[0]?.toUpperCase() + part.slice(1)
|
|
75
|
-
)
|
|
70
|
+
.map((part, i) => (i === 0 ? part : part[0]?.toUpperCase() + part.slice(1)))
|
|
76
71
|
.join("");
|
|
77
72
|
|
|
78
73
|
// If you care about fallbacks, you can use `fallback` here to decide whether to resolve or not.
|
|
@@ -97,18 +92,20 @@ const adapter = defineAdapter({
|
|
|
97
92
|
// `calleeSource` tells you where it came from:
|
|
98
93
|
// - { kind: "absolutePath", value: "/abs/path" } for relative imports
|
|
99
94
|
// - { kind: "specifier", value: "some-package/foo" } for package imports
|
|
95
|
+
//
|
|
96
|
+
// The codemod determines how to use the result based on context:
|
|
97
|
+
// - If `ctx.cssProperty` exists (e.g., `border: ${helper()}`) → result is used as a CSS value
|
|
98
|
+
// - If `ctx.cssProperty` is undefined (e.g., `${helper()}`) → result is used as a StyleX style object
|
|
99
|
+
//
|
|
100
|
+
// Use `ctx.cssProperty` to return the appropriate expression for the context.
|
|
100
101
|
|
|
101
102
|
const arg0 = ctx.args[0];
|
|
102
|
-
const key =
|
|
103
|
-
arg0?.kind === "literal" && typeof arg0.value === "string"
|
|
104
|
-
? arg0.value
|
|
105
|
-
: null;
|
|
103
|
+
const key = arg0?.kind === "literal" && typeof arg0.value === "string" ? arg0.value : null;
|
|
106
104
|
if (ctx.calleeImportedName !== "transitionSpeed" || !key) {
|
|
107
105
|
return null;
|
|
108
106
|
}
|
|
109
107
|
|
|
110
108
|
return {
|
|
111
|
-
usage: "create",
|
|
112
109
|
expr: `transitionSpeedVars.${key}`,
|
|
113
110
|
imports: [
|
|
114
111
|
{
|
|
@@ -119,9 +116,11 @@ const adapter = defineAdapter({
|
|
|
119
116
|
};
|
|
120
117
|
},
|
|
121
118
|
|
|
122
|
-
|
|
123
|
-
return
|
|
119
|
+
externalInterface() {
|
|
120
|
+
return null;
|
|
124
121
|
},
|
|
122
|
+
|
|
123
|
+
styleMerger: null,
|
|
125
124
|
});
|
|
126
125
|
|
|
127
126
|
const result = await runTransform({
|
|
@@ -139,10 +138,10 @@ console.log(result);
|
|
|
139
138
|
|
|
140
139
|
Adapters are the main extension point. They let you control:
|
|
141
140
|
|
|
142
|
-
- how theme paths
|
|
141
|
+
- how theme paths, CSS variables, and imported values are turned into StyleX-compatible JS values (`resolveValue`)
|
|
143
142
|
- what extra imports to inject into transformed files (returned from `resolveValue`)
|
|
144
|
-
- how helper calls are resolved (via `resolveCall({ ... })` returning `
|
|
145
|
-
- which exported components should support external className/style extension (`
|
|
143
|
+
- how helper calls are resolved (via `resolveCall({ ... })` returning `{ expr, imports }`; `null`/`undefined` bails the file)
|
|
144
|
+
- which exported components should support external className/style extension and/or polymorphic `as` prop (`externalInterface`)
|
|
146
145
|
- how className/style merging is handled for components accepting external styling (`styleMerger`)
|
|
147
146
|
|
|
148
147
|
#### Style Merger
|
|
@@ -160,8 +159,15 @@ const adapter = defineAdapter({
|
|
|
160
159
|
return null;
|
|
161
160
|
},
|
|
162
161
|
|
|
163
|
-
|
|
164
|
-
return
|
|
162
|
+
resolveCall() {
|
|
163
|
+
return null;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
externalInterface(ctx) {
|
|
167
|
+
if (ctx.filePath.includes("/shared/components/")) {
|
|
168
|
+
return { styles: true };
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
165
171
|
},
|
|
166
172
|
|
|
167
173
|
// Use a custom merger function for cleaner output
|
|
@@ -178,15 +184,15 @@ The merger function should have this signature:
|
|
|
178
184
|
function mergedSx(
|
|
179
185
|
styles: StyleXStyles,
|
|
180
186
|
className?: string,
|
|
181
|
-
style?: React.CSSProperties
|
|
187
|
+
style?: React.CSSProperties,
|
|
182
188
|
): ReturnType<typeof stylex.props>;
|
|
183
189
|
```
|
|
184
190
|
|
|
185
191
|
See [`test-cases/lib/mergedSx.ts`](./test-cases/lib/mergedSx.ts) for a reference implementation.
|
|
186
192
|
|
|
187
|
-
#### External Styles Support
|
|
193
|
+
#### External Interface (Styles and Polymorphic `as` Support)
|
|
188
194
|
|
|
189
|
-
Transformed components are "closed" by default — they don't accept external `className` or `style` props. Use `
|
|
195
|
+
Transformed components are "closed" by default — they don't accept external `className` or `style` props, and exported components only get `as` support when it is used inside the file. Use `externalInterface` to control which exported components should support these features:
|
|
190
196
|
|
|
191
197
|
```ts
|
|
192
198
|
const adapter = defineAdapter({
|
|
@@ -195,39 +201,59 @@ const adapter = defineAdapter({
|
|
|
195
201
|
return null;
|
|
196
202
|
},
|
|
197
203
|
|
|
198
|
-
|
|
204
|
+
resolveCall() {
|
|
205
|
+
return null;
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
externalInterface(ctx) {
|
|
199
209
|
// ctx: { filePath, componentName, exportName, isDefaultExport }
|
|
200
210
|
|
|
201
|
-
// Example: Enable for all exports in shared components folder
|
|
211
|
+
// Example: Enable styles (and `as`) for all exports in shared components folder
|
|
202
212
|
if (ctx.filePath.includes("/shared/components/")) {
|
|
203
|
-
return true;
|
|
213
|
+
return { styles: true };
|
|
204
214
|
}
|
|
205
215
|
|
|
206
|
-
// Example: Enable
|
|
207
|
-
if (ctx.componentName === "
|
|
208
|
-
return true;
|
|
216
|
+
// Example: Enable only `as` prop (no style merging)
|
|
217
|
+
if (ctx.componentName === "Typography") {
|
|
218
|
+
return { styles: false, as: true };
|
|
209
219
|
}
|
|
210
220
|
|
|
211
|
-
|
|
221
|
+
// Disable both (default)
|
|
222
|
+
return null;
|
|
212
223
|
},
|
|
224
|
+
|
|
225
|
+
styleMerger: null,
|
|
213
226
|
});
|
|
214
227
|
```
|
|
215
228
|
|
|
216
|
-
|
|
229
|
+
The `externalInterface` method returns:
|
|
230
|
+
|
|
231
|
+
- `null` — no external interface (neither className/style nor `as` prop)
|
|
232
|
+
- `{ styles: true }` — accept className/style props AND polymorphic `as` prop
|
|
233
|
+
- `{ styles: false, as: true }` — accept only polymorphic `as` prop (no style merging)
|
|
234
|
+
- `{ styles: false, as: false }` — equivalent to `null`
|
|
235
|
+
|
|
236
|
+
When `styles: true`, the generated component will:
|
|
217
237
|
|
|
218
238
|
- Accept `className` and `style` props
|
|
219
239
|
- Merge them with the StyleX-generated styles
|
|
220
240
|
- Forward remaining props via `...rest`
|
|
241
|
+
- Accept polymorphic `as` prop (required for style merging to work correctly)
|
|
242
|
+
|
|
243
|
+
When `{ styles: false, as: true }`, the generated component will accept a polymorphic `as` prop but won't include className/style merging.
|
|
221
244
|
|
|
222
245
|
#### Dynamic interpolations
|
|
223
246
|
|
|
224
247
|
When the codemod encounters an interpolation inside a styled template literal, it runs an internal dynamic resolution pipeline which covers common cases like:
|
|
225
248
|
|
|
226
249
|
- theme access (`props.theme...`) via `resolveValue({ kind: "theme", path })`
|
|
250
|
+
- imported value access (`import { zIndex } ...; ${zIndex.popover}`) via `resolveValue({ kind: "importedValue", importedName, source, path })`
|
|
227
251
|
- prop access (`props.foo`) and conditionals (`props.foo ? "a" : "b"`, `props.foo && "color: red;"`)
|
|
228
|
-
-
|
|
229
|
-
-
|
|
230
|
-
-
|
|
252
|
+
- helper calls (`transitionSpeed("slowTransition")`) via `resolveCall({ ... })` — the codemod infers usage from context:
|
|
253
|
+
- With `ctx.cssProperty` (e.g., `color: ${helper()}`) → result used as CSS value in `stylex.create()`
|
|
254
|
+
- Without `ctx.cssProperty` (e.g., `${helper()}`) → result used as StyleX styles in `stylex.props()`
|
|
255
|
+
- Use the optional `usage: "create" | "props"` field to override the default inference
|
|
256
|
+
- if `resolveCall` returns `null` or `undefined`, the transform **bails the file** and logs a warning
|
|
231
257
|
- helper calls applied to prop values (e.g. `shadow(props.shadow)`) by emitting a StyleX style function that calls the helper at runtime
|
|
232
258
|
- conditional CSS blocks via ternary (e.g. `props.$dim ? "opacity: 0.5;" : ""`)
|
|
233
259
|
|
|
@@ -239,8 +265,8 @@ If the pipeline can’t resolve an interpolation:
|
|
|
239
265
|
### Limitations
|
|
240
266
|
|
|
241
267
|
- **Flow** type generation is non-existing, works best with TypeScript or plain JS right now. Contributions more than welcome!
|
|
242
|
-
- **ThemeProvider**: if a file imports and uses `ThemeProvider` from `styled-components`, the transform **skips the entire file** (theming strategy is project-specific).
|
|
243
268
|
- **createGlobalStyle**: detected usage is reported as an **unsupported-feature** warning (StyleX does not support global styles in the same way).
|
|
269
|
+
- **Theme prop overrides**: passing a `theme` prop directly to styled components (e.g. `<Button theme={...} />`) is not supported and will bail with a warning.
|
|
244
270
|
|
|
245
271
|
## License
|
|
246
272
|
|
package/dist/index.d.mts
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as assertValidAdapter, r as describeValue, t as Logger } from "./logger-
|
|
1
|
+
import { n as assertValidAdapter, r as describeValue, t as Logger } from "./logger-BLeJjMzG.mjs";
|
|
2
2
|
import { run } from "jscodeshift/src/Runner.js";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
@@ -8,7 +8,8 @@ import { spawn } from "node:child_process";
|
|
|
8
8
|
|
|
9
9
|
//#region src/adapter.ts
|
|
10
10
|
/**
|
|
11
|
-
* Adapter
|
|
11
|
+
* Adapter entry point for customizing the codemod.
|
|
12
|
+
* Core concepts: value resolution hooks and adapter validation.
|
|
12
13
|
*/
|
|
13
14
|
/**
|
|
14
15
|
* Helper for nicer user authoring + type inference.
|
|
@@ -27,23 +28,33 @@ import { spawn } from "node:child_process";
|
|
|
27
28
|
* ],
|
|
28
29
|
* };
|
|
29
30
|
* }
|
|
30
|
-
*
|
|
31
|
+
* // Return undefined to bail/skip the file
|
|
31
32
|
* },
|
|
32
33
|
*
|
|
33
34
|
* resolveCall(ctx) {
|
|
34
35
|
* // Resolve helper calls inside template interpolations.
|
|
36
|
+
* // Use ctx.cssProperty to determine context:
|
|
37
|
+
* // - If ctx.cssProperty exists → return a CSS value expression
|
|
38
|
+
* // - If ctx.cssProperty is undefined → return a StyleX style object reference
|
|
39
|
+
* // Return { expr, imports } or undefined to bail/skip the file
|
|
40
|
+
* void ctx;
|
|
41
|
+
* },
|
|
42
|
+
*
|
|
43
|
+
* resolveSelector(ctx) {
|
|
44
|
+
* // Resolve imported values used in selector position (e.g., media query helpers).
|
|
35
45
|
* // Return:
|
|
36
|
-
* // - {
|
|
37
|
-
* // -
|
|
38
|
-
* // - null to leave the call unresolved
|
|
46
|
+
* // - { kind: "media", expr, imports } for media queries (e.g., breakpoints.phone)
|
|
47
|
+
* // - undefined to bail/skip the file
|
|
39
48
|
* void ctx;
|
|
40
|
-
* return null;
|
|
41
49
|
* },
|
|
42
50
|
*
|
|
43
|
-
* //
|
|
44
|
-
*
|
|
45
|
-
* // Example: Enable
|
|
46
|
-
*
|
|
51
|
+
* // Configure external interface for exported components
|
|
52
|
+
* externalInterface(ctx) {
|
|
53
|
+
* // Example: Enable styles (and `as`) for shared components folder
|
|
54
|
+
* if (ctx.filePath.includes("/shared/components/")) {
|
|
55
|
+
* return { styles: true };
|
|
56
|
+
* }
|
|
57
|
+
* return null;
|
|
47
58
|
* },
|
|
48
59
|
*
|
|
49
60
|
* // Optional: provide a custom merger, or use `null` for the default verbose merge output
|
|
@@ -57,6 +68,10 @@ function defineAdapter(adapter) {
|
|
|
57
68
|
|
|
58
69
|
//#endregion
|
|
59
70
|
//#region src/run.ts
|
|
71
|
+
/**
|
|
72
|
+
* Runs the codemod over input files with an adapter.
|
|
73
|
+
* Core concepts: jscodeshift execution, globs, and adapter hooks.
|
|
74
|
+
*/
|
|
60
75
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
61
76
|
/**
|
|
62
77
|
* Run the styled-components to StyleX transform on files matching the glob pattern.
|
|
@@ -114,7 +129,7 @@ async function runTransform(options) {
|
|
|
114
129
|
} catch (e) {
|
|
115
130
|
const msg = `adapter.resolveValue threw an error: ${e instanceof Error ? e.message : String(e)}`;
|
|
116
131
|
const filePath = ctx.filePath ?? "<unknown>";
|
|
117
|
-
Logger.logError(msg, filePath,
|
|
132
|
+
Logger.logError(msg, filePath, ctx.loc, ctx);
|
|
118
133
|
throw e;
|
|
119
134
|
}
|
|
120
135
|
};
|
|
@@ -123,17 +138,27 @@ async function runTransform(options) {
|
|
|
123
138
|
return adapter.resolveCall(ctx);
|
|
124
139
|
} catch (e) {
|
|
125
140
|
const msg = `adapter.resolveCall threw an error: ${e instanceof Error ? e.message : String(e)}`;
|
|
126
|
-
Logger.logError(msg, ctx.callSiteFilePath,
|
|
141
|
+
Logger.logError(msg, ctx.callSiteFilePath, ctx.loc, ctx);
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const resolveSelectorWithLogging = (ctx) => {
|
|
146
|
+
try {
|
|
147
|
+
return adapter.resolveSelector(ctx);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
const msg = `adapter.resolveSelector threw an error: ${e instanceof Error ? e.message : String(e)}`;
|
|
150
|
+
Logger.logError(msg, ctx.filePath, ctx.loc, ctx);
|
|
127
151
|
throw e;
|
|
128
152
|
}
|
|
129
153
|
};
|
|
130
154
|
const adapterWithLogging = {
|
|
131
155
|
styleMerger: adapter.styleMerger,
|
|
132
|
-
|
|
133
|
-
return adapter.
|
|
156
|
+
externalInterface(ctx) {
|
|
157
|
+
return adapter.externalInterface(ctx);
|
|
134
158
|
},
|
|
135
159
|
resolveValue: resolveValueWithLogging,
|
|
136
|
-
resolveCall: resolveCallWithLogging
|
|
160
|
+
resolveCall: resolveCallWithLogging,
|
|
161
|
+
resolveSelector: resolveSelectorWithLogging
|
|
137
162
|
};
|
|
138
163
|
const patterns = Array.isArray(files) ? files : [files];
|
|
139
164
|
const filePaths = [];
|
|
@@ -171,13 +196,13 @@ async function runTransform(options) {
|
|
|
171
196
|
if (formatterCommand && result.ok > 0 && !dryRun) {
|
|
172
197
|
const [cmd, ...cmdArgs] = formatterCommand.split(/\s+/);
|
|
173
198
|
if (cmd) try {
|
|
174
|
-
await new Promise((resolve
|
|
199
|
+
await new Promise((resolve, reject) => {
|
|
175
200
|
const proc = spawn(cmd, [...cmdArgs, ...filePaths], {
|
|
176
201
|
stdio: "inherit",
|
|
177
202
|
shell: true
|
|
178
203
|
});
|
|
179
204
|
proc.on("close", (code) => {
|
|
180
|
-
if (code === 0) resolve
|
|
205
|
+
if (code === 0) resolve();
|
|
181
206
|
else reject(/* @__PURE__ */ new Error(`Formatter command exited with code ${code}`));
|
|
182
207
|
});
|
|
183
208
|
proc.on("error", reject);
|
|
@@ -186,13 +211,15 @@ async function runTransform(options) {
|
|
|
186
211
|
Logger.warn(`Formatter command failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
187
212
|
}
|
|
188
213
|
}
|
|
214
|
+
const report = Logger.createReport();
|
|
215
|
+
report.print();
|
|
189
216
|
return {
|
|
190
217
|
errors: result.error,
|
|
191
218
|
unchanged: result.nochange,
|
|
192
219
|
skipped: result.skip,
|
|
193
220
|
transformed: result.ok,
|
|
194
221
|
timeElapsed: parseFloat(result.timeElapsed) || 0,
|
|
195
|
-
warnings:
|
|
222
|
+
warnings: report.getWarnings()
|
|
196
223
|
};
|
|
197
224
|
}
|
|
198
225
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
1
3
|
//#region src/internal/public-api-validation.ts
|
|
2
4
|
function describeValue(value) {
|
|
3
5
|
if (value === null) return "null";
|
|
@@ -24,7 +26,8 @@ function assertValidAdapter(candidate, where) {
|
|
|
24
26
|
const obj = candidate;
|
|
25
27
|
const resolveValue = obj?.resolveValue;
|
|
26
28
|
const resolveCall = obj?.resolveCall;
|
|
27
|
-
const
|
|
29
|
+
const resolveSelector = obj?.resolveSelector;
|
|
30
|
+
const externalInterface = obj?.externalInterface;
|
|
28
31
|
if (!candidate || typeof candidate !== "object") throw new Error([
|
|
29
32
|
`${where}: expected an adapter object.`,
|
|
30
33
|
`Received: ${describeValue(candidate)}`,
|
|
@@ -32,15 +35,20 @@ function assertValidAdapter(candidate, where) {
|
|
|
32
35
|
"Adapter requirements:",
|
|
33
36
|
" - adapter.resolveValue(context) is required",
|
|
34
37
|
" - adapter.resolveCall(context) is required",
|
|
35
|
-
" - adapter.
|
|
38
|
+
" - adapter.resolveSelector(context) is required",
|
|
39
|
+
" - adapter.externalInterface(context) is required",
|
|
36
40
|
"",
|
|
37
41
|
"resolveValue(context) is called with one of these shapes:",
|
|
38
42
|
" - { kind: \"theme\", path }",
|
|
39
43
|
" - { kind: \"cssVariable\", name, fallback?, definedValue? }",
|
|
44
|
+
" - { kind: \"importedValue\", importedName, source, path? }",
|
|
40
45
|
"",
|
|
41
46
|
"resolveCall(context) is called with:",
|
|
42
47
|
" - { callSiteFilePath, calleeImportedName, calleeSource, args }",
|
|
43
48
|
"",
|
|
49
|
+
"resolveSelector(context) is called with:",
|
|
50
|
+
" - { kind: \"selectorInterpolation\", importedName, source, path? }",
|
|
51
|
+
"",
|
|
44
52
|
`Docs/examples: ${ADAPTER_DOCS_URL}`
|
|
45
53
|
].join("\n"));
|
|
46
54
|
if (typeof resolveValue !== "function") throw new Error([
|
|
@@ -52,7 +60,7 @@ function assertValidAdapter(candidate, where) {
|
|
|
52
60
|
" resolveValue(context) {",
|
|
53
61
|
" // theme/cssVariable -> { expr, imports, dropDefinition? } | null",
|
|
54
62
|
" }",
|
|
55
|
-
" resolveCall(context) { return {
|
|
63
|
+
" resolveCall(context) { return { expr, imports } | null }",
|
|
56
64
|
" }",
|
|
57
65
|
"",
|
|
58
66
|
`Docs/examples: ${ADAPTER_DOCS_URL}`
|
|
@@ -63,12 +71,23 @@ function assertValidAdapter(candidate, where) {
|
|
|
63
71
|
"",
|
|
64
72
|
"Adapter shape:",
|
|
65
73
|
" {",
|
|
66
|
-
" resolveCall(context) { return {
|
|
74
|
+
" resolveCall(context) { return { expr: string, imports: ImportSpec[] } | null }",
|
|
75
|
+
" }",
|
|
76
|
+
"",
|
|
77
|
+
`Docs/examples: ${ADAPTER_DOCS_URL}`
|
|
78
|
+
].join("\n"));
|
|
79
|
+
if (typeof resolveSelector !== "function") throw new Error([
|
|
80
|
+
`${where}: adapter.resolveSelector must be a function.`,
|
|
81
|
+
`Received: resolveSelector=${describeValue(resolveSelector)}`,
|
|
82
|
+
"",
|
|
83
|
+
"Adapter shape:",
|
|
84
|
+
" {",
|
|
85
|
+
" resolveSelector(context) { return { kind: \"media\", expr: string, imports: ImportSpec[] } | undefined }",
|
|
67
86
|
" }",
|
|
68
87
|
"",
|
|
69
88
|
`Docs/examples: ${ADAPTER_DOCS_URL}`
|
|
70
89
|
].join("\n"));
|
|
71
|
-
if (typeof
|
|
90
|
+
if (typeof externalInterface !== "function") throw new Error([`${where}: adapter.externalInterface must be a function.`, `Received: externalInterface=${describeValue(externalInterface)}`].join("\n"));
|
|
72
91
|
const styleMerger = obj?.styleMerger;
|
|
73
92
|
if (styleMerger !== null && styleMerger !== void 0) {
|
|
74
93
|
if (typeof styleMerger !== "object") throw new Error([
|
|
@@ -101,21 +120,20 @@ const ADAPTER_DOCS_URL = `https://github.com/skovhus/styled-components-to-stylex
|
|
|
101
120
|
|
|
102
121
|
//#endregion
|
|
103
122
|
//#region src/internal/logger.ts
|
|
123
|
+
/**
|
|
124
|
+
* Logger and warning types for transform diagnostics.
|
|
125
|
+
* Core concepts: severity classification and source context reporting.
|
|
126
|
+
*/
|
|
104
127
|
var Logger = class Logger {
|
|
105
|
-
static flushWarnings() {
|
|
106
|
-
const result = collected;
|
|
107
|
-
collected = [];
|
|
108
|
-
return result;
|
|
109
|
-
}
|
|
110
128
|
/**
|
|
111
|
-
* Log a warning message to
|
|
129
|
+
* Log a warning message to stdout.
|
|
112
130
|
* All codemod warnings go through this so tests can mock it.
|
|
113
131
|
*/
|
|
114
132
|
static warn(message, context) {
|
|
115
133
|
Logger.writeWithSpacing(message, context);
|
|
116
134
|
}
|
|
117
135
|
/**
|
|
118
|
-
* Log an error message to
|
|
136
|
+
* Log an error message to stdout with file path and optional location.
|
|
119
137
|
* Formats like warnings: "Error filepath:line:column\nmessage"
|
|
120
138
|
*/
|
|
121
139
|
static logError(message, filePath, loc, context) {
|
|
@@ -124,23 +142,34 @@ var Logger = class Logger {
|
|
|
124
142
|
Logger.writeWithSpacing(`${label} ${location}\n${message}`, context);
|
|
125
143
|
}
|
|
126
144
|
/**
|
|
127
|
-
* Log transform warnings to
|
|
145
|
+
* Log transform warnings to stdout and collect them.
|
|
128
146
|
*/
|
|
129
147
|
static logWarnings(warnings, filePath) {
|
|
130
148
|
for (const warning of warnings) {
|
|
131
|
-
collected.push({
|
|
149
|
+
Logger.collected.push({
|
|
132
150
|
...warning,
|
|
133
151
|
filePath
|
|
134
152
|
});
|
|
135
153
|
const location = warning.loc ? `${filePath}:${warning.loc.line}:${warning.loc.column}` : `${filePath}`;
|
|
136
154
|
const label = Logger.colorizeSeverityLabel(warning.severity);
|
|
137
|
-
Logger.writeWithSpacing(`${label} ${location}\n${warning.
|
|
155
|
+
Logger.writeWithSpacing(`${label} ${location}\n${warning.type}`, warning.context);
|
|
138
156
|
}
|
|
139
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Create a report from all collected warnings.
|
|
160
|
+
*/
|
|
161
|
+
static createReport() {
|
|
162
|
+
return new LoggerReport([...Logger.collected]);
|
|
163
|
+
}
|
|
164
|
+
/** @internal - for testing only */
|
|
165
|
+
static _clearCollected() {
|
|
166
|
+
Logger.collected = [];
|
|
167
|
+
}
|
|
168
|
+
static collected = [];
|
|
140
169
|
static writeWithSpacing(message, context) {
|
|
141
170
|
const trimmed = message.replace(/\s+$/u, "");
|
|
142
171
|
const serialized = Logger.formatContext(context);
|
|
143
|
-
process.
|
|
172
|
+
process.stdout.write(`${trimmed}${serialized ? `\n${serialized}` : ""}\n\n`);
|
|
144
173
|
}
|
|
145
174
|
static colorizeSeverityLabel(severity) {
|
|
146
175
|
if (severity === "error") return Logger.colorizeErrorLabel("Error");
|
|
@@ -148,15 +177,15 @@ var Logger = class Logger {
|
|
|
148
177
|
return Logger.colorizeWarnLabel("Warning");
|
|
149
178
|
}
|
|
150
179
|
static colorizeWarnLabel(label) {
|
|
151
|
-
if (!process.
|
|
180
|
+
if (!process.stdout.isTTY) return label;
|
|
152
181
|
return `${WARN_BG_COLOR}${WARN_TEXT_COLOR}${label}${RESET_COLOR}`;
|
|
153
182
|
}
|
|
154
183
|
static colorizeErrorLabel(label) {
|
|
155
|
-
if (!process.
|
|
184
|
+
if (!process.stdout.isTTY) return label;
|
|
156
185
|
return `${ERROR_BG_COLOR}${ERROR_TEXT_COLOR}${label}${RESET_COLOR}`;
|
|
157
186
|
}
|
|
158
187
|
static colorizeInfoLabel(label) {
|
|
159
|
-
if (!process.
|
|
188
|
+
if (!process.stdout.isTTY) return label;
|
|
160
189
|
return `${INFO_BG_COLOR}${INFO_TEXT_COLOR}${label}${RESET_COLOR}`;
|
|
161
190
|
}
|
|
162
191
|
static formatContext(context) {
|
|
@@ -164,13 +193,113 @@ var Logger = class Logger {
|
|
|
164
193
|
return JSON.stringify(context, null, 2);
|
|
165
194
|
}
|
|
166
195
|
};
|
|
167
|
-
|
|
196
|
+
var LoggerReport = class {
|
|
197
|
+
warnings;
|
|
198
|
+
fileCache = /* @__PURE__ */ new Map();
|
|
199
|
+
constructor(warnings) {
|
|
200
|
+
this.warnings = warnings;
|
|
201
|
+
}
|
|
202
|
+
getWarnings() {
|
|
203
|
+
return this.warnings;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Get the formatted report as a string.
|
|
207
|
+
*/
|
|
208
|
+
toString() {
|
|
209
|
+
if (this.warnings.length === 0) return "";
|
|
210
|
+
const lines = [];
|
|
211
|
+
const groups = this.groupWarnings();
|
|
212
|
+
lines.push("");
|
|
213
|
+
lines.push("─".repeat(60));
|
|
214
|
+
lines.push(`Warning Summary: ${this.warnings.length} warning(s) in ${groups.length} category(s)`);
|
|
215
|
+
lines.push("─".repeat(60));
|
|
216
|
+
const MAX_EXAMPLES = 15;
|
|
217
|
+
for (const group of groups) {
|
|
218
|
+
lines.push("");
|
|
219
|
+
lines.push(`▸ ${group.message} (${group.warnings.length})`);
|
|
220
|
+
lines.push("");
|
|
221
|
+
const seenFiles = /* @__PURE__ */ new Set();
|
|
222
|
+
const uniqueLocations = [];
|
|
223
|
+
for (const loc of group.warnings) if (!seenFiles.has(loc.filePath)) {
|
|
224
|
+
seenFiles.add(loc.filePath);
|
|
225
|
+
uniqueLocations.push(loc);
|
|
226
|
+
}
|
|
227
|
+
const displayed = uniqueLocations.slice(0, MAX_EXAMPLES);
|
|
228
|
+
for (const loc of displayed) {
|
|
229
|
+
const location = loc.loc ? `${loc.filePath}:${loc.loc.line}:${loc.loc.column}` : loc.filePath;
|
|
230
|
+
lines.push(` ${location}`);
|
|
231
|
+
if (loc.snippet) lines.push(loc.snippet);
|
|
232
|
+
lines.push("");
|
|
233
|
+
}
|
|
234
|
+
const remaining = uniqueLocations.length - MAX_EXAMPLES;
|
|
235
|
+
if (remaining > 0) {
|
|
236
|
+
lines.push(` ... and ${remaining} more file(s)`);
|
|
237
|
+
lines.push("");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return lines.join("\n");
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Print the formatted warning report to stdout.
|
|
244
|
+
*/
|
|
245
|
+
print() {
|
|
246
|
+
const output = this.toString();
|
|
247
|
+
if (output) {
|
|
248
|
+
const colored = output.replace(/▸ (.+?) \((\d+)\)/g, `${SECTION_COLOR}▸ $1 ($2)${RESET_COLOR}`);
|
|
249
|
+
process.stdout.write(colored + "\n");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
groupWarnings() {
|
|
253
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
254
|
+
for (const warning of this.warnings) {
|
|
255
|
+
const enrichedWarning = {
|
|
256
|
+
...warning,
|
|
257
|
+
snippet: warning.loc ? this.getSnippet(warning.filePath, warning.loc) : void 0
|
|
258
|
+
};
|
|
259
|
+
const existing = groupMap.get(warning.type);
|
|
260
|
+
if (existing) existing.warnings.push(enrichedWarning);
|
|
261
|
+
else groupMap.set(warning.type, {
|
|
262
|
+
message: warning.type,
|
|
263
|
+
warnings: [enrichedWarning]
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return Array.from(groupMap.values()).sort((a, b) => b.warnings.length - a.warnings.length);
|
|
267
|
+
}
|
|
268
|
+
getSnippet(filePath, loc) {
|
|
269
|
+
if (!loc) return;
|
|
270
|
+
const lines = this.getFileLines(filePath);
|
|
271
|
+
if (!lines) return;
|
|
272
|
+
const lineIndex = loc.line - 1;
|
|
273
|
+
if (lineIndex < 0 || lineIndex >= lines.length) return;
|
|
274
|
+
const snippetLines = [];
|
|
275
|
+
const startLine = Math.max(0, lineIndex - 2);
|
|
276
|
+
const endLine = Math.min(lines.length - 1, lineIndex + 4);
|
|
277
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
278
|
+
const lineNum = String(i + 1).padStart(4, " ");
|
|
279
|
+
const marker = i === lineIndex ? ">" : " ";
|
|
280
|
+
snippetLines.push(` ${marker} ${lineNum} | ${lines[i]}`);
|
|
281
|
+
}
|
|
282
|
+
return snippetLines.join("\n");
|
|
283
|
+
}
|
|
284
|
+
getFileLines(filePath) {
|
|
285
|
+
if (this.fileCache.has(filePath)) return this.fileCache.get(filePath) ?? null;
|
|
286
|
+
try {
|
|
287
|
+
const lines = readFileSync(filePath, "utf-8").split("\n");
|
|
288
|
+
this.fileCache.set(filePath, lines);
|
|
289
|
+
return lines;
|
|
290
|
+
} catch {
|
|
291
|
+
this.fileCache.set(filePath, null);
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
};
|
|
168
296
|
const WARN_BG_COLOR = "\x1B[43m";
|
|
169
297
|
const WARN_TEXT_COLOR = "\x1B[30m";
|
|
170
298
|
const ERROR_BG_COLOR = "\x1B[41m";
|
|
171
299
|
const ERROR_TEXT_COLOR = "\x1B[37m";
|
|
172
300
|
const INFO_BG_COLOR = "\x1B[44m";
|
|
173
301
|
const INFO_TEXT_COLOR = "\x1B[37m";
|
|
302
|
+
const SECTION_COLOR = "\x1B[36m";
|
|
174
303
|
const RESET_COLOR = "\x1B[0m";
|
|
175
304
|
|
|
176
305
|
//#endregion
|