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 CHANGED
@@ -21,10 +21,7 @@ pnpm add styled-components-to-stylex-codemod
21
21
  Use `runTransform` to transform files matching a glob pattern:
22
22
 
23
23
  ```ts
24
- import {
25
- runTransform,
26
- defineAdapter,
27
- } from "styled-components-to-stylex-codemod";
24
+ import { runTransform, defineAdapter } from "styled-components-to-stylex-codemod";
28
25
 
29
26
  const adapter = defineAdapter({
30
27
  resolveValue(ctx) {
@@ -70,9 +67,7 @@ const adapter = defineAdapter({
70
67
  .replace(/^--/, "")
71
68
  .split("-")
72
69
  .filter(Boolean)
73
- .map((part, i) =>
74
- i === 0 ? part : part[0]?.toUpperCase() + part.slice(1)
75
- )
70
+ .map((part, i) => (i === 0 ? part : part[0]?.toUpperCase() + part.slice(1)))
76
71
  .join("");
77
72
 
78
73
  // If you care about fallbacks, you can use `fallback` here to decide whether to resolve or not.
@@ -97,18 +92,20 @@ const adapter = defineAdapter({
97
92
  // `calleeSource` tells you where it came from:
98
93
  // - { kind: "absolutePath", value: "/abs/path" } for relative imports
99
94
  // - { kind: "specifier", value: "some-package/foo" } for package imports
95
+ //
96
+ // The codemod determines how to use the result based on context:
97
+ // - If `ctx.cssProperty` exists (e.g., `border: ${helper()}`) → result is used as a CSS value
98
+ // - If `ctx.cssProperty` is undefined (e.g., `${helper()}`) → result is used as a StyleX style object
99
+ //
100
+ // Use `ctx.cssProperty` to return the appropriate expression for the context.
100
101
 
101
102
  const arg0 = ctx.args[0];
102
- const key =
103
- arg0?.kind === "literal" && typeof arg0.value === "string"
104
- ? arg0.value
105
- : null;
103
+ const key = arg0?.kind === "literal" && typeof arg0.value === "string" ? arg0.value : null;
106
104
  if (ctx.calleeImportedName !== "transitionSpeed" || !key) {
107
105
  return null;
108
106
  }
109
107
 
110
108
  return {
111
- usage: "create",
112
109
  expr: `transitionSpeedVars.${key}`,
113
110
  imports: [
114
111
  {
@@ -119,9 +116,11 @@ const adapter = defineAdapter({
119
116
  };
120
117
  },
121
118
 
122
- shouldSupportExternalStyling() {
123
- return false;
119
+ externalInterface() {
120
+ return null;
124
121
  },
122
+
123
+ styleMerger: null,
125
124
  });
126
125
 
127
126
  const result = await runTransform({
@@ -139,10 +138,10 @@ console.log(result);
139
138
 
140
139
  Adapters are the main extension point. They let you control:
141
140
 
142
- - how theme paths and CSS variables are turned into StyleX-compatible JS values (`resolveValue`)
141
+ - how theme paths, CSS variables, and imported values are turned into StyleX-compatible JS values (`resolveValue`)
143
142
  - what extra imports to inject into transformed files (returned from `resolveValue`)
144
- - how helper calls are resolved (via `resolveCall({ ... })` returning `usage: "props" | "create"`; `null`/`undefined` now bails)
145
- - which exported components should support external className/style extension (`shouldSupportExternalStyling`)
143
+ - how helper calls are resolved (via `resolveCall({ ... })` returning `{ expr, imports }`; `null`/`undefined` bails the file)
144
+ - which exported components should support external className/style extension and/or polymorphic `as` prop (`externalInterface`)
146
145
  - how className/style merging is handled for components accepting external styling (`styleMerger`)
147
146
 
148
147
  #### Style Merger
@@ -160,8 +159,15 @@ const adapter = defineAdapter({
160
159
  return null;
161
160
  },
162
161
 
163
- shouldSupportExternalStyling(ctx) {
164
- return ctx.filePath.includes("/shared/components/");
162
+ resolveCall() {
163
+ return null;
164
+ },
165
+
166
+ externalInterface(ctx) {
167
+ if (ctx.filePath.includes("/shared/components/")) {
168
+ return { styles: true };
169
+ }
170
+ return null;
165
171
  },
166
172
 
167
173
  // Use a custom merger function for cleaner output
@@ -178,15 +184,15 @@ The merger function should have this signature:
178
184
  function mergedSx(
179
185
  styles: StyleXStyles,
180
186
  className?: string,
181
- style?: React.CSSProperties
187
+ style?: React.CSSProperties,
182
188
  ): ReturnType<typeof stylex.props>;
183
189
  ```
184
190
 
185
191
  See [`test-cases/lib/mergedSx.ts`](./test-cases/lib/mergedSx.ts) for a reference implementation.
186
192
 
187
- #### External Styles Support
193
+ #### External Interface (Styles and Polymorphic `as` Support)
188
194
 
189
- Transformed components are "closed" by default — they don't accept external `className` or `style` props. Use `shouldSupportExternalStyling` to control which exported components should support external styling:
195
+ Transformed components are "closed" by default — they don't accept external `className` or `style` props, and exported components only get `as` support when it is used inside the file. Use `externalInterface` to control which exported components should support these features:
190
196
 
191
197
  ```ts
192
198
  const adapter = defineAdapter({
@@ -195,39 +201,59 @@ const adapter = defineAdapter({
195
201
  return null;
196
202
  },
197
203
 
198
- shouldSupportExternalStyling(ctx) {
204
+ resolveCall() {
205
+ return null;
206
+ },
207
+
208
+ externalInterface(ctx) {
199
209
  // ctx: { filePath, componentName, exportName, isDefaultExport }
200
210
 
201
- // Example: Enable for all exports in shared components folder
211
+ // Example: Enable styles (and `as`) for all exports in shared components folder
202
212
  if (ctx.filePath.includes("/shared/components/")) {
203
- return true;
213
+ return { styles: true };
204
214
  }
205
215
 
206
- // Example: Enable for specific component names
207
- if (ctx.componentName === "Button" || ctx.componentName === "Card") {
208
- return true;
216
+ // Example: Enable only `as` prop (no style merging)
217
+ if (ctx.componentName === "Typography") {
218
+ return { styles: false, as: true };
209
219
  }
210
220
 
211
- return false;
221
+ // Disable both (default)
222
+ return null;
212
223
  },
224
+
225
+ styleMerger: null,
213
226
  });
214
227
  ```
215
228
 
216
- When `shouldSupportExternalStyling` returns `true`, the generated component will:
229
+ The `externalInterface` method returns:
230
+
231
+ - `null` — no external interface (neither className/style nor `as` prop)
232
+ - `{ styles: true }` — accept className/style props AND polymorphic `as` prop
233
+ - `{ styles: false, as: true }` — accept only polymorphic `as` prop (no style merging)
234
+ - `{ styles: false, as: false }` — equivalent to `null`
235
+
236
+ When `styles: true`, the generated component will:
217
237
 
218
238
  - Accept `className` and `style` props
219
239
  - Merge them with the StyleX-generated styles
220
240
  - Forward remaining props via `...rest`
241
+ - Accept polymorphic `as` prop (required for style merging to work correctly)
242
+
243
+ When `{ styles: false, as: true }`, the generated component will accept a polymorphic `as` prop but won't include className/style merging.
221
244
 
222
245
  #### Dynamic interpolations
223
246
 
224
247
  When the codemod encounters an interpolation inside a styled template literal, it runs an internal dynamic resolution pipeline which covers common cases like:
225
248
 
226
249
  - theme access (`props.theme...`) via `resolveValue({ kind: "theme", path })`
250
+ - imported value access (`import { zIndex } ...; ${zIndex.popover}`) via `resolveValue({ kind: "importedValue", importedName, source, path })`
227
251
  - prop access (`props.foo`) and conditionals (`props.foo ? "a" : "b"`, `props.foo && "color: red;"`)
228
- - simple helper calls (`transitionSpeed("slowTransition")`) via `resolveCall({ ... })` returning `usage: "create"`
229
- - style helper calls (returning StyleX styles) via `resolveCall({ ... })` returning `usage: "props"`; these are emitted as extra `stylex.props(...)` args
230
- - if `resolveCall` returns `null` or `undefined`, the transform now **bails the file** and logs a warning
252
+ - helper calls (`transitionSpeed("slowTransition")`) via `resolveCall({ ... })` the codemod infers usage from context:
253
+ - With `ctx.cssProperty` (e.g., `color: ${helper()}`) result used as CSS value in `stylex.create()`
254
+ - Without `ctx.cssProperty` (e.g., `${helper()}`) result used as StyleX styles in `stylex.props()`
255
+ - Use the optional `usage: "create" | "props"` field to override the default inference
256
+ - if `resolveCall` returns `null` or `undefined`, the transform **bails the file** and logs a warning
231
257
  - helper calls applied to prop values (e.g. `shadow(props.shadow)`) by emitting a StyleX style function that calls the helper at runtime
232
258
  - conditional CSS blocks via ternary (e.g. `props.$dim ? "opacity: 0.5;" : ""`)
233
259
 
@@ -239,8 +265,8 @@ If the pipeline can’t resolve an interpolation:
239
265
  ### Limitations
240
266
 
241
267
  - **Flow** type generation is non-existing, works best with TypeScript or plain JS right now. Contributions more than welcome!
242
- - **ThemeProvider**: if a file imports and uses `ThemeProvider` from `styled-components`, the transform **skips the entire file** (theming strategy is project-specific).
243
268
  - **createGlobalStyle**: detected usage is reported as an **unsupported-feature** warning (StyleX does not support global styles in the same way).
269
+ - **Theme prop overrides**: passing a `theme` prop directly to styled components (e.g. `<Button theme={...} />`) is not supported and will bail with a warning.
244
270
 
245
271
  ## License
246
272
 
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as defineAdapter, r as Adapter, t as CollectedWarning } from "./logger-CNmtK-uJ.mjs";
1
+ import { i as defineAdapter, r as Adapter, t as CollectedWarning } from "./logger-DC-1uogs.mjs";
2
2
 
3
3
  //#region src/run.d.ts
4
4
  interface RunTransformOptions {
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as assertValidAdapter, r as describeValue, t as Logger } from "./logger-DKelw2HS.mjs";
1
+ import { n as assertValidAdapter, r as describeValue, t as Logger } from "./logger-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 - Single user entry point for customizing the codemod.
11
+ * Adapter entry point for customizing the codemod.
12
+ * Core concepts: value resolution hooks and adapter validation.
12
13
  */
13
14
  /**
14
15
  * Helper for nicer user authoring + type inference.
@@ -27,23 +28,33 @@ import { spawn } from "node:child_process";
27
28
  * ],
28
29
  * };
29
30
  * }
30
- * return null;
31
+ * // Return undefined to bail/skip the file
31
32
  * },
32
33
  *
33
34
  * resolveCall(ctx) {
34
35
  * // Resolve helper calls inside template interpolations.
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
- * // - { usage: "props", expr, imports } for StyleX styles (usable in stylex.props)
37
- * // - { usage: "create", expr, imports } for a single value (usable in stylex.create)
38
- * // - null to leave the call unresolved
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
- * // Enable className/style/rest support for exported components
44
- * shouldSupportExternalStyling(ctx) {
45
- * // Example: Enable for all exported components in a shared components folder
46
- * return ctx.filePath.includes("/shared/components/");
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, void 0, ctx);
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, void 0, ctx);
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
- shouldSupportExternalStyling(ctx) {
133
- return adapter.shouldSupportExternalStyling(ctx);
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$1, reject) => {
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$1();
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: Logger.flushWarnings()
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 shouldSupportExternalStyling = obj?.shouldSupportExternalStyling;
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.shouldSupportExternalStyling(context) is required",
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 { usage: \"props\" | \"create\", expr, imports } | null }",
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 { usage: \"props\" | \"create\", expr: string, imports: ImportSpec[] } | null }",
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 shouldSupportExternalStyling !== "function") throw new Error([`${where}: adapter.shouldSupportExternalStyling must be a function.`, `Received: shouldSupportExternalStyling=${describeValue(shouldSupportExternalStyling)}`].join("\n"));
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 stderr.
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 stderr with file path and optional location.
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 stderr and collect them.
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.message}`, warning.context);
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.stderr.write(`${trimmed}${serialized ? `\n${serialized}` : ""}\n\n`);
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.stderr.isTTY) return label;
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.stderr.isTTY) return label;
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.stderr.isTTY) return label;
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
- let collected = [];
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