styled-components-to-stylex-codemod 0.0.14 → 0.0.15

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
@@ -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, you can use `createExternalInterface` to auto-detect usage by scanning your consumer code with [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`).
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 `rg` installed and available in `$PATH`. Not supported on Windows.
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 { defineAdapter, createExternalInterface } from "styled-components-to-stylex-codemod";
167
-
168
- const externalInterface = createExternalInterface({ searchDirs: ["src/"] });
186
+ import { runTransform, defineAdapter } from "styled-components-to-stylex-codemod";
169
187
 
170
- export default defineAdapter({
188
+ const adapter = defineAdapter({
171
189
  // ...
172
- externalInterface: externalInterface.get,
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
- This scans the given directories 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.
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 cant resolve an interpolation:
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 { i as defineAdapter, t as Adapter } from "./adapter-DilRjT4L.mjs";
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: 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 15
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 { createExternalInterface, defineAdapter, runTransform };
105
+ export { type AdapterInput, defineAdapter, runTransform };
package/dist/index.mjs CHANGED
@@ -1,10 +1,9 @@
1
- import { n as assertValidAdapter, r as describeValue, t as Logger } from "./logger-D3j-qxgZ.mjs";
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
- assertValidAdapter(adapter, "defineAdapter(adapter)");
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
- const { files, dryRun = false, print = false, parser = "tsx", formatterCommands, maxExamples } = options;
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 adapter = options.adapter;
128
- assertValidAdapter(adapter, "runTransform(options)");
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 adapter.resolveValue(ctx);
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 adapter.resolveCall(ctx);
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 adapter.resolveSelector(ctx);
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 result = await run((() => {
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-BcidTT9f.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
- })(), filePaths, {
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 (formatterCommands && formatterCommands.length > 0 && result.ok > 0 && !dryRun) for (const formatterCommand of formatterCommands) {
201
- const [cmd, ...cmdArgs] = formatterCommand.split(/\s+/);
202
- if (cmd) try {
203
- await new Promise((resolve, reject) => {
204
- const proc = spawn(cmd, [...cmdArgs, ...filePaths], {
205
- stdio: "inherit",
206
- shell: true
207
- });
208
- proc.on("close", (code) => {
209
- if (code === 0) resolve();
210
- else reject(/* @__PURE__ */ new Error(`Formatter command exited with code ${code}`));
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 { createExternalInterface, defineAdapter, runTransform };
369
+ export { defineAdapter, runTransform };