styled-components-to-stylex-codemod 0.0.55 → 0.0.57

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/dist/index.mjs CHANGED
@@ -1,13 +1,173 @@
1
- import { $ as describeValue, Q as assertValidAdapterInput, Y as defineAdapter, q as mergeMarkerDeclarations, t as transformedComponentAcceptsSx, x as identifierName } from "./sx-surface-Cth8EesU.mjs";
2
- import { c as resolveBarrelReExport, d as Logger, n as createPrepassParser, t as walkAst } from "./ast-walk-C226poBl.mjs";
3
- import { r as extractStyledDefBasesFromSource } from "./compute-leaf-set-Cu4lMMQ9.mjs";
1
+ import { t as createModuleResolver } from "./resolve-imports-DgSAddIF.mjs";
2
+ import { $ as assertValidAdapterInput, J as mergeMarkerDeclarations, X as defineAdapter, et as describeValue, t as transformedComponentAcceptsSx, x as identifierName } from "./sx-surface-Kv8zK8L4.mjs";
3
+ import { S as Logger, T as getCascadeDependedFilePath, a as buildImportMapFromNodes, b as resolveBarrelReExportBinding, f as walkForImportsAndTemplates, m as createPrepassParser, s as collectStyledLocalBindingNames, t as walkAst, y as resolveBarrelReExport } from "./ast-walk-DVmYZ2mK.mjs";
4
4
  import { r as toRealPath } from "./path-utils-BC4U8X_q.mjs";
5
5
  import jscodeshift from "jscodeshift";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
- import { dirname, join, resolve } from "node:path";
7
+ import { dirname, join, relative, resolve } from "node:path";
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { glob, readFile, writeFile } from "node:fs/promises";
10
10
  import { spawn } from "node:child_process";
11
+ //#region src/internal/prepass/styled-def-bases.ts
12
+ /**
13
+ * Extracts styled-component definition bases for prepass consumers that need
14
+ * component names and their root shape.
15
+ */
16
+ const RX_EXPORT_DECL = String.raw`(?:export\s+)?(?:const|let|var)\s+`;
17
+ /** `const Name = styled.tag` — intrinsic HTML/SVG tag member. */
18
+ const STYLED_INTRINSIC_MEMBER_RE = new RegExp(String.raw`\b${RX_EXPORT_DECL}([A-Z][A-Za-z0-9]*)\b[^=]*=\s*styled\.([a-z][a-zA-Z0-9]*)\b`, "g");
19
+ /** `const Name = styled("tag")` — intrinsic string tag. */
20
+ const STYLED_INTRINSIC_STRING_RE = new RegExp(String.raw`\b${RX_EXPORT_DECL}([A-Z][A-Za-z0-9]*)\b[^=]*=\s*styled\s*\(\s*["']([^"']+)["']`, "g");
21
+ /** `const Name = styled(Component)` — wraps another component identifier. */
22
+ const STYLED_COMPONENT_RE = new RegExp(String.raw`\b${RX_EXPORT_DECL}([A-Z][A-Za-z0-9]*)\b[^=]*=\s*styled\s*\(\s*([A-Z][A-Za-z0-9]*)\s*\)`, "g");
23
+ /**
24
+ * Regex-derived styled definition bases for files in the transform set.
25
+ * Later entries for the same component name overwrite earlier ones (rare).
26
+ */
27
+ function extractStyledDefBasesFromSource(filePath, source, into) {
28
+ let map = into.get(filePath);
29
+ if (!map) {
30
+ map = /* @__PURE__ */ new Map();
31
+ into.set(filePath, map);
32
+ }
33
+ STYLED_INTRINSIC_MEMBER_RE.lastIndex = 0;
34
+ for (const m of source.matchAll(STYLED_INTRINSIC_MEMBER_RE)) {
35
+ const name = m[1];
36
+ if (name) map.set(name, { kind: "intrinsic" });
37
+ }
38
+ STYLED_INTRINSIC_STRING_RE.lastIndex = 0;
39
+ for (const m of source.matchAll(STYLED_INTRINSIC_STRING_RE)) {
40
+ const name = m[1];
41
+ if (name) map.set(name, { kind: "intrinsic" });
42
+ }
43
+ STYLED_COMPONENT_RE.lastIndex = 0;
44
+ for (const m of source.matchAll(STYLED_COMPONENT_RE)) {
45
+ const name = m[1];
46
+ const ident = m[2];
47
+ if (name && ident) map.set(name, {
48
+ kind: "component",
49
+ ident
50
+ });
51
+ }
52
+ }
53
+ /**
54
+ * Regex baseline for styled defs, then an AST pass overrides/adds rows when the
55
+ * source parses. The AST pass understands aliased/named `styled` imports
56
+ * (`import { styled as sc }`) that the regexes (which assume the literal `styled`)
57
+ * miss, so callers that only ran the regex extractor under-report components.
58
+ */
59
+ function extractStyledDefBases(filePath, source, parser, into) {
60
+ extractStyledDefBasesFromSource(filePath, source, into);
61
+ try {
62
+ const ast = parser.parse(source);
63
+ const program = ast.program ?? ast;
64
+ const importNodes = [];
65
+ walkForImportsAndTemplates(program, importNodes, []);
66
+ extractStyledDefBasesFromAstProgram(filePath, program, collectStyledLocalBindingNames(importNodes), into);
67
+ } catch {}
68
+ }
69
+ /**
70
+ * AST-based extraction: understands `let`/`var`, export blocks, named `styled` imports,
71
+ * and `.attrs` / `.withConfig` chains before the tagged template.
72
+ * Results merge into `into`; bindings found here override regex entries for the same name.
73
+ */
74
+ function extractStyledDefBasesFromAstProgram(filePath, program, styledLocalNames, into) {
75
+ if (styledLocalNames.size === 0) return;
76
+ let map = into.get(filePath);
77
+ if (!map) {
78
+ map = /* @__PURE__ */ new Map();
79
+ into.set(filePath, map);
80
+ }
81
+ const body = program.body;
82
+ if (!body) return;
83
+ for (const stmt of body) walkStatement(stmt);
84
+ function walkStatement(stmt) {
85
+ if (stmt.type === "VariableDeclaration") {
86
+ for (const d of stmt.declarations ?? []) processDeclarator(d);
87
+ return;
88
+ }
89
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) walkStatement(stmt.declaration);
90
+ }
91
+ function processDeclarator(decl) {
92
+ if (decl.type !== "VariableDeclarator") return;
93
+ const id = decl.id;
94
+ if (id.type !== "Identifier" || typeof id.name !== "string") return;
95
+ const tpl = findTaggedTemplate(unwrapInitializer(decl.init));
96
+ if (!tpl || tpl.type !== "TaggedTemplateExpression") return;
97
+ const base = classifyStyledTemplateTag(tpl.tag, styledLocalNames);
98
+ if (base) map.set(id.name, base);
99
+ }
100
+ }
101
+ function unwrapInitializer(node) {
102
+ let cur = node ?? void 0;
103
+ while (cur) {
104
+ if (cur.type === "TSAsExpression" || cur.type === "AsExpression") {
105
+ cur = cur.expression;
106
+ continue;
107
+ }
108
+ if (cur.type === "ParenthesizedExpression") {
109
+ cur = cur.expression;
110
+ continue;
111
+ }
112
+ return cur;
113
+ }
114
+ }
115
+ function findTaggedTemplate(node) {
116
+ const n = unwrapInitializer(node);
117
+ if (!n) return;
118
+ if (n.type === "TaggedTemplateExpression") return n;
119
+ }
120
+ /** Peel `.attrs` / `.withConfig` / nested calls down to `styled.div` or `styled(X)`. */
121
+ function peelStyledApplication(tag, styledNames) {
122
+ let cur = tag;
123
+ while (cur) {
124
+ if (cur.type === "CallExpression") {
125
+ const callee = cur.callee;
126
+ if (callee?.type === "MemberExpression") {
127
+ cur = callee;
128
+ continue;
129
+ }
130
+ if (callee?.type === "Identifier" && typeof callee.name === "string" && styledNames.has(callee.name)) return cur;
131
+ return null;
132
+ }
133
+ if (cur.type === "MemberExpression") {
134
+ const obj = cur.object;
135
+ if (obj?.type === "Identifier" && typeof obj.name === "string" && styledNames.has(obj.name)) return cur;
136
+ cur = obj;
137
+ continue;
138
+ }
139
+ break;
140
+ }
141
+ return null;
142
+ }
143
+ function classifyStyledTemplateTag(tag, styledNames) {
144
+ const root = peelStyledApplication(tag, styledNames);
145
+ if (!root) return null;
146
+ if (root.type === "MemberExpression") {
147
+ const obj = root.object;
148
+ const prop = root.property;
149
+ const objName = obj?.type === "Identifier" ? obj.name : void 0;
150
+ if (obj?.type !== "Identifier" || typeof objName !== "string" || !styledNames.has(objName)) return null;
151
+ const isComputed = Boolean(root.computed);
152
+ if (isComputed && prop?.type === "StringLiteral" && typeof prop.value === "string") return { kind: "intrinsic" };
153
+ if (!isComputed && prop?.type === "Identifier" && typeof prop.name === "string") return { kind: "intrinsic" };
154
+ return null;
155
+ }
156
+ if (root.type === "CallExpression") {
157
+ const callee = root.callee;
158
+ const arg0 = root.arguments?.[0];
159
+ const calleeName = callee?.type === "Identifier" ? callee.name : void 0;
160
+ if (callee?.type !== "Identifier" || typeof calleeName !== "string" || !styledNames.has(calleeName) || !arg0) return null;
161
+ if (arg0.type === "Identifier" && typeof arg0.name === "string") return {
162
+ kind: "component",
163
+ ident: arg0.name
164
+ };
165
+ if (arg0.type === "StringLiteral" && typeof arg0.value === "string") return { kind: "intrinsic" };
166
+ return null;
167
+ }
168
+ return null;
169
+ }
170
+ //#endregion
11
171
  //#region src/internal/prepass/resolve-static-members.ts
12
172
  /**
13
173
  * Resolves the underlying component name(s) a static member access like `Select.Option` refers to,
@@ -24,7 +184,7 @@ import { spawn } from "node:child_process";
24
184
  * behavior so downstream metadata lookups still have something to try).
25
185
  */
26
186
  function resolveStaticMemberComponentNames(source, rootNames, memberPath, parserName = "tsx") {
27
- const program = parseProgram(source, parserName);
187
+ const program = parseProgram$1(source, parserName);
28
188
  const fallbackMember = memberPath[memberPath.length - 1];
29
189
  const fallback = fallbackMember ? [fallbackMember] : [];
30
190
  if (!program) return [...new Set([...rootNames, ...fallback])];
@@ -39,7 +199,7 @@ function resolveStaticMemberComponentNames(source, rootNames, memberPath, parser
39
199
  return [...new Set([...owners, ...fallback])];
40
200
  }
41
201
  const programCache = /* @__PURE__ */ new Map();
42
- function parseProgram(source, parserName) {
202
+ function parseProgram$1(source, parserName) {
43
203
  const cached = programCache.get(source);
44
204
  if (cached !== void 0) return cached;
45
205
  const parsed = tryParse(source, parserName);
@@ -167,6 +327,12 @@ function capitalizedIdentifierName(node) {
167
327
  * Core concepts: jscodeshift execution, globs, and adapter hooks.
168
328
  */
169
329
  const __dirname = dirname(fileURLToPath(import.meta.url));
330
+ /** Expand glob pattern(s) into file paths relative to `cwd`. */
331
+ async function expandGlobFiles(patterns, cwd) {
332
+ const filePaths = [];
333
+ for (const pattern of patterns) for await (const file of glob(pattern, { cwd })) filePaths.push(file);
334
+ return filePaths;
335
+ }
170
336
  /**
171
337
  * Run the styled-components to StyleX transform on files matching the glob pattern.
172
338
  *
@@ -241,13 +407,11 @@ async function runTransform(options) {
241
407
  "Example: consumerPaths: \"src/**/*.tsx\" // scan for cross-file usage",
242
408
  "Example: consumerPaths: null // opt out"
243
409
  ].join("\n"));
244
- const transformModeRaw = options.transformMode;
245
- if (transformModeRaw !== void 0 && transformModeRaw !== "all" && transformModeRaw !== "leavesOnly") throw new Error(["runTransform(options): `transformMode` must be one of: \"all\", \"leavesOnly\".", `Received: transformMode=${describeValue(transformModeRaw)}`].join("\n"));
246
- const leavesOnly = options.transformMode === "leavesOnly";
247
410
  const { files, consumerPaths: consumerPathsOption, dryRun = false, print = false, parser = "tsx", formatterCommands, maxExamples } = options;
248
411
  if (maxExamples !== void 0) Logger.setMaxExamples(maxExamples);
249
412
  const adapterInput = options.adapter;
250
413
  assertValidAdapterInput(adapterInput, "runTransform(options)");
414
+ if ((options.assumeConvertedFiles?.length ?? 0) > 0 && !dryRun) throw new Error("runTransform(options): `assumeConvertedFiles` is only supported with `dryRun: true` (it is used by the migration planner's analysis passes). Seeding assumed conversions on a writing run would bypass the cascade-conflict safety bail.");
251
415
  if (adapterInput.externalInterface === "auto" && consumerPathsOption === null) throw new Error([
252
416
  "runTransform(options): externalInterface is \"auto\" but consumerPaths is null.",
253
417
  "Auto-detection needs consumer file globs to scan for styled(Component) and as-prop usage.",
@@ -296,9 +460,8 @@ async function runTransform(options) {
296
460
  }
297
461
  };
298
462
  const patterns = Array.isArray(files) ? files : [files];
299
- let filePaths = [];
300
463
  const cwd = process.cwd();
301
- for (const pattern of patterns) for await (const file of glob(pattern, { cwd })) filePaths.push(file);
464
+ let filePaths = await expandGlobFiles(patterns, cwd);
302
465
  if (filePaths.length === 0) {
303
466
  Logger.warn("No files matched the provided glob pattern(s)");
304
467
  return {
@@ -307,13 +470,13 @@ async function runTransform(options) {
307
470
  skipped: 0,
308
471
  transformed: 0,
309
472
  timeElapsed: 0,
310
- warnings: []
473
+ warnings: [],
474
+ fileResults: []
311
475
  };
312
476
  }
313
477
  Logger.setFileCount(filePaths.length);
314
478
  const consumerPatterns = consumerPathsOption ? Array.isArray(consumerPathsOption) ? consumerPathsOption : [consumerPathsOption] : [];
315
- const consumerFilePaths = [];
316
- for (const pattern of consumerPatterns) for await (const file of glob(pattern, { cwd })) consumerFilePaths.push(file);
479
+ const consumerFilePaths = await expandGlobFiles(consumerPatterns, cwd);
317
480
  if (consumerPatterns.length > 0 && consumerFilePaths.length === 0) throw new Error([
318
481
  "runTransform(options): consumerPaths matched no files.",
319
482
  `Pattern(s): ${consumerPatterns.join(", ")}`,
@@ -322,7 +485,7 @@ async function runTransform(options) {
322
485
  const { createModuleResolver } = await import("./resolve-imports-DgSAddIF.mjs").then((n) => n.n);
323
486
  const sharedResolver = createModuleResolver();
324
487
  filePaths = orderFilesByLocalImportDependencies(filePaths, sharedResolver, toRealPath);
325
- const { runPrepass } = await import("./run-prepass-D3Ti1ryc.mjs");
488
+ const { runPrepass } = await import("./run-prepass-CGL_ugPB.mjs");
326
489
  const absoluteFiles = filePaths.map((f) => resolve(f));
327
490
  const absoluteConsumers = consumerFilePaths.map((f) => resolve(f));
328
491
  let prepassResult;
@@ -335,9 +498,7 @@ async function runTransform(options) {
335
498
  resolver: sharedResolver,
336
499
  parserName: parser,
337
500
  createExternalInterface: adapterInput.externalInterface === "auto",
338
- enableAstCache: true,
339
- leavesOnly,
340
- resolveBaseComponent: adapterInput.resolveBaseComponent
501
+ enableAstCache: true
341
502
  });
342
503
  Logger.info(`Prepass: completed in ${formatElapsedSeconds(prepassStartedAt)}s\n`);
343
504
  } catch (err) {
@@ -349,8 +510,7 @@ async function runTransform(options) {
349
510
  componentsNeedingMarkerSidecar: /* @__PURE__ */ new Map(),
350
511
  componentsNeedingGlobalSelectorBridge: /* @__PURE__ */ new Map(),
351
512
  propUsageByFile: /* @__PURE__ */ new Map(),
352
- stylexComponentFiles: /* @__PURE__ */ new Map(),
353
- globalLeafKeys: leavesOnly ? /* @__PURE__ */ new Set() : void 0
513
+ stylexComponentFiles: /* @__PURE__ */ new Map()
354
514
  },
355
515
  consumerAnalysis: void 0,
356
516
  forwardedAsConsumers: /* @__PURE__ */ new Map(),
@@ -360,6 +520,21 @@ async function runTransform(options) {
360
520
  const transformedFiles = /* @__PURE__ */ new Set();
361
521
  const transformedComponents = /* @__PURE__ */ new Map();
362
522
  const transformedFileSources = /* @__PURE__ */ new Map();
523
+ const seedParser = createPrepassParser(parser);
524
+ for (const assumedFile of options.assumeConvertedFiles ?? []) {
525
+ const realPath = toRealPath(resolve(assumedFile));
526
+ transformedFiles.add(realPath);
527
+ if (!transformedComponents.has(realPath)) {
528
+ const extracted = /* @__PURE__ */ new Map();
529
+ let source = "";
530
+ try {
531
+ source = readFileSync(realPath, "utf-8");
532
+ extractStyledDefBases(realPath, source, seedParser, extracted);
533
+ } catch {}
534
+ transformedComponents.set(realPath, new Set(extracted.get(realPath)?.keys() ?? []));
535
+ transformedFileSources.set(realPath, source);
536
+ }
537
+ }
363
538
  const crossFilePrepassResult = {
364
539
  ...prepassResult.crossFileInfo,
365
540
  transformedFiles,
@@ -400,7 +575,7 @@ async function runTransform(options) {
400
575
  const cached = styledDefinitionNamesByFile.get(realPath);
401
576
  if (cached) return cached;
402
577
  const extracted = /* @__PURE__ */ new Map();
403
- extractStyledDefBasesFromSource(realPath, cachedRead(realPath), extracted);
578
+ extractStyledDefBases(realPath, cachedRead(realPath), seedParser, extracted);
404
579
  const names = new Set(extracted.get(realPath)?.keys() ?? []);
405
580
  styledDefinitionNamesByFile.set(realPath, names);
406
581
  return names;
@@ -495,9 +670,7 @@ async function runTransform(options) {
495
670
  transformedComponents,
496
671
  transformedFileSources,
497
672
  transientPropRenames,
498
- allowPartialMigration: options.allowPartialMigration ?? (leavesOnly ? true : false),
499
- transformMode: leavesOnly ? "leavesOnly" : options.transformMode ?? "all",
500
- globalLeafKeys: crossFilePrepassResult.globalLeafKeys,
673
+ allowPartialMigration: options.allowPartialMigration ?? false,
501
674
  resolveModule: (fromFile, specifier) => sharedResolver.resolve(resolve(fromFile), specifier),
502
675
  runInBand: true,
503
676
  silent: options.silent ?? false
@@ -548,7 +721,7 @@ async function runTransform(options) {
548
721
  const result = await runTransformSequentially(transformModule, filePaths, runnerOptions);
549
722
  if (sidecarFiles.size > 0 && !dryRun) for (const [sidecarPath, content] of sidecarFiles) await writeFile(sidecarPath, mergeSidecarContent(sidecarPath, content), "utf-8");
550
723
  if (bridgeResults.size > 0 && !dryRun) {
551
- const { buildConsumerReplacements, patchConsumerFile } = await import("./bridge-consumer-patcher-DDcYZM_G.mjs");
724
+ const { buildConsumerReplacements, patchConsumerFile } = await import("./bridge-consumer-patcher-jeeDUlId.mjs");
552
725
  const consumerReplacements = buildConsumerReplacements(crossFilePrepassResult.selectorUsages, bridgeResults, transformedFiles);
553
726
  const patchedFiles = [];
554
727
  for (const [consumerPath, replacements] of consumerReplacements) {
@@ -561,7 +734,7 @@ async function runTransform(options) {
561
734
  if (formatterCommands && patchedFiles.length > 0) await runFormatters(formatterCommands, patchedFiles);
562
735
  }
563
736
  if (prepassResult.forwardedAsConsumers.size > 0 && !dryRun) {
564
- const { buildForwardedAsReplacements, patchConsumerForwardedAs } = await import("./forwarded-as-consumer-patcher-Bva_36Gy.mjs");
737
+ const { buildForwardedAsReplacements, patchConsumerForwardedAs } = await import("./forwarded-as-consumer-patcher-Do4PI4Qs.mjs");
565
738
  const forwardedAsReplacements = buildForwardedAsReplacements(prepassResult.forwardedAsConsumers, transformedFiles);
566
739
  const patchedFiles = [];
567
740
  for (const [consumerPath, entries] of forwardedAsReplacements) {
@@ -574,7 +747,7 @@ async function runTransform(options) {
574
747
  if (formatterCommands && patchedFiles.length > 0) await runFormatters(formatterCommands, patchedFiles);
575
748
  }
576
749
  if (transientPropRenames.size > 0 && !dryRun) {
577
- const { collectTransientPropPatches } = await import("./transient-prop-consumer-patcher-DSd7uVA6.mjs");
750
+ const { collectTransientPropPatches } = await import("./transient-prop-consumer-patcher-D-iqO8-T.mjs");
578
751
  const patches = collectTransientPropPatches({
579
752
  transientPropRenames,
580
753
  consumerFilePaths: consumerFilePaths.map((p) => resolve(p)),
@@ -589,7 +762,7 @@ async function runTransform(options) {
589
762
  }
590
763
  if (formatterCommands && formatterCommands.length > 0 && result.ok > 0 && !dryRun) await runFormatters(formatterCommands, filePaths);
591
764
  const report = Logger.createReport();
592
- report.print();
765
+ if (!(options.silent ?? false)) report.print();
593
766
  return {
594
767
  errors: result.error,
595
768
  unchanged: result.nochange,
@@ -597,6 +770,7 @@ async function runTransform(options) {
597
770
  transformed: result.ok,
598
771
  timeElapsed: parseFloat(result.timeElapsed) || 0,
599
772
  warnings: report.getWarnings(),
773
+ fileResults: result.files,
600
774
  standaloneFileResults: standaloneResult?.files,
601
775
  standaloneWarnings
602
776
  };
@@ -703,16 +877,10 @@ function createStandalonePrepassResult(prepass, filePath, transformedFiles, tran
703
877
  selectorUsages,
704
878
  componentsNeedingMarkerSidecar,
705
879
  componentsNeedingGlobalSelectorBridge,
706
- globalLeafKeys: getStandaloneGlobalLeafKeys(prepass.globalLeafKeys, standaloneFile),
707
880
  transformedFiles,
708
881
  transformedComponents
709
882
  };
710
883
  }
711
- function getStandaloneGlobalLeafKeys(globalLeafKeys, standaloneFile) {
712
- if (!globalLeafKeys) return;
713
- const filePrefix = `${standaloneFile}:`;
714
- return new Set([...globalLeafKeys].filter((key) => key.startsWith(filePrefix)));
715
- }
716
884
  function addSetMapEntry(map, key, value) {
717
885
  const values = map.get(key);
718
886
  if (values) {
@@ -837,4 +1005,423 @@ async function runFormatters(commands, files) {
837
1005
  }
838
1006
  }
839
1007
  //#endregion
840
- export { defineAdapter, runTransform };
1008
+ //#region src/migration-plan.ts
1009
+ /**
1010
+ * Analysis-only mode: produce an ordered plan of the files that must be
1011
+ * converted by hand before the codemod can finish the rest of the migration.
1012
+ *
1013
+ * Core concepts: genuine blocker detection (files the codemod truly cannot
1014
+ * convert, as opposed to files that only bail because a dependency is still
1015
+ * styled-components), bottom-up dependency ordering, and consumer/imported-export
1016
+ * accounting so each blocker is presented with the impact of converting it.
1017
+ */
1018
+ /**
1019
+ * Run the codemod in analysis-only (dry) mode and compute the ordered list of
1020
+ * files that block the rest of the migration and must be converted manually.
1021
+ */
1022
+ async function analyzeMigrationPlan(options) {
1023
+ const cwd = process.cwd();
1024
+ const filePatterns = Array.isArray(options.files) ? options.files : [options.files];
1025
+ const consumerPatterns = options.consumerPaths === null ? [] : Array.isArray(options.consumerPaths) ? options.consumerPaths : [options.consumerPaths];
1026
+ const runFiles = await expandGlobFiles(filePatterns, cwd);
1027
+ const scanFiles = unique([...runFiles, ...await expandGlobFiles(consumerPatterns, cwd)]);
1028
+ const parser = options.parser ?? "tsx";
1029
+ const runFilesNorm = new Set(runFiles.map((file) => norm(file, cwd)));
1030
+ const cascadeUnblocks = collectCascadeUnblocks((await runAnalysisPass(options, parser, [])).warnings, cwd);
1031
+ const externalBlockerSet = new Set([...cascadeUnblocks.keys()].filter((target) => !runFilesNorm.has(target)));
1032
+ const maxPasses = options.maxAnalysisPasses ?? MAX_ANALYSIS_PASSES;
1033
+ const assumedConverted = new Set(externalBlockerSet);
1034
+ let blockerReasons = /* @__PURE__ */ new Map();
1035
+ let stabilized = false;
1036
+ for (let pass = 0; pass < maxPasses; pass++) {
1037
+ const passResult = await runAnalysisPass(options, parser, [...assumedConverted]);
1038
+ blockerReasons = collectGenuineBlockers(passResult.warnings, passResult.fileResults, cwd);
1039
+ const newlyFound = [...blockerReasons.keys()].filter((file) => !assumedConverted.has(file));
1040
+ if (newlyFound.length === 0) {
1041
+ stabilized = true;
1042
+ break;
1043
+ }
1044
+ for (const file of newlyFound) assumedConverted.add(file);
1045
+ }
1046
+ if (!stabilized) throw new Error(`Migration plan analysis did not stabilize within ${maxPasses} passes — the cascade blocker chain is deeper than the analysis cap, so the plan would be incomplete. Raise \`maxAnalysisPasses\` if this is a legitimately deep chain.`);
1047
+ const reachableExternalBlockers = new Set([...externalBlockerSet].filter((target) => (cascadeUnblocks.get(target)?.size ?? 0) > 0));
1048
+ if (blockerReasons.size === 0 && reachableExternalBlockers.size === 0) return {
1049
+ manualConversionFiles: [],
1050
+ totalFiles: runFiles.length,
1051
+ unlocksFileCount: 0
1052
+ };
1053
+ const graph = buildImportGraph(scanFiles, cwd, parser);
1054
+ const displayByNorm = buildDisplayMap(scanFiles, cwd);
1055
+ const blockerSet = new Set([...blockerReasons.keys(), ...reachableExternalBlockers]);
1056
+ const reasonsByBlocker = new Map(blockerReasons);
1057
+ for (const target of reachableExternalBlockers) reasonsByBlocker.set(target, [{
1058
+ message: EXTERNAL_BLOCKER_REASON,
1059
+ locations: []
1060
+ }]);
1061
+ const { consumersByBlocker, depsByBlocker, consumerCountByBlocker, blockedCountByBlocker, soleBlockerCountByBlocker, weightByBlocker } = buildBlockerGraph(blockerSet, graph, cascadeUnblocks);
1062
+ const manualConversionFiles = orderBottomUp([...blockerSet], depsByBlocker, (blocker) => weightByBlocker.get(blocker) ?? 0).map((blocker, index) => {
1063
+ const importedExports = [...(consumersByBlocker.get(blocker) ?? /* @__PURE__ */ new Map()).entries()].map(([exportName, consumers]) => ({
1064
+ exportName,
1065
+ consumerCount: consumers.size
1066
+ })).sort((a, b) => b.consumerCount - a.consumerCount || a.exportName.localeCompare(b.exportName));
1067
+ const dependsOn = [...depsByBlocker.get(blocker) ?? []].map((dep) => displayByNorm.get(dep) ?? relative(cwd, dep)).sort();
1068
+ return {
1069
+ filePath: displayByNorm.get(blocker) ?? relative(cwd, blocker),
1070
+ order: index + 1,
1071
+ consumerCount: consumerCountByBlocker.get(blocker) ?? 0,
1072
+ soleBlockerFileCount: soleBlockerCountByBlocker.get(blocker) ?? 0,
1073
+ blockedFileCount: blockedCountByBlocker.get(blocker) ?? 0,
1074
+ importedExports,
1075
+ reasons: reasonsByBlocker.get(blocker) ?? [],
1076
+ dependsOn
1077
+ };
1078
+ });
1079
+ const unlockedFiles = /* @__PURE__ */ new Set();
1080
+ for (const consumers of cascadeUnblocks.values()) for (const consumer of consumers) if (!blockerSet.has(consumer)) unlockedFiles.add(consumer);
1081
+ return {
1082
+ manualConversionFiles,
1083
+ totalFiles: runFiles.length,
1084
+ unlocksFileCount: unlockedFiles.size
1085
+ };
1086
+ }
1087
+ /** Render a {@link MigrationPlan} as a human-readable, actionable report. */
1088
+ function formatMigrationPlan(plan) {
1089
+ const { manualConversionFiles, totalFiles, unlocksFileCount } = plan;
1090
+ if (manualConversionFiles.length === 0) return `No manual conversion needed — the codemod can convert all ${totalFiles} file(s) in dependency order.`;
1091
+ const focusPaths = collectFocusPaths(manualConversionFiles);
1092
+ const priority = manualConversionFiles.filter((file) => focusPaths.has(file.filePath));
1093
+ const standalone = manualConversionFiles.filter((file) => !focusPaths.has(file.filePath));
1094
+ const lines = [];
1095
+ lines.push("Manual conversion plan");
1096
+ lines.push("======================");
1097
+ lines.push(`${manualConversionFiles.length} of ${totalFiles} file(s) need manual conversion.`);
1098
+ if (unlocksFileCount > 0) {
1099
+ lines.push(`Focus on the ${priority.length} file(s) below. Direct payoff: converting all listed blockers lets ${unlocksFileCount} file(s) auto-migrate.`);
1100
+ lines.push(`Per-file "directly unlocks" counts are sole-blocker payoffs; chain context is secondary and may need other blockers or manual fixes too.`);
1101
+ }
1102
+ lines.push("");
1103
+ if (priority.length > 0) {
1104
+ const positionByPath = new Map(priority.map((file, index) => [file.filePath, index + 1]));
1105
+ lines.push("Convert in this order (dependencies first):");
1106
+ lines.push("");
1107
+ priority.forEach((file, index) => appendFileEntry(lines, file, index + 1, positionByPath));
1108
+ }
1109
+ if (standalone.length > 0) {
1110
+ lines.push(`Standalone file(s) — nothing else in the plan depends on these; convert as you reach them (${standalone.length}):`);
1111
+ lines.push("");
1112
+ appendStandaloneSummary(lines, standalone);
1113
+ }
1114
+ return lines.join("\n").trimEnd();
1115
+ }
1116
+ /**
1117
+ * Run one dry analysis pass and return only the warnings it produced. `Logger`
1118
+ * is process-global, so snapshot the pre-existing warnings, keep only this run's,
1119
+ * and restore the snapshot afterward so analysis never leaks blocker warnings
1120
+ * into a later transform in the same process.
1121
+ */
1122
+ async function runAnalysisPass(options, parser, assumeConvertedFiles) {
1123
+ const snapshot = Logger.createReport().getWarnings();
1124
+ const priorWarnings = new Set(snapshot);
1125
+ let result;
1126
+ try {
1127
+ result = await runTransform({
1128
+ files: options.files,
1129
+ consumerPaths: options.consumerPaths,
1130
+ adapter: options.adapter,
1131
+ parser,
1132
+ dryRun: true,
1133
+ silent: true,
1134
+ assumeConvertedFiles
1135
+ });
1136
+ } finally {
1137
+ Logger.restoreWarnings(snapshot);
1138
+ }
1139
+ return {
1140
+ warnings: result.warnings.filter((warning) => !priorWarnings.has(warning)),
1141
+ fileResults: result.fileResults
1142
+ };
1143
+ }
1144
+ /** Attribute consumers, in-plan dependencies, and impact weights to each blocker. */
1145
+ function buildBlockerGraph(blockerSet, graph, cascadeUnblocks) {
1146
+ const consumersByBlocker = /* @__PURE__ */ new Map();
1147
+ const depsByBlocker = /* @__PURE__ */ new Map();
1148
+ for (const blocker of blockerSet) {
1149
+ consumersByBlocker.set(blocker, /* @__PURE__ */ new Map());
1150
+ depsByBlocker.set(blocker, /* @__PURE__ */ new Set());
1151
+ }
1152
+ for (const [consumer, edges] of graph) for (const edge of edges) {
1153
+ if (!blockerSet.has(edge.dep) || edge.dep === consumer) continue;
1154
+ addConsumer(consumersByBlocker.get(edge.dep), edge.exportName, consumer);
1155
+ if (blockerSet.has(consumer)) depsByBlocker.get(consumer).add(edge.dep);
1156
+ }
1157
+ const blockersByConsumer = /* @__PURE__ */ new Map();
1158
+ for (const [blocker, consumers] of cascadeUnblocks) for (const consumer of consumers) {
1159
+ const set = blockersByConsumer.get(consumer) ?? /* @__PURE__ */ new Set();
1160
+ set.add(blocker);
1161
+ blockersByConsumer.set(consumer, set);
1162
+ }
1163
+ const consumerCountByBlocker = /* @__PURE__ */ new Map();
1164
+ const blockedCountByBlocker = /* @__PURE__ */ new Map();
1165
+ const soleBlockerCountByBlocker = /* @__PURE__ */ new Map();
1166
+ const weightByBlocker = /* @__PURE__ */ new Map();
1167
+ for (const blocker of blockerSet) {
1168
+ const importers = /* @__PURE__ */ new Set();
1169
+ for (const set of consumersByBlocker.get(blocker).values()) for (const consumer of set) importers.add(consumer);
1170
+ const blocked = [...cascadeUnblocks.get(blocker) ?? []];
1171
+ const soleBlocked = blocked.filter((consumer) => !blockerSet.has(consumer) && (blockersByConsumer.get(consumer)?.size ?? 0) === 1);
1172
+ consumerCountByBlocker.set(blocker, importers.size);
1173
+ blockedCountByBlocker.set(blocker, blocked.length);
1174
+ soleBlockerCountByBlocker.set(blocker, soleBlocked.length);
1175
+ weightByBlocker.set(blocker, soleBlocked.length * 1e6 + blocked.length);
1176
+ }
1177
+ return {
1178
+ consumersByBlocker,
1179
+ depsByBlocker,
1180
+ consumerCountByBlocker,
1181
+ blockedCountByBlocker,
1182
+ soleBlockerCountByBlocker,
1183
+ weightByBlocker
1184
+ };
1185
+ }
1186
+ /**
1187
+ * Files to surface in the ordered focus list: any file that unblocks automatic
1188
+ * migration, plus any file involved in an in-plan dependency relationship (it
1189
+ * depends on another listed file, or another listed file depends on it). Only
1190
+ * fully isolated blockers fall through to the standalone summary, so the ordered
1191
+ * list never loses a real dependency chain — even when nothing unlocks a wrapper.
1192
+ */
1193
+ function collectFocusPaths(files) {
1194
+ const dependedUpon = /* @__PURE__ */ new Set();
1195
+ for (const file of files) for (const dependency of file.dependsOn) dependedUpon.add(dependency);
1196
+ const focus = /* @__PURE__ */ new Set();
1197
+ for (const file of files) if (file.blockedFileCount > 0 || file.dependsOn.length > 0 || dependedUpon.has(file.filePath)) focus.add(file.filePath);
1198
+ return focus;
1199
+ }
1200
+ function appendFileEntry(lines, file, position, positionByPath) {
1201
+ lines.push(`${position}. ${file.filePath}`);
1202
+ const impact = [];
1203
+ if (file.blockedFileCount > 0) impact.push(`directly unlocks ${file.soleBlockerFileCount} file(s)`);
1204
+ const chainOnlyFileCount = file.blockedFileCount - file.soleBlockerFileCount;
1205
+ if (chainOnlyFileCount > 0) {
1206
+ const prefix = file.soleBlockerFileCount > 0 ? "also " : "";
1207
+ impact.push(`chain context: ${chainOnlyFileCount} file(s) ${prefix}bail through this file but are not unlocked by it alone`);
1208
+ }
1209
+ if (file.consumerCount > 0) impact.push(`imported by ${file.consumerCount} file(s)`);
1210
+ if (impact.length > 0) lines.push(` → ${impact.join(" · ")}`);
1211
+ if (file.dependsOn.length > 0) {
1212
+ const deps = file.dependsOn.map((dep) => {
1213
+ const depPosition = positionByPath.get(dep);
1214
+ return depPosition === void 0 ? dep : `#${depPosition} ${dep}`;
1215
+ }).sort();
1216
+ lines.push(` Requires first: ${deps.join(", ")}`);
1217
+ }
1218
+ if (file.importedExports.length > 0) {
1219
+ const exportList = file.importedExports.map((usage) => `${formatExportName(usage.exportName)} (used by ${usage.consumerCount})`).join(", ");
1220
+ lines.push(` Convert these exports: ${exportList}`);
1221
+ }
1222
+ lines.push(" Blocked by:");
1223
+ for (const reason of file.reasons) {
1224
+ lines.push(` • ${reason.message}`);
1225
+ for (const loc of reason.locations.slice(0, MAX_REASON_LOCATIONS)) lines.push(` ${loc.filePath}:${loc.line}:${loc.column}`);
1226
+ const remaining = reason.locations.length - MAX_REASON_LOCATIONS;
1227
+ if (remaining > 0) lines.push(` ... and ${remaining} more location(s)`);
1228
+ }
1229
+ lines.push("");
1230
+ }
1231
+ /** Group standalone blockers by reason so the long tail stays scannable. */
1232
+ function appendStandaloneSummary(lines, standalone) {
1233
+ const filesByReason = /* @__PURE__ */ new Map();
1234
+ for (const file of standalone) {
1235
+ const reasonMessage = file.reasons[0]?.message ?? "Unsupported pattern";
1236
+ const files = filesByReason.get(reasonMessage) ?? [];
1237
+ files.push(file.filePath);
1238
+ filesByReason.set(reasonMessage, files);
1239
+ }
1240
+ const grouped = [...filesByReason.entries()].sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]));
1241
+ for (const [reasonMessage, files] of grouped) {
1242
+ lines.push(` • ${reasonMessage} (${files.length} file(s))`);
1243
+ for (const filePath of files.slice(0, MAX_STANDALONE_FILES_PER_REASON)) lines.push(` ${filePath}`);
1244
+ const remaining = files.length - MAX_STANDALONE_FILES_PER_REASON;
1245
+ if (remaining > 0) lines.push(` ... and ${remaining} more file(s)`);
1246
+ }
1247
+ lines.push("");
1248
+ }
1249
+ function formatExportName(exportName) {
1250
+ if (exportName === "default") return "default export";
1251
+ if (exportName === "*") return "* (namespace import)";
1252
+ return exportName;
1253
+ }
1254
+ const MAX_REASON_LOCATIONS = 3;
1255
+ const MAX_STANDALONE_FILES_PER_REASON = 5;
1256
+ const EXTERNAL_BLOCKER_REASON = "Outside the analyzed files — still uses styled-components and is wrapped by in-scope component(s); convert it or add it to the migration scope first";
1257
+ /** Safety cap on fixpoint passes; each pass reveals at least one new blocker until stable. */
1258
+ const MAX_ANALYSIS_PASSES = 50;
1259
+ /**
1260
+ * A file is a genuine blocker when the codemod did not convert it and the reason
1261
+ * is something other than a dependency-order cascade conflict (which resolves on
1262
+ * its own once the depended-on file is converted), or when it threw outright.
1263
+ */
1264
+ function collectGenuineBlockers(warnings, fileResults, cwd) {
1265
+ const reasonsByFile = /* @__PURE__ */ new Map();
1266
+ const ensureReason = (fileNorm, message) => {
1267
+ let reasons = reasonsByFile.get(fileNorm);
1268
+ if (!reasons) {
1269
+ reasons = /* @__PURE__ */ new Map();
1270
+ reasonsByFile.set(fileNorm, reasons);
1271
+ }
1272
+ let reason = reasons.get(message);
1273
+ if (!reason) {
1274
+ reason = {
1275
+ message,
1276
+ locations: []
1277
+ };
1278
+ reasons.set(message, reason);
1279
+ }
1280
+ return reason;
1281
+ };
1282
+ const unconverted = /* @__PURE__ */ new Set();
1283
+ const erroredFiles = /* @__PURE__ */ new Set();
1284
+ for (const fileResult of fileResults) {
1285
+ if (fileResult.status === "skipped" || fileResult.status === "error") unconverted.add(norm(fileResult.filePath, cwd));
1286
+ if (fileResult.status === "error") erroredFiles.add(norm(fileResult.filePath, cwd));
1287
+ }
1288
+ for (const warning of warnings) {
1289
+ if (warning.type === "styled(ImportedComponent) wraps a component whose file uses styled-components — convert the base component's file first to avoid CSS cascade conflicts") continue;
1290
+ const fileNorm = norm(warning.filePath, cwd);
1291
+ if (!unconverted.has(fileNorm)) continue;
1292
+ const reason = ensureReason(fileNorm, warning.type);
1293
+ if (warning.loc) reason.locations.push({
1294
+ filePath: warning.filePath,
1295
+ line: warning.loc.line,
1296
+ column: warning.loc.column
1297
+ });
1298
+ }
1299
+ for (const fileNorm of erroredFiles) ensureReason(fileNorm, "The codemod threw an error while transforming this file");
1300
+ const result = /* @__PURE__ */ new Map();
1301
+ for (const [fileNorm, reasons] of reasonsByFile) result.set(fileNorm, [...reasons.values()]);
1302
+ return result;
1303
+ }
1304
+ /** Map each blocker file to the set of files that cascade-bail because of it. */
1305
+ function collectCascadeUnblocks(warnings, cwd) {
1306
+ const unblocks = /* @__PURE__ */ new Map();
1307
+ for (const warning of warnings) {
1308
+ if (warning.type !== "styled(ImportedComponent) wraps a component whose file uses styled-components — convert the base component's file first to avoid CSS cascade conflicts") continue;
1309
+ const target = getCascadeDependedFilePath(warning);
1310
+ if (!target) continue;
1311
+ const targetNorm = norm(target, cwd);
1312
+ let consumers = unblocks.get(targetNorm);
1313
+ if (!consumers) {
1314
+ consumers = /* @__PURE__ */ new Set();
1315
+ unblocks.set(targetNorm, consumers);
1316
+ }
1317
+ consumers.add(norm(warning.filePath, cwd));
1318
+ }
1319
+ return unblocks;
1320
+ }
1321
+ /** Build a `consumer -> [{ dep, exportName }]` import graph over all scanned files. */
1322
+ function buildImportGraph(scanFiles, cwd, parserName) {
1323
+ const graph = /* @__PURE__ */ new Map();
1324
+ const resolver = createModuleResolver();
1325
+ const parser = createPrepassParser(parserName);
1326
+ const read = (filePath) => {
1327
+ try {
1328
+ return readFileSync(filePath, "utf-8");
1329
+ } catch {
1330
+ return "";
1331
+ }
1332
+ };
1333
+ const resolveForBarrel = (specifier, fromFile) => resolver.resolve(fromFile, specifier) ?? null;
1334
+ for (const relPath of scanFiles) {
1335
+ const absFrom = resolve(cwd, relPath);
1336
+ const source = read(absFrom);
1337
+ if (!source) continue;
1338
+ const program = parseProgram(parser, source);
1339
+ if (!program) continue;
1340
+ const importNodes = [];
1341
+ walkForImportsAndTemplates(program, importNodes, []);
1342
+ const importMap = buildImportMapFromNodes(importNodes.map(stripTypeOnlyImports));
1343
+ const edges = [];
1344
+ for (const entry of importMap.values()) {
1345
+ const resolved = resolver.resolve(absFrom, entry.source);
1346
+ if (!resolved) continue;
1347
+ const resolvedReal = toRealPath(resolved);
1348
+ const binding = resolveBarrelReExportBinding(resolvedReal, entry.importedName, resolveForBarrel, read);
1349
+ const definitionPath = binding?.filePath ?? resolvedReal;
1350
+ const exportName = binding?.exportedName ?? entry.importedName;
1351
+ edges.push({
1352
+ dep: toRealPath(definitionPath),
1353
+ exportName
1354
+ });
1355
+ }
1356
+ graph.set(toRealPath(absFrom), edges);
1357
+ }
1358
+ return graph;
1359
+ }
1360
+ /** Remove type-only specifiers (or the whole declaration) so only runtime imports remain. */
1361
+ function stripTypeOnlyImports(node) {
1362
+ if (isTypeOnlyImportKind(node.importKind)) return {
1363
+ ...node,
1364
+ specifiers: []
1365
+ };
1366
+ const specifiers = node.specifiers;
1367
+ if (!specifiers) return node;
1368
+ const valueSpecifiers = specifiers.filter((spec) => !isTypeOnlyImportKind(spec.importKind));
1369
+ return valueSpecifiers.length === specifiers.length ? node : {
1370
+ ...node,
1371
+ specifiers: valueSpecifiers
1372
+ };
1373
+ }
1374
+ function isTypeOnlyImportKind(importKind) {
1375
+ return importKind === "type" || importKind === "typeof";
1376
+ }
1377
+ function parseProgram(parser, source) {
1378
+ try {
1379
+ const ast = parser.parse(source);
1380
+ return ast.program ?? ast;
1381
+ } catch {
1382
+ return null;
1383
+ }
1384
+ }
1385
+ /**
1386
+ * Topologically order blockers so that dependencies come before the blockers
1387
+ * that depend on them (bottom-up). Ties are broken by impact (consumer count)
1388
+ * then path for stable output.
1389
+ */
1390
+ function orderBottomUp(blockers, depsByBlocker, weight) {
1391
+ const byImpact = (a, b) => weight(b) - weight(a) || a.localeCompare(b);
1392
+ const seeds = [...blockers].sort(byImpact);
1393
+ const ordered = [];
1394
+ const visited = /* @__PURE__ */ new Set();
1395
+ const visiting = /* @__PURE__ */ new Set();
1396
+ const visit = (blocker) => {
1397
+ if (visited.has(blocker) || visiting.has(blocker)) return;
1398
+ visiting.add(blocker);
1399
+ for (const dep of [...depsByBlocker.get(blocker) ?? []].sort(byImpact)) visit(dep);
1400
+ visiting.delete(blocker);
1401
+ visited.add(blocker);
1402
+ ordered.push(blocker);
1403
+ };
1404
+ for (const seed of seeds) visit(seed);
1405
+ return ordered;
1406
+ }
1407
+ function buildDisplayMap(scanFiles, cwd) {
1408
+ const displayByNorm = /* @__PURE__ */ new Map();
1409
+ for (const relPath of scanFiles) displayByNorm.set(norm(relPath, cwd), relPath);
1410
+ return displayByNorm;
1411
+ }
1412
+ function addConsumer(exportUsage, exportName, consumer) {
1413
+ let consumers = exportUsage.get(exportName);
1414
+ if (!consumers) {
1415
+ consumers = /* @__PURE__ */ new Set();
1416
+ exportUsage.set(exportName, consumers);
1417
+ }
1418
+ consumers.add(consumer);
1419
+ }
1420
+ function norm(filePath, cwd) {
1421
+ return toRealPath(resolve(cwd, filePath));
1422
+ }
1423
+ function unique(values) {
1424
+ return [...new Set(values)];
1425
+ }
1426
+ //#endregion
1427
+ export { analyzeMigrationPlan, defineAdapter, formatMigrationPlan, runTransform };