styled-components-to-stylex-codemod 0.0.13 → 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.
@@ -0,0 +1,885 @@
1
+ import { t as PLACEHOLDER_RE } from "./styled-css-DBryFqQM.mjs";
2
+ import { t as isSelectorContext } from "./selector-context-heuristic-CGwiJ3HL.mjs";
3
+ import path, { relative, resolve } from "node:path";
4
+ import { readFileSync, realpathSync } from "node:fs";
5
+ import { execSync } from "node:child_process";
6
+ import { createHash } from "node:crypto";
7
+ import { parse } from "@babel/parser";
8
+
9
+ //#region src/internal/utilities/collection-utils.ts
10
+ /** Add a value to a Set stored in a Map, creating the Set if it doesn't exist. */
11
+ function addToSetMap(map, key, value) {
12
+ let set = map.get(key);
13
+ if (!set) {
14
+ set = /* @__PURE__ */ new Set();
15
+ map.set(key, set);
16
+ }
17
+ set.add(value);
18
+ }
19
+
20
+ //#endregion
21
+ //#region src/internal/prepass/extract-external-interface.ts
22
+ function findImportSource(src, localName) {
23
+ const [aliasRe, namedRe, defaultRe] = getImportSourceRes(localName);
24
+ const aliasMatch = src.match(aliasRe);
25
+ if (aliasMatch?.[1] && aliasMatch[1] !== "default" && aliasMatch[2]) return {
26
+ source: aliasMatch[2],
27
+ exportedName: aliasMatch[1]
28
+ };
29
+ const namedMatch = src.match(namedRe);
30
+ if (namedMatch?.[1]) return {
31
+ source: namedMatch[1],
32
+ exportedName: localName
33
+ };
34
+ const defaultMatch = src.match(defaultRe);
35
+ if (defaultMatch?.[1]) return {
36
+ source: defaultMatch[1],
37
+ exportedName: localName
38
+ };
39
+ return null;
40
+ }
41
+ const importSourceReCache = /* @__PURE__ */ new Map();
42
+ function getImportSourceRes(localName) {
43
+ let cached = importSourceReCache.get(localName);
44
+ if (!cached) {
45
+ cached = [
46
+ new RegExp(String.raw`import\s+\{[^}]*\b(\w+)\s+as\s+${localName}\b[^}]*\}\s+from\s+["']([^"']+)["']`),
47
+ new RegExp(String.raw`import\s+\{[^}]*\b${localName}\b[^}]*\}\s+from\s+["']([^"']+)["']`),
48
+ new RegExp(String.raw`import\s+${localName}(?:\s*,\s*\{[^}]*\})?\s+from\s+["']([^"']+)["']`)
49
+ ];
50
+ importSourceReCache.set(localName, cached);
51
+ }
52
+ return cached;
53
+ }
54
+ function resolveBarrelReExport(filePath, name, resolve, read) {
55
+ const basename = path.basename(filePath);
56
+ if (basename !== "index.ts" && basename !== "index.tsx") return null;
57
+ let src;
58
+ try {
59
+ src = read(filePath);
60
+ } catch {
61
+ return null;
62
+ }
63
+ const namedMatch = src.match(getBarrelExportRe(name));
64
+ if (namedMatch?.[1]) return resolve(namedMatch[1], filePath);
65
+ for (const match of src.matchAll(/export\s*\*\s*from\s*["']([^"']+)["']/g)) {
66
+ const specifier = match[1];
67
+ if (!specifier) continue;
68
+ const resolved = resolve(specifier, filePath);
69
+ if (resolved) try {
70
+ if (fileExports(read(resolved), name)) return resolved;
71
+ } catch {}
72
+ }
73
+ return null;
74
+ }
75
+ function fileExports(src, name) {
76
+ return getFileExportsRe(name).test(src);
77
+ }
78
+ const fileExportsReCache = /* @__PURE__ */ new Map();
79
+ function getFileExportsRe(name) {
80
+ let re = fileExportsReCache.get(name);
81
+ if (!re) {
82
+ re = new RegExp(String.raw`export\s+(?:(?:const|function|class|let|var)\s+${name}\b|default\s+${name}\b)` + String.raw`|export\s*\{[^}]*\b${name}\b[^}]*\}`);
83
+ fileExportsReCache.set(name, re);
84
+ }
85
+ return re;
86
+ }
87
+ const barrelExportReCache = /* @__PURE__ */ new Map();
88
+ function getBarrelExportRe(name) {
89
+ let re = barrelExportReCache.get(name);
90
+ if (!re) {
91
+ re = new RegExp(String.raw`export\s*\{[^}]*\b${name}\b[^}]*\}\s*from\s*["']([^"']+)["']`);
92
+ barrelExportReCache.set(name, re);
93
+ }
94
+ return re;
95
+ }
96
+ function fileImportsFrom(usageSrc, usageFile, name, defFile, resolve) {
97
+ const [namedRe, defaultRe] = getFileImportsFromRes(name);
98
+ namedRe.lastIndex = 0;
99
+ defaultRe.lastIndex = 0;
100
+ const stem = path.parse(defFile).name;
101
+ const parent = path.basename(path.dirname(defFile));
102
+ for (const re of [namedRe, defaultRe]) for (const match of usageSrc.matchAll(re)) {
103
+ const specifier = match[1];
104
+ if (!specifier) continue;
105
+ const resolved = resolve(specifier, usageFile);
106
+ if (resolved && path.resolve(resolved) === path.resolve(defFile)) return true;
107
+ if (specifier.endsWith(stem) || specifier.endsWith(`${parent}/${stem}`) || specifier.endsWith(parent)) return true;
108
+ }
109
+ return false;
110
+ }
111
+ const fileImportsFromReCache = /* @__PURE__ */ new Map();
112
+ function getFileImportsFromRes(name) {
113
+ let cached = fileImportsFromReCache.get(name);
114
+ if (!cached) {
115
+ cached = [new RegExp(String.raw`import\s+\{[^}]*\b${name}\b[^}]*\}\s+from\s+["']([^"']+)["']`, "g"), new RegExp(String.raw`import\s+${name}(?:\s*,\s*\{[^}]*\})?\s+from\s+["']([^"']+)["']`, "g")];
116
+ fileImportsFromReCache.set(name, cached);
117
+ }
118
+ return cached;
119
+ }
120
+
121
+ //#endregion
122
+ //#region src/internal/prepass/prepass-parser.ts
123
+ /**
124
+ * Shared babel parser for prepass modules.
125
+ *
126
+ * Uses @babel/parser directly with `tokens: false` for ~35% faster parsing
127
+ * compared to jscodeshift's getParser (which enables token generation).
128
+ *
129
+ * Both scan-cross-file-selectors and extract-external-interface can share this parser
130
+ * to avoid duplicate parser initialization.
131
+ */
132
+ /**
133
+ * Create a babel parser with tokens disabled, matching jscodeshift's plugin set
134
+ * for the given parser name.
135
+ *
136
+ * - `tsx` / `ts`: TypeScript plugins (tsx also includes JSX)
137
+ * - `babel` / `babylon` / `flow`: Flow plugins + JSX
138
+ *
139
+ * Note: jscodeshift's `flow` parser uses the `flow-parser` package (not babel).
140
+ * We always use `@babel/parser` with the `flow` plugin instead, since it produces
141
+ * the same AST node types (ImportDeclaration, TaggedTemplateExpression) that the
142
+ * prepass walks, and avoids an extra parser dependency.
143
+ */
144
+ function createPrepassParser(parserName = "tsx") {
145
+ const options = parserName === "ts" || parserName === "tsx" ? buildOptions(TS_PLUGINS, parserName === "tsx") : buildOptions(FLOW_PLUGINS, true);
146
+ return { parse(source) {
147
+ return parse(source, options);
148
+ } };
149
+ }
150
+ function buildOptions(plugins, includeJsx) {
151
+ return {
152
+ sourceType: "module",
153
+ allowImportExportEverywhere: true,
154
+ allowReturnOutsideFunction: true,
155
+ startLine: 1,
156
+ tokens: false,
157
+ plugins: includeJsx ? ["jsx", ...plugins] : plugins
158
+ };
159
+ }
160
+ /**
161
+ * Plugins for TypeScript parsers (ts, tsx).
162
+ * Same as jscodeshift's tsOptions.plugins minus "jsx" (added conditionally).
163
+ */
164
+ const TS_PLUGINS = [
165
+ "asyncGenerators",
166
+ "decoratorAutoAccessors",
167
+ "bigInt",
168
+ "classPrivateMethods",
169
+ "classPrivateProperties",
170
+ "classProperties",
171
+ "decorators-legacy",
172
+ "doExpressions",
173
+ "dynamicImport",
174
+ "exportDefaultFrom",
175
+ "exportNamespaceFrom",
176
+ "functionBind",
177
+ "functionSent",
178
+ "importAttributes",
179
+ "importMeta",
180
+ "nullishCoalescingOperator",
181
+ "numericSeparator",
182
+ "objectRestSpread",
183
+ "optionalCatchBinding",
184
+ "optionalChaining",
185
+ ["pipelineOperator", { proposal: "minimal" }],
186
+ "throwExpressions",
187
+ "typescript"
188
+ ];
189
+ /**
190
+ * Plugins for Flow/Babylon parsers (babel, babylon, flow).
191
+ * Same as jscodeshift's babylon parser plugins minus "jsx" (added conditionally).
192
+ */
193
+ const FLOW_PLUGINS = [
194
+ ["flow", { all: true }],
195
+ "flowComments",
196
+ "asyncGenerators",
197
+ "bigInt",
198
+ "classProperties",
199
+ "classPrivateProperties",
200
+ "classPrivateMethods",
201
+ ["decorators", { decoratorsBeforeExport: false }],
202
+ "doExpressions",
203
+ "dynamicImport",
204
+ "exportDefaultFrom",
205
+ "exportNamespaceFrom",
206
+ "functionBind",
207
+ "functionSent",
208
+ "importMeta",
209
+ "logicalAssignment",
210
+ "nullishCoalescingOperator",
211
+ "numericSeparator",
212
+ "objectRestSpread",
213
+ "optionalCatchBinding",
214
+ "optionalChaining",
215
+ ["pipelineOperator", { proposal: "minimal" }],
216
+ "throwExpressions"
217
+ ];
218
+
219
+ //#endregion
220
+ //#region src/internal/prepass/scan-cross-file-selectors.ts
221
+ /**
222
+ * Pre-filter: matches any bare `${Identifier}` template expression.
223
+ * Used to skip files that only contain arrow functions or member expressions
224
+ * in template literals (e.g. `${props => ...}`, `${theme.color}`).
225
+ */
226
+ const BARE_TEMPLATE_IDENTIFIER_RE = /\$\{\s*[a-zA-Z_$][\w$]*\s*\}/;
227
+ /**
228
+ * Categorize cross-file selector usages into marker sidecar and global selector bridge maps.
229
+ *
230
+ * Bridge usages (from already-converted files) are skipped — the consumer handles marker
231
+ * generation via the forward selector handler, so no sidecar/bridge is needed on the target.
232
+ */
233
+ function categorizeSelectorUsages(usages, componentsNeedingMarkerSidecar, componentsNeedingGlobalSelectorBridge) {
234
+ for (const usage of usages) {
235
+ if (usage.bridgeComponentName) continue;
236
+ if (usage.consumerIsTransformed) addToSetMap(componentsNeedingMarkerSidecar, usage.resolvedPath, usage.importedName);
237
+ else addToSetMap(componentsNeedingGlobalSelectorBridge, usage.resolvedPath, usage.importedName);
238
+ }
239
+ }
240
+ /** Regex matching `export const XGlobalSelector = ".sc2sx-` pattern (global for matchAll). */
241
+ const BRIDGE_EXPORT_RE = /export\s+const\s+(\w+GlobalSelector)\s*=\s*["']\.sc2sx-/g;
242
+ /**
243
+ * Detect whether an imported name is a bridge GlobalSelector from an
244
+ * already-converted StyleX file.
245
+ *
246
+ * Detection criteria (hybrid fast + safe):
247
+ * 1. Variable name ends with "GlobalSelector" AND the stripped name starts uppercase
248
+ * 2. Target file contains "@stylexjs/stylex" (string check, no parse)
249
+ * 3. Target file has a matching `export const XGlobalSelector = ".sc2sx-"` pattern
250
+ *
251
+ * @returns The stripped component name (e.g., "CollapseArrowIcon" for
252
+ * "CollapseArrowIconGlobalSelector"), or null if not a bridge.
253
+ */
254
+ function detectBridgeGlobalSelector(importedName, resolvedPath, readFile) {
255
+ if (!importedName.endsWith("GlobalSelector")) return null;
256
+ const stripped = importedName.slice(0, -14);
257
+ if (!stripped || !/^[A-Z]/.test(stripped)) return null;
258
+ const content = readFile(resolvedPath);
259
+ if (!content || !content.includes("@stylexjs/stylex")) return null;
260
+ let found = false;
261
+ for (const m of content.matchAll(BRIDGE_EXPORT_RE)) if (m[1] === importedName) {
262
+ found = true;
263
+ break;
264
+ }
265
+ if (!found) return null;
266
+ return stripped;
267
+ }
268
+ /**
269
+ * If `importedName` is a bridge GlobalSelector, populate bridge fields on `usage`
270
+ * and find the corresponding component import from the same source.
271
+ */
272
+ function applyBridgeFields(usage, importedName, localName, resolvedPath, importMap, readFile) {
273
+ const bridgeName = detectBridgeGlobalSelector(importedName, resolvedPath, readFile);
274
+ if (!bridgeName) return;
275
+ usage.bridgeComponentName = bridgeName;
276
+ const imp = importMap.get(localName);
277
+ if (!imp) return;
278
+ let defaultImportLocal;
279
+ for (const [otherLocal, otherImp] of importMap) {
280
+ if (otherImp.source !== imp.source || otherLocal === localName) continue;
281
+ if (otherImp.importedName === bridgeName) {
282
+ usage.bridgeComponentLocalName = otherLocal;
283
+ defaultImportLocal = void 0;
284
+ break;
285
+ }
286
+ if (otherImp.importedName === "default" && defaultImportLocal === void 0) defaultImportLocal = otherLocal;
287
+ }
288
+ if (defaultImportLocal !== void 0) usage.bridgeComponentLocalName = defaultImportLocal;
289
+ }
290
+ /** Global version for matchAll/replace operations */
291
+ const PLACEHOLDER_RE_G = new RegExp(PLACEHOLDER_RE.source, "g");
292
+ /**
293
+ * Walk the AST collecting ImportDeclaration and TaggedTemplateExpression nodes.
294
+ *
295
+ * Uses a targeted recursive walk — only descends into node types that can
296
+ * contain these targets (skips type annotations, comments, etc.).
297
+ */
298
+ function walkForImportsAndTemplates(node, imports, templates) {
299
+ if (!node || typeof node !== "object") return;
300
+ const n = node;
301
+ if (n.type === "ImportDeclaration") {
302
+ imports.push(n);
303
+ return;
304
+ }
305
+ if (n.type === "TaggedTemplateExpression") {
306
+ templates.push(n);
307
+ return;
308
+ }
309
+ for (const key of Object.keys(n)) {
310
+ if (key === "type" || key === "start" || key === "end" || key === "loc") continue;
311
+ const val = n[key];
312
+ if (Array.isArray(val)) for (const child of val) walkForImportsAndTemplates(child, imports, templates);
313
+ else if (val && typeof val === "object" && val.type) walkForImportsAndTemplates(val, imports, templates);
314
+ }
315
+ }
316
+ /** Build a map of localName → import info from raw ImportDeclaration nodes. */
317
+ function buildImportMapFromNodes(importNodes) {
318
+ const map = /* @__PURE__ */ new Map();
319
+ for (const node of importNodes) {
320
+ const sourceValue = node.source?.value;
321
+ if (typeof sourceValue !== "string") continue;
322
+ const specifiers = node.specifiers;
323
+ if (!specifiers) continue;
324
+ for (const spec of specifiers) {
325
+ const localName = getNodeName(spec.local);
326
+ if (!localName) continue;
327
+ if (spec.type === "ImportDefaultSpecifier") map.set(localName, {
328
+ source: sourceValue,
329
+ importedName: "default"
330
+ });
331
+ else if (spec.type === "ImportSpecifier") {
332
+ const importedName = getNodeName(spec.imported) ?? localName;
333
+ map.set(localName, {
334
+ source: sourceValue,
335
+ importedName
336
+ });
337
+ }
338
+ }
339
+ }
340
+ return map;
341
+ }
342
+ /** Find the local name for the styled-components default import. */
343
+ function findStyledImportNameFromNodes(importNodes) {
344
+ for (const node of importNodes) {
345
+ if (node.source?.value !== "styled-components") continue;
346
+ const specifiers = node.specifiers;
347
+ if (!specifiers) continue;
348
+ for (const spec of specifiers) if (spec.type === "ImportDefaultSpecifier") {
349
+ const name = getNodeName(spec.local);
350
+ if (name) return name;
351
+ }
352
+ }
353
+ }
354
+ /**
355
+ * Find local names of imported components used as selectors inside
356
+ * styled-components template literals.
357
+ */
358
+ function findComponentSelectorLocalsFromNodes(templateNodes, styledImportName) {
359
+ const selectorLocals = /* @__PURE__ */ new Set();
360
+ for (const node of templateNodes) {
361
+ if (!isStyledTag(node.tag, styledImportName)) continue;
362
+ const quasi = node.quasi;
363
+ if (!quasi) continue;
364
+ const quasis = quasi.quasis;
365
+ const expressions = quasi.expressions;
366
+ if (!quasis || !expressions) continue;
367
+ const rawParts = [];
368
+ for (let i = 0; i < quasis.length; i++) {
369
+ const value = quasis[i]?.value;
370
+ rawParts.push(value?.raw ?? "");
371
+ if (i < expressions.length) rawParts.push(`__SC_EXPR_${i}__`);
372
+ }
373
+ const rawCss = rawParts.join("");
374
+ for (const match of rawCss.matchAll(PLACEHOLDER_RE_G)) {
375
+ const exprIndex = Number(match[1]);
376
+ const pos = match.index;
377
+ if (isPlaceholderInSelectorContext(rawCss, pos, match[0].length)) {
378
+ const expr = expressions[exprIndex];
379
+ if (expr?.type === "Identifier" && typeof expr.name === "string") selectorLocals.add(expr.name);
380
+ }
381
+ }
382
+ }
383
+ return selectorLocals;
384
+ }
385
+ /**
386
+ * Check whether a styled-components tag expression is a styled call.
387
+ * Matches: styled.div, styled(X), styled.div.attrs(...), styled(X).withConfig(...), etc.
388
+ */
389
+ function isStyledTag(tag, styledName) {
390
+ if (!tag || typeof tag !== "object") return false;
391
+ if (tag.type === "MemberExpression") {
392
+ const obj = tag.object;
393
+ if (obj?.type === "Identifier" && obj.name === styledName) return true;
394
+ }
395
+ if (tag.type === "CallExpression") {
396
+ const callee = tag.callee;
397
+ if (callee?.type === "Identifier" && callee.name === styledName) return true;
398
+ if (callee?.type === "MemberExpression" && callee.object) return isStyledTag(callee.object, styledName);
399
+ }
400
+ return false;
401
+ }
402
+ /** Check if a placeholder at the given position is in a CSS selector context. */
403
+ function isPlaceholderInSelectorContext(rawCss, pos, length) {
404
+ const after = rawCss.slice(pos + length).trimStart();
405
+ return isSelectorContext(rawCss.slice(0, pos).trimEnd().replace(PLACEHOLDER_RE_G, "hover"), after);
406
+ }
407
+ /** Safely extract the name string from an AST identifier-like node. */
408
+ function getNodeName(node) {
409
+ if (!node || typeof node !== "object") return;
410
+ if (node.type === "Identifier" && typeof node.name === "string") return node.name;
411
+ }
412
+ /** Deduplicate and resolve two file lists into a single array of absolute paths. */
413
+ function deduplicateAndResolve(filesToTransform, consumerPaths) {
414
+ const seen = /* @__PURE__ */ new Set();
415
+ const result = [];
416
+ for (const f of filesToTransform) {
417
+ const abs = resolve(f);
418
+ if (!seen.has(abs)) {
419
+ seen.add(abs);
420
+ result.push(abs);
421
+ }
422
+ }
423
+ for (const f of consumerPaths) {
424
+ const abs = resolve(f);
425
+ if (!seen.has(abs)) {
426
+ seen.add(abs);
427
+ result.push(abs);
428
+ }
429
+ }
430
+ return result;
431
+ }
432
+
433
+ //#endregion
434
+ //#region src/internal/prepass/run-prepass.ts
435
+ /**
436
+ * Unified prepass: single pass for both cross-file selector scanning
437
+ * and consumer analysis (external interface detection).
438
+ *
439
+ * Reads each file once, classifies by content (styled-components / as-prop),
440
+ * and runs AST parsing + consumer analysis only on relevant files.
441
+ */
442
+ const AS_PROP_RE = /\bas[={]/;
443
+ const STYLED_CALL_RE = /styled\(([A-Z][A-Za-z0-9]+)/g;
444
+ const STYLED_DEF_RE = /const\s+([A-Z][A-Za-z0-9]*)\b[^=]*=\s*styled[.(]/g;
445
+ /** Matches <Component ...as= across lines. [^<>]* avoids crossing tag boundaries. */
446
+ const JSX_AS_COMPONENT_RE = /<([A-Z][A-Za-z0-9]*)\b[^<>]*\bas[={]/g;
447
+ async function runPrepass(options) {
448
+ const { filesToTransform, consumerPaths, resolver, parserName, createExternalInterface, enableAstCache } = options;
449
+ const t0 = performance.now();
450
+ const astCache = enableAstCache ? /* @__PURE__ */ new Map() : void 0;
451
+ const needsRealpath = (() => {
452
+ if (filesToTransform.length === 0) return false;
453
+ const sample = resolve(filesToTransform[0]);
454
+ try {
455
+ return realpathSync(sample) !== sample;
456
+ } catch {
457
+ return false;
458
+ }
459
+ })();
460
+ const realPathCache = /* @__PURE__ */ new Map();
461
+ const toRealPath = needsRealpath ? (p) => {
462
+ const abs = resolve(p);
463
+ let real = realPathCache.get(abs);
464
+ if (real === void 0) {
465
+ try {
466
+ real = realpathSync(abs);
467
+ } catch {
468
+ real = abs;
469
+ }
470
+ realPathCache.set(abs, real);
471
+ }
472
+ return real;
473
+ } : (p) => resolve(p);
474
+ const transformSet = new Set(filesToTransform.map(toRealPath));
475
+ const allFiles = deduplicateAndResolve(filesToTransform, consumerPaths).map(toRealPath);
476
+ const allFilesSet = new Set(allFiles);
477
+ const uniqueAllFiles = [...allFilesSet];
478
+ const parser = createPrepassParser(parserName);
479
+ const resolveCache = /* @__PURE__ */ new Map();
480
+ const resolve$1 = (specifier, fromFile) => {
481
+ const absFromFile = resolve(fromFile);
482
+ const key = `${absFromFile}\0${specifier}`;
483
+ const cached = resolveCache.get(key);
484
+ if (cached !== void 0) return cached;
485
+ const result = resolver.resolve(absFromFile, specifier);
486
+ const normalized = result ? toRealPath(result) : null;
487
+ resolveCache.set(key, normalized);
488
+ return normalized;
489
+ };
490
+ const selectorUsages = /* @__PURE__ */ new Map();
491
+ const componentsNeedingMarkerSidecar = /* @__PURE__ */ new Map();
492
+ const componentsNeedingGlobalSelectorBridge = /* @__PURE__ */ new Map();
493
+ const asUsages = /* @__PURE__ */ new Map();
494
+ const styledCallUsages = [];
495
+ const styledDefFiles = /* @__PURE__ */ new Map();
496
+ const classNameStyleUsages = /* @__PURE__ */ new Map();
497
+ const fileContents = /* @__PURE__ */ new Map();
498
+ const cachedRead = (filePath) => {
499
+ const content = fileContents.get(filePath);
500
+ if (content !== void 0) return content;
501
+ try {
502
+ const src = readFileSync(filePath, "utf-8");
503
+ fileContents.set(filePath, src);
504
+ return src;
505
+ } catch {
506
+ return "";
507
+ }
508
+ };
509
+ const rgFiltered = rgPreFilter(uniqueAllFiles);
510
+ for (const filePath of uniqueAllFiles) {
511
+ if (rgFiltered && !rgFiltered.has(filePath)) continue;
512
+ let source;
513
+ try {
514
+ source = readFileSync(filePath, "utf-8");
515
+ } catch {
516
+ continue;
517
+ }
518
+ const hasStyled = source.includes("styled-components");
519
+ const hasAsProp = createExternalInterface && AS_PROP_RE.test(source);
520
+ if (!hasStyled && !hasAsProp) continue;
521
+ fileContents.set(filePath, source);
522
+ if (hasStyled && BARE_TEMPLATE_IDENTIFIER_RE.test(source) && hasRegexSelectorCandidate(source)) {
523
+ const usages = scanFileForSelectorsAst(filePath, source, transformSet, resolver, parser, toRealPath, cachedRead, astCache, createExternalInterface);
524
+ if (usages.length > 0) {
525
+ selectorUsages.set(filePath, usages);
526
+ categorizeSelectorUsages(usages, componentsNeedingMarkerSidecar, componentsNeedingGlobalSelectorBridge);
527
+ }
528
+ }
529
+ if (createExternalInterface && hasStyled) {
530
+ STYLED_CALL_RE.lastIndex = 0;
531
+ for (const m of source.matchAll(STYLED_CALL_RE)) if (m[1]) styledCallUsages.push({
532
+ file: filePath,
533
+ name: m[1]
534
+ });
535
+ STYLED_DEF_RE.lastIndex = 0;
536
+ for (const m of source.matchAll(STYLED_DEF_RE)) if (m[1]) addToSetMap(styledDefFiles, filePath, m[1]);
537
+ }
538
+ if (hasAsProp) {
539
+ JSX_AS_COMPONENT_RE.lastIndex = 0;
540
+ for (const m of source.matchAll(JSX_AS_COMPONENT_RE)) if (m[1]) addToSetMap(asUsages, m[1], filePath);
541
+ }
542
+ }
543
+ const styledFileCount = fileContents.size;
544
+ if (createExternalInterface && styledDefFiles.size > 0) {
545
+ const allStyledNames = /* @__PURE__ */ new Set();
546
+ for (const names of styledDefFiles.values()) for (const name of names) allStyledNames.add(name);
547
+ if (allStyledNames.size > 0) {
548
+ const rgHits = rgClassNameStyleFilter(uniqueAllFiles);
549
+ for (const [filePath, source] of fileContents) for (const name of scanClassNameStyleUsages(source, allStyledNames)) addToSetMap(classNameStyleUsages, name, filePath);
550
+ const filesToScan = rgHits ? [...rgHits].filter((f) => allFilesSet.has(f) && !fileContents.has(f)) : uniqueAllFiles.filter((f) => !fileContents.has(f));
551
+ for (const filePath of filesToScan) {
552
+ const source = cachedRead(filePath);
553
+ if (!source) continue;
554
+ for (const name of scanClassNameStyleUsages(source, allStyledNames)) addToSetMap(classNameStyleUsages, name, filePath);
555
+ }
556
+ }
557
+ }
558
+ let consumerAnalysis;
559
+ if (createExternalInterface) {
560
+ consumerAnalysis = /* @__PURE__ */ new Map();
561
+ const ensure = (filePath, name) => {
562
+ const key = `${toRealPath(filePath)}:${name}`;
563
+ let entry = consumerAnalysis.get(key);
564
+ if (!entry) {
565
+ entry = {
566
+ styles: false,
567
+ as: false
568
+ };
569
+ consumerAnalysis.set(key, entry);
570
+ }
571
+ return entry;
572
+ };
573
+ if (asUsages.size > 0) for (const [defFile, names] of styledDefFiles) {
574
+ const defSrc = cachedRead(defFile);
575
+ for (const name of names) {
576
+ const usageFiles = asUsages.get(name);
577
+ if (!usageFiles) continue;
578
+ if (!fileExports(defSrc, name)) continue;
579
+ if (usageFiles.has(defFile)) {
580
+ ensure(defFile, name).as = true;
581
+ continue;
582
+ }
583
+ for (const usageFile of usageFiles) if (fileImportsFrom(cachedRead(usageFile), usageFile, name, defFile, resolve$1)) {
584
+ ensure(defFile, name).as = true;
585
+ break;
586
+ }
587
+ }
588
+ }
589
+ if (classNameStyleUsages.size > 0) for (const [defFile, names] of styledDefFiles) {
590
+ const defSrc = cachedRead(defFile);
591
+ for (const name of names) {
592
+ const usageFiles = classNameStyleUsages.get(name);
593
+ if (!usageFiles) continue;
594
+ if (!fileExports(defSrc, name)) continue;
595
+ if (usageFiles.has(defFile)) {
596
+ ensure(defFile, name).styles = true;
597
+ continue;
598
+ }
599
+ for (const usageFile of usageFiles) if (fileImportsFrom(cachedRead(usageFile), usageFile, name, defFile, resolve$1)) {
600
+ ensure(defFile, name).styles = true;
601
+ break;
602
+ }
603
+ }
604
+ }
605
+ {
606
+ const seen = /* @__PURE__ */ new Set();
607
+ for (const { file, name } of styledCallUsages) {
608
+ const importInfo = findImportSource(cachedRead(file), name);
609
+ if (!importInfo) continue;
610
+ const { source: importSource, exportedName } = importInfo;
611
+ let defFile = resolve$1(importSource, file);
612
+ if (!defFile) continue;
613
+ defFile = resolveBarrelReExport(defFile, exportedName, resolve$1, cachedRead) ?? defFile;
614
+ const key = `${defFile}:${exportedName}`;
615
+ if (seen.has(key)) continue;
616
+ seen.add(key);
617
+ try {
618
+ if (!fileExports(cachedRead(defFile), exportedName)) continue;
619
+ } catch {
620
+ continue;
621
+ }
622
+ ensure(defFile, exportedName).styles = true;
623
+ }
624
+ }
625
+ }
626
+ const crossFileInfo = {
627
+ selectorUsages,
628
+ componentsNeedingMarkerSidecar,
629
+ componentsNeedingGlobalSelectorBridge
630
+ };
631
+ {
632
+ const elapsed = ((performance.now() - t0) / 1e3).toFixed(1);
633
+ const reStyled = consumerAnalysis ? [...consumerAnalysis.values()].filter((v) => v.styles).length : 0;
634
+ const asProp = consumerAnalysis ? [...consumerAnalysis.values()].filter((v) => v.as).length : 0;
635
+ process.stdout.write(`Prepass: scanned ${uniqueAllFiles.length} files in ${elapsed}s — ${styledFileCount} with styled-components, ${selectorUsages.size} cross-file selectors, ${reStyled} re-styled, ${asProp} as-prop, ${classNameStyleUsages.size} className/style\n`);
636
+ }
637
+ if (process.env.DEBUG_CODEMOD) logPrepassDebug(uniqueAllFiles, crossFileInfo, consumerAnalysis);
638
+ return {
639
+ crossFileInfo,
640
+ consumerAnalysis
641
+ };
642
+ }
643
+ /** Matches `${Identifier}` in source — used to find potential selector expressions. */
644
+ const SELECTOR_EXPR_RE = /\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g;
645
+ /** Matches static `import ... from "..."` declarations (multi-line safe). */
646
+ const IMPORT_DECLARATION_RE = /import\s+[\s\S]*?\s+from\s+["'][^"']+["']/g;
647
+ const IMPORT_IDENTIFIER_RE_CACHE = /* @__PURE__ */ new Map();
648
+ /**
649
+ * Fast regex pre-filter: checks if the source contains any `${Identifier}`
650
+ * that appears to be in a CSS selector context (before `{`, not after `:`).
651
+ *
652
+ * This reduces the number of files needing AST parsing from ~888 to ~500,
653
+ * saving ~120ms of babel parsing time per run.
654
+ */
655
+ function hasRegexSelectorCandidate(source) {
656
+ const importText = collectImportDeclarationText(source);
657
+ if (importText.length === 0) return false;
658
+ SELECTOR_EXPR_RE.lastIndex = 0;
659
+ for (const m of source.matchAll(SELECTOR_EXPR_RE)) {
660
+ const identifier = m[1];
661
+ if (!identifier || !importTextMentionsIdentifier(importText, identifier)) continue;
662
+ const pos = m.index;
663
+ if (isSelectorContext(source.slice(0, pos).trimEnd(), source.slice(pos + m[0].length).trimStart())) return true;
664
+ }
665
+ return false;
666
+ }
667
+ function collectImportDeclarationText(source) {
668
+ IMPORT_DECLARATION_RE.lastIndex = 0;
669
+ const blocks = [];
670
+ for (const match of source.matchAll(IMPORT_DECLARATION_RE)) if (match[0]) blocks.push(match[0]);
671
+ return blocks.join("\n");
672
+ }
673
+ function importTextMentionsIdentifier(importText, identifier) {
674
+ let re = IMPORT_IDENTIFIER_RE_CACHE.get(identifier);
675
+ if (!re) {
676
+ re = new RegExp(`(?:^|[^A-Za-z0-9_$])${escapeRegexForRegExp(identifier)}(?:$|[^A-Za-z0-9_$])`);
677
+ IMPORT_IDENTIFIER_RE_CACHE.set(identifier, re);
678
+ }
679
+ return re.test(importText);
680
+ }
681
+ function escapeRegexForRegExp(s) {
682
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
683
+ }
684
+ /** Quick pre-check: does this source mention className or style in a JSX prop context? */
685
+ const CLASSNAME_STYLE_QUICK_RE = /\b(className|style)\s*[={]/;
686
+ /** Matches `import { Original as Local, ... }` — captures original and local names. */
687
+ const IMPORT_ALIAS_ENTRY_RE = /\b(\w+)\s+as\s+(\w+)/g;
688
+ /**
689
+ * Build a mapping from local alias names to original imported names for a source file.
690
+ * Only includes PascalCase names that differ from their original (actual aliases).
691
+ */
692
+ function buildLocalToImportedMap(source) {
693
+ const map = /* @__PURE__ */ new Map();
694
+ IMPORT_ALIAS_ENTRY_RE.lastIndex = 0;
695
+ for (const m of source.matchAll(IMPORT_ALIAS_ENTRY_RE)) {
696
+ const original = m[1];
697
+ const local = m[2];
698
+ if (original !== local && /^[A-Z]/.test(local)) map.set(local, original);
699
+ }
700
+ return map;
701
+ }
702
+ /**
703
+ * Scan source for JSX usage of specific components with className or style props.
704
+ * Uses a two-step approach: first quick-checks for className/style keywords,
705
+ * then scans JSX open tags to match component names — avoids building a huge
706
+ * alternation regex when there are hundreds of component names.
707
+ * Handles aliased imports (e.g., `import { Alert as MyAlert }`) by resolving
708
+ * local tag names back to their original exported names.
709
+ */
710
+ function scanClassNameStyleUsages(source, componentNames) {
711
+ if (!CLASSNAME_STYLE_QUICK_RE.test(source)) return [];
712
+ let aliasMap;
713
+ const matches = [];
714
+ for (const m of source.matchAll(/<([A-Z][A-Za-z0-9]*)\b[^<>]*\b(?:className|style)\s*[={]/gs)) {
715
+ const tagName = m[1];
716
+ if (!tagName) continue;
717
+ if (componentNames.has(tagName)) {
718
+ matches.push(tagName);
719
+ continue;
720
+ }
721
+ aliasMap ??= buildLocalToImportedMap(source);
722
+ const originalName = aliasMap.get(tagName);
723
+ if (originalName && componentNames.has(originalName)) matches.push(originalName);
724
+ }
725
+ return matches;
726
+ }
727
+ /**
728
+ * Use ripgrep to find files containing `className` or `style` props.
729
+ * Searches for the prop keywords (not component names) to keep the pattern
730
+ * small and fast — the full component-name regex narrows results down.
731
+ */
732
+ function rgClassNameStyleFilter(files) {
733
+ const dirs = deduplicateParentDirs(files);
734
+ if (dirs.length === 0) return;
735
+ try {
736
+ const globArgs = [
737
+ "*.tsx",
738
+ "*.ts",
739
+ "*.jsx",
740
+ "*.js",
741
+ "*.mts",
742
+ "*.cts",
743
+ "*.mjs",
744
+ "*.cjs"
745
+ ].map((glob) => `--glob ${shellQuote(glob)}`).join(" ");
746
+ const output = execSync(`rg -l ${shellQuote(String.raw`\b(className|style)\s*[={]`)} ${globArgs} ${dirs.map(shellQuote).join(" ")}`, {
747
+ encoding: "utf-8",
748
+ maxBuffer: 10 * 1024 * 1024
749
+ });
750
+ return new Set(output.trim().split("\n").filter(Boolean).map((f) => resolve(f)));
751
+ } catch (err) {
752
+ if (err instanceof Error && "status" in err && err.status === 1) return /* @__PURE__ */ new Set();
753
+ return;
754
+ }
755
+ }
756
+ /** Scan a single file for cross-file selector usages using AST parsing. */
757
+ function scanFileForSelectorsAst(filePath, source, transformSet, resolver, parser, toRealPath, readFile, cache, failOnParseError) {
758
+ const hash = cache ? createHash("md5").update(source).digest("hex") : void 0;
759
+ const cached = cache && hash ? cache.get(hash) : void 0;
760
+ let importMap;
761
+ let styledImportName;
762
+ let selectorLocals;
763
+ if (cached) {
764
+ importMap = cached.importMap;
765
+ styledImportName = cached.styledImportName;
766
+ selectorLocals = cached.selectorLocals;
767
+ } else {
768
+ let ast;
769
+ try {
770
+ ast = parser.parse(source);
771
+ } catch (err) {
772
+ if (failOnParseError) {
773
+ const reason = err instanceof Error ? err.message : String(err);
774
+ throw new Error(`Failed to parse ${filePath}: ${reason}`);
775
+ }
776
+ return [];
777
+ }
778
+ const program = ast.program ?? ast;
779
+ const importNodes = [];
780
+ const taggedTemplateNodes = [];
781
+ walkForImportsAndTemplates(program, importNodes, taggedTemplateNodes);
782
+ importMap = buildImportMapFromNodes(importNodes);
783
+ styledImportName = findStyledImportNameFromNodes(importNodes);
784
+ selectorLocals = styledImportName ? findComponentSelectorLocalsFromNodes(taggedTemplateNodes, styledImportName) : /* @__PURE__ */ new Set();
785
+ if (cache && hash) cache.set(hash, {
786
+ importMap,
787
+ styledImportName,
788
+ selectorLocals
789
+ });
790
+ }
791
+ if (importMap.size === 0 || !styledImportName || selectorLocals.size === 0) return [];
792
+ const consumerIsTransformed = transformSet.has(filePath);
793
+ const usages = [];
794
+ for (const localName of selectorLocals) {
795
+ const imp = importMap.get(localName);
796
+ if (!imp || imp.source === "styled-components") continue;
797
+ const resolvedPath = resolver.resolve(filePath, imp.source);
798
+ if (!resolvedPath) continue;
799
+ const realResolved = toRealPath(resolvedPath);
800
+ if (realResolved === filePath) continue;
801
+ const usage = {
802
+ localName,
803
+ importSource: imp.source,
804
+ importedName: imp.importedName,
805
+ resolvedPath: realResolved,
806
+ consumerPath: filePath,
807
+ consumerIsTransformed
808
+ };
809
+ applyBridgeFields(usage, imp.importedName, localName, realResolved, importMap, readFile);
810
+ usages.push(usage);
811
+ }
812
+ return usages;
813
+ }
814
+ /**
815
+ * Use ripgrep to quickly find files containing "styled-components" or `as[={]`.
816
+ * Returns a Set of absolute file paths, or undefined if rg is not available.
817
+ */
818
+ function rgPreFilter(files) {
819
+ const dirs = deduplicateParentDirs(files);
820
+ if (dirs.length === 0) return;
821
+ try {
822
+ const pattern = String.raw`(styled-components|\bas[={])`;
823
+ const globArgs = [
824
+ "*.tsx",
825
+ "*.ts",
826
+ "*.jsx",
827
+ "*.js",
828
+ "*.mts",
829
+ "*.cts",
830
+ "*.mjs",
831
+ "*.cjs"
832
+ ].map((glob) => `--glob ${shellQuote(glob)}`).join(" ");
833
+ const output = execSync(`rg -l ${shellQuote(pattern)} ${globArgs} ${dirs.map(shellQuote).join(" ")}`, {
834
+ encoding: "utf-8",
835
+ maxBuffer: 10 * 1024 * 1024
836
+ });
837
+ return new Set(output.trim().split("\n").filter(Boolean).map((f) => resolve(f)));
838
+ } catch (err) {
839
+ if (err instanceof Error && "status" in err && err.status === 1) return /* @__PURE__ */ new Set();
840
+ return;
841
+ }
842
+ }
843
+ function shellQuote(s) {
844
+ return "'" + s.replace(/'/g, "'\\''") + "'";
845
+ }
846
+ /**
847
+ * Given a list of absolute file paths, extract the minimal set of parent directories.
848
+ * E.g., ["/a/b/c.ts", "/a/b/d.ts", "/a/e/f.ts"] → ["/a/b/", "/a/e/"]
849
+ * Then dedup so "/a/" subsumes both "/a/b/" and "/a/e/".
850
+ */
851
+ function deduplicateParentDirs(files) {
852
+ const dirSet = /* @__PURE__ */ new Set();
853
+ for (const f of files) dirSet.add(f.slice(0, f.lastIndexOf("/") + 1));
854
+ const sorted = [...dirSet].sort();
855
+ const result = [];
856
+ for (const d of sorted) {
857
+ if (result.length > 0 && d.startsWith(result[result.length - 1])) continue;
858
+ result.push(d);
859
+ }
860
+ return result;
861
+ }
862
+ function logPrepassDebug(scannedFiles, info, consumerAnalysis) {
863
+ const cwd = process.cwd();
864
+ const rel = (p) => relative(cwd, p);
865
+ const lines = ["[DEBUG_CODEMOD] Unified prepass:"];
866
+ lines.push(` Scanned ${scannedFiles.length} file(s)`);
867
+ if (info.selectorUsages.size === 0) lines.push(" No cross-file selector usages found.");
868
+ else {
869
+ lines.push(` Found cross-file selector usages in ${info.selectorUsages.size} file(s):`);
870
+ for (const [consumer, usages] of info.selectorUsages) for (const u of usages) lines.push(` ${rel(consumer)} → ${u.importedName} (from ${rel(u.resolvedPath)}, transformed=${u.consumerIsTransformed})`);
871
+ }
872
+ if (info.componentsNeedingMarkerSidecar.size > 0) {
873
+ lines.push(" Components needing marker sidecar (both consumer and target transformed):");
874
+ for (const [file, names] of info.componentsNeedingMarkerSidecar) lines.push(` ${rel(file)}: ${[...names].join(", ")}`);
875
+ }
876
+ if (info.componentsNeedingGlobalSelectorBridge.size > 0) {
877
+ lines.push(" Components needing global selector bridge className (consumer not transformed):");
878
+ for (const [file, names] of info.componentsNeedingGlobalSelectorBridge) lines.push(` ${rel(file)}: ${[...names].join(", ")}`);
879
+ }
880
+ if (consumerAnalysis) lines.push(` Consumer analysis: ${consumerAnalysis.size} entries`);
881
+ process.stderr.write(lines.join("\n") + "\n");
882
+ }
883
+
884
+ //#endregion
885
+ export { runPrepass };