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

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,10 +1,11 @@
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";
4
- import { r as toRealPath } from "./path-utils-BC4U8X_q.mjs";
1
+ import { t as createModuleResolver } from "./resolve-imports-DgSAddIF.mjs";
2
+ import { $ as describeValue, Q as assertValidAdapterInput, Y as defineAdapter, q as mergeMarkerDeclarations, t as transformedComponentAcceptsSx, x as identifierName } from "./sx-surface-_Hjc6ZDq.mjs";
3
+ import { S as Logger, T as getCascadeDependedFilePath, a as buildImportMapFromNodes, b as resolveBarrelReExportBinding, f as walkForImportsAndTemplates, m as createPrepassParser, t as walkAst, y as resolveBarrelReExport } from "./ast-walk-CLvMH7Lm.mjs";
4
+ import { n as extractStyledDefBases } from "./compute-leaf-set-D5GvkV-H.mjs";
5
+ import { r as toRealPath } from "./path-utils-ByFNVtHo.mjs";
5
6
  import jscodeshift from "jscodeshift";
6
7
  import { fileURLToPath, pathToFileURL } from "node:url";
7
- import { dirname, join, resolve } from "node:path";
8
+ import { dirname, join, relative, resolve } from "node:path";
8
9
  import { existsSync, readFileSync } from "node:fs";
9
10
  import { glob, readFile, writeFile } from "node:fs/promises";
10
11
  import { spawn } from "node:child_process";
@@ -24,7 +25,7 @@ import { spawn } from "node:child_process";
24
25
  * behavior so downstream metadata lookups still have something to try).
25
26
  */
26
27
  function resolveStaticMemberComponentNames(source, rootNames, memberPath, parserName = "tsx") {
27
- const program = parseProgram(source, parserName);
28
+ const program = parseProgram$1(source, parserName);
28
29
  const fallbackMember = memberPath[memberPath.length - 1];
29
30
  const fallback = fallbackMember ? [fallbackMember] : [];
30
31
  if (!program) return [...new Set([...rootNames, ...fallback])];
@@ -39,7 +40,7 @@ function resolveStaticMemberComponentNames(source, rootNames, memberPath, parser
39
40
  return [...new Set([...owners, ...fallback])];
40
41
  }
41
42
  const programCache = /* @__PURE__ */ new Map();
42
- function parseProgram(source, parserName) {
43
+ function parseProgram$1(source, parserName) {
43
44
  const cached = programCache.get(source);
44
45
  if (cached !== void 0) return cached;
45
46
  const parsed = tryParse(source, parserName);
@@ -167,6 +168,12 @@ function capitalizedIdentifierName(node) {
167
168
  * Core concepts: jscodeshift execution, globs, and adapter hooks.
168
169
  */
169
170
  const __dirname = dirname(fileURLToPath(import.meta.url));
171
+ /** Expand glob pattern(s) into file paths relative to `cwd`. */
172
+ async function expandGlobFiles(patterns, cwd) {
173
+ const filePaths = [];
174
+ for (const pattern of patterns) for await (const file of glob(pattern, { cwd })) filePaths.push(file);
175
+ return filePaths;
176
+ }
170
177
  /**
171
178
  * Run the styled-components to StyleX transform on files matching the glob pattern.
172
179
  *
@@ -248,6 +255,7 @@ async function runTransform(options) {
248
255
  if (maxExamples !== void 0) Logger.setMaxExamples(maxExamples);
249
256
  const adapterInput = options.adapter;
250
257
  assertValidAdapterInput(adapterInput, "runTransform(options)");
258
+ 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
259
  if (adapterInput.externalInterface === "auto" && consumerPathsOption === null) throw new Error([
252
260
  "runTransform(options): externalInterface is \"auto\" but consumerPaths is null.",
253
261
  "Auto-detection needs consumer file globs to scan for styled(Component) and as-prop usage.",
@@ -296,9 +304,8 @@ async function runTransform(options) {
296
304
  }
297
305
  };
298
306
  const patterns = Array.isArray(files) ? files : [files];
299
- let filePaths = [];
300
307
  const cwd = process.cwd();
301
- for (const pattern of patterns) for await (const file of glob(pattern, { cwd })) filePaths.push(file);
308
+ let filePaths = await expandGlobFiles(patterns, cwd);
302
309
  if (filePaths.length === 0) {
303
310
  Logger.warn("No files matched the provided glob pattern(s)");
304
311
  return {
@@ -307,13 +314,13 @@ async function runTransform(options) {
307
314
  skipped: 0,
308
315
  transformed: 0,
309
316
  timeElapsed: 0,
310
- warnings: []
317
+ warnings: [],
318
+ fileResults: []
311
319
  };
312
320
  }
313
321
  Logger.setFileCount(filePaths.length);
314
322
  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);
323
+ const consumerFilePaths = await expandGlobFiles(consumerPatterns, cwd);
317
324
  if (consumerPatterns.length > 0 && consumerFilePaths.length === 0) throw new Error([
318
325
  "runTransform(options): consumerPaths matched no files.",
319
326
  `Pattern(s): ${consumerPatterns.join(", ")}`,
@@ -322,7 +329,7 @@ async function runTransform(options) {
322
329
  const { createModuleResolver } = await import("./resolve-imports-DgSAddIF.mjs").then((n) => n.n);
323
330
  const sharedResolver = createModuleResolver();
324
331
  filePaths = orderFilesByLocalImportDependencies(filePaths, sharedResolver, toRealPath);
325
- const { runPrepass } = await import("./run-prepass-D3Ti1ryc.mjs");
332
+ const { runPrepass } = await import("./run-prepass-BueJvYyf.mjs");
326
333
  const absoluteFiles = filePaths.map((f) => resolve(f));
327
334
  const absoluteConsumers = consumerFilePaths.map((f) => resolve(f));
328
335
  let prepassResult;
@@ -360,6 +367,21 @@ async function runTransform(options) {
360
367
  const transformedFiles = /* @__PURE__ */ new Set();
361
368
  const transformedComponents = /* @__PURE__ */ new Map();
362
369
  const transformedFileSources = /* @__PURE__ */ new Map();
370
+ const seedParser = createPrepassParser(parser);
371
+ for (const assumedFile of options.assumeConvertedFiles ?? []) {
372
+ const realPath = toRealPath(resolve(assumedFile));
373
+ transformedFiles.add(realPath);
374
+ if (!transformedComponents.has(realPath)) {
375
+ const extracted = /* @__PURE__ */ new Map();
376
+ let source = "";
377
+ try {
378
+ source = readFileSync(realPath, "utf-8");
379
+ extractStyledDefBases(realPath, source, seedParser, extracted);
380
+ } catch {}
381
+ transformedComponents.set(realPath, new Set(extracted.get(realPath)?.keys() ?? []));
382
+ transformedFileSources.set(realPath, source);
383
+ }
384
+ }
363
385
  const crossFilePrepassResult = {
364
386
  ...prepassResult.crossFileInfo,
365
387
  transformedFiles,
@@ -400,7 +422,7 @@ async function runTransform(options) {
400
422
  const cached = styledDefinitionNamesByFile.get(realPath);
401
423
  if (cached) return cached;
402
424
  const extracted = /* @__PURE__ */ new Map();
403
- extractStyledDefBasesFromSource(realPath, cachedRead(realPath), extracted);
425
+ extractStyledDefBases(realPath, cachedRead(realPath), seedParser, extracted);
404
426
  const names = new Set(extracted.get(realPath)?.keys() ?? []);
405
427
  styledDefinitionNamesByFile.set(realPath, names);
406
428
  return names;
@@ -548,7 +570,7 @@ async function runTransform(options) {
548
570
  const result = await runTransformSequentially(transformModule, filePaths, runnerOptions);
549
571
  if (sidecarFiles.size > 0 && !dryRun) for (const [sidecarPath, content] of sidecarFiles) await writeFile(sidecarPath, mergeSidecarContent(sidecarPath, content), "utf-8");
550
572
  if (bridgeResults.size > 0 && !dryRun) {
551
- const { buildConsumerReplacements, patchConsumerFile } = await import("./bridge-consumer-patcher-DDcYZM_G.mjs");
573
+ const { buildConsumerReplacements, patchConsumerFile } = await import("./bridge-consumer-patcher-B__X3jOg.mjs");
552
574
  const consumerReplacements = buildConsumerReplacements(crossFilePrepassResult.selectorUsages, bridgeResults, transformedFiles);
553
575
  const patchedFiles = [];
554
576
  for (const [consumerPath, replacements] of consumerReplacements) {
@@ -561,7 +583,7 @@ async function runTransform(options) {
561
583
  if (formatterCommands && patchedFiles.length > 0) await runFormatters(formatterCommands, patchedFiles);
562
584
  }
563
585
  if (prepassResult.forwardedAsConsumers.size > 0 && !dryRun) {
564
- const { buildForwardedAsReplacements, patchConsumerForwardedAs } = await import("./forwarded-as-consumer-patcher-Bva_36Gy.mjs");
586
+ const { buildForwardedAsReplacements, patchConsumerForwardedAs } = await import("./forwarded-as-consumer-patcher-Bs9ymhBa.mjs");
565
587
  const forwardedAsReplacements = buildForwardedAsReplacements(prepassResult.forwardedAsConsumers, transformedFiles);
566
588
  const patchedFiles = [];
567
589
  for (const [consumerPath, entries] of forwardedAsReplacements) {
@@ -574,7 +596,7 @@ async function runTransform(options) {
574
596
  if (formatterCommands && patchedFiles.length > 0) await runFormatters(formatterCommands, patchedFiles);
575
597
  }
576
598
  if (transientPropRenames.size > 0 && !dryRun) {
577
- const { collectTransientPropPatches } = await import("./transient-prop-consumer-patcher-DSd7uVA6.mjs");
599
+ const { collectTransientPropPatches } = await import("./transient-prop-consumer-patcher-BDruM1OI.mjs");
578
600
  const patches = collectTransientPropPatches({
579
601
  transientPropRenames,
580
602
  consumerFilePaths: consumerFilePaths.map((p) => resolve(p)),
@@ -589,7 +611,7 @@ async function runTransform(options) {
589
611
  }
590
612
  if (formatterCommands && formatterCommands.length > 0 && result.ok > 0 && !dryRun) await runFormatters(formatterCommands, filePaths);
591
613
  const report = Logger.createReport();
592
- report.print();
614
+ if (!(options.silent ?? false)) report.print();
593
615
  return {
594
616
  errors: result.error,
595
617
  unchanged: result.nochange,
@@ -597,6 +619,7 @@ async function runTransform(options) {
597
619
  transformed: result.ok,
598
620
  timeElapsed: parseFloat(result.timeElapsed) || 0,
599
621
  warnings: report.getWarnings(),
622
+ fileResults: result.files,
600
623
  standaloneFileResults: standaloneResult?.files,
601
624
  standaloneWarnings
602
625
  };
@@ -837,4 +860,416 @@ async function runFormatters(commands, files) {
837
860
  }
838
861
  }
839
862
  //#endregion
840
- export { defineAdapter, runTransform };
863
+ //#region src/migration-plan.ts
864
+ /**
865
+ * Analysis-only mode: produce an ordered plan of the files that must be
866
+ * converted by hand before the codemod can finish the rest of the migration.
867
+ *
868
+ * Core concepts: genuine blocker detection (files the codemod truly cannot
869
+ * convert, as opposed to files that only bail because a dependency is still
870
+ * styled-components), bottom-up dependency ordering, and consumer/imported-export
871
+ * accounting so each blocker is presented with the impact of converting it.
872
+ */
873
+ /**
874
+ * Run the codemod in analysis-only (dry) mode and compute the ordered list of
875
+ * files that block the rest of the migration and must be converted manually.
876
+ */
877
+ async function analyzeMigrationPlan(options) {
878
+ const cwd = process.cwd();
879
+ const filePatterns = Array.isArray(options.files) ? options.files : [options.files];
880
+ const consumerPatterns = options.consumerPaths === null ? [] : Array.isArray(options.consumerPaths) ? options.consumerPaths : [options.consumerPaths];
881
+ const runFiles = await expandGlobFiles(filePatterns, cwd);
882
+ const scanFiles = unique([...runFiles, ...await expandGlobFiles(consumerPatterns, cwd)]);
883
+ const parser = options.parser ?? "tsx";
884
+ const runFilesNorm = new Set(runFiles.map((file) => norm(file, cwd)));
885
+ const cascadeUnblocks = collectCascadeUnblocks((await runAnalysisPass(options, parser, [])).warnings, cwd);
886
+ const externalBlockerSet = new Set([...cascadeUnblocks.keys()].filter((target) => !runFilesNorm.has(target)));
887
+ const maxPasses = options.maxAnalysisPasses ?? MAX_ANALYSIS_PASSES;
888
+ const assumedConverted = new Set(externalBlockerSet);
889
+ let blockerReasons = /* @__PURE__ */ new Map();
890
+ let stabilized = false;
891
+ for (let pass = 0; pass < maxPasses; pass++) {
892
+ const passResult = await runAnalysisPass(options, parser, [...assumedConverted]);
893
+ blockerReasons = collectGenuineBlockers(passResult.warnings, passResult.fileResults, cwd);
894
+ const newlyFound = [...blockerReasons.keys()].filter((file) => !assumedConverted.has(file));
895
+ if (newlyFound.length === 0) {
896
+ stabilized = true;
897
+ break;
898
+ }
899
+ for (const file of newlyFound) assumedConverted.add(file);
900
+ }
901
+ 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.`);
902
+ const reachableExternalBlockers = new Set([...externalBlockerSet].filter((target) => (cascadeUnblocks.get(target)?.size ?? 0) > 0));
903
+ if (blockerReasons.size === 0 && reachableExternalBlockers.size === 0) return {
904
+ manualConversionFiles: [],
905
+ totalFiles: runFiles.length,
906
+ unlocksFileCount: 0
907
+ };
908
+ const graph = buildImportGraph(scanFiles, cwd, parser);
909
+ const displayByNorm = buildDisplayMap(scanFiles, cwd);
910
+ const blockerSet = new Set([...blockerReasons.keys(), ...reachableExternalBlockers]);
911
+ const reasonsByBlocker = new Map(blockerReasons);
912
+ for (const target of reachableExternalBlockers) reasonsByBlocker.set(target, [{
913
+ message: EXTERNAL_BLOCKER_REASON,
914
+ locations: []
915
+ }]);
916
+ const { consumersByBlocker, depsByBlocker, consumerCountByBlocker, blockedCountByBlocker, soleBlockerCountByBlocker, weightByBlocker } = buildBlockerGraph(blockerSet, graph, cascadeUnblocks);
917
+ const manualConversionFiles = orderBottomUp([...blockerSet], depsByBlocker, (blocker) => weightByBlocker.get(blocker) ?? 0).map((blocker, index) => {
918
+ const importedExports = [...(consumersByBlocker.get(blocker) ?? /* @__PURE__ */ new Map()).entries()].map(([exportName, consumers]) => ({
919
+ exportName,
920
+ consumerCount: consumers.size
921
+ })).sort((a, b) => b.consumerCount - a.consumerCount || a.exportName.localeCompare(b.exportName));
922
+ const dependsOn = [...depsByBlocker.get(blocker) ?? []].map((dep) => displayByNorm.get(dep) ?? relative(cwd, dep)).sort();
923
+ return {
924
+ filePath: displayByNorm.get(blocker) ?? relative(cwd, blocker),
925
+ order: index + 1,
926
+ consumerCount: consumerCountByBlocker.get(blocker) ?? 0,
927
+ soleBlockerFileCount: soleBlockerCountByBlocker.get(blocker) ?? 0,
928
+ blockedFileCount: blockedCountByBlocker.get(blocker) ?? 0,
929
+ importedExports,
930
+ reasons: reasonsByBlocker.get(blocker) ?? [],
931
+ dependsOn
932
+ };
933
+ });
934
+ const unlockedFiles = /* @__PURE__ */ new Set();
935
+ for (const consumers of cascadeUnblocks.values()) for (const consumer of consumers) if (!blockerSet.has(consumer)) unlockedFiles.add(consumer);
936
+ return {
937
+ manualConversionFiles,
938
+ totalFiles: runFiles.length,
939
+ unlocksFileCount: unlockedFiles.size
940
+ };
941
+ }
942
+ /** Render a {@link MigrationPlan} as a human-readable, actionable report. */
943
+ function formatMigrationPlan(plan) {
944
+ const { manualConversionFiles, totalFiles, unlocksFileCount } = plan;
945
+ if (manualConversionFiles.length === 0) return `No manual conversion needed — the codemod can convert all ${totalFiles} file(s) in dependency order.`;
946
+ const focusPaths = collectFocusPaths(manualConversionFiles);
947
+ const priority = manualConversionFiles.filter((file) => focusPaths.has(file.filePath));
948
+ const standalone = manualConversionFiles.filter((file) => !focusPaths.has(file.filePath));
949
+ const lines = [];
950
+ lines.push("Manual conversion plan");
951
+ lines.push("======================");
952
+ lines.push(`${manualConversionFiles.length} of ${totalFiles} file(s) need manual conversion.`);
953
+ if (unlocksFileCount > 0) lines.push(`Focus on the ${priority.length} file(s) below — converting them unblocks ${unlocksFileCount} file(s) for automatic migration.`);
954
+ lines.push("");
955
+ if (priority.length > 0) {
956
+ const positionByPath = new Map(priority.map((file, index) => [file.filePath, index + 1]));
957
+ lines.push("Convert in this order (dependencies first):");
958
+ lines.push("");
959
+ priority.forEach((file, index) => appendFileEntry(lines, file, index + 1, positionByPath));
960
+ }
961
+ if (standalone.length > 0) {
962
+ lines.push(`Standalone file(s) — nothing else in the plan depends on these; convert as you reach them (${standalone.length}):`);
963
+ lines.push("");
964
+ appendStandaloneSummary(lines, standalone);
965
+ }
966
+ return lines.join("\n").trimEnd();
967
+ }
968
+ /**
969
+ * Run one dry analysis pass and return only the warnings it produced. `Logger`
970
+ * is process-global, so snapshot the pre-existing warnings, keep only this run's,
971
+ * and restore the snapshot afterward so analysis never leaks blocker warnings
972
+ * into a later transform in the same process.
973
+ */
974
+ async function runAnalysisPass(options, parser, assumeConvertedFiles) {
975
+ const snapshot = Logger.createReport().getWarnings();
976
+ const priorWarnings = new Set(snapshot);
977
+ let result;
978
+ try {
979
+ result = await runTransform({
980
+ files: options.files,
981
+ consumerPaths: options.consumerPaths,
982
+ adapter: options.adapter,
983
+ parser,
984
+ dryRun: true,
985
+ silent: true,
986
+ assumeConvertedFiles
987
+ });
988
+ } finally {
989
+ Logger.restoreWarnings(snapshot);
990
+ }
991
+ return {
992
+ warnings: result.warnings.filter((warning) => !priorWarnings.has(warning)),
993
+ fileResults: result.fileResults
994
+ };
995
+ }
996
+ /** Attribute consumers, in-plan dependencies, and impact weights to each blocker. */
997
+ function buildBlockerGraph(blockerSet, graph, cascadeUnblocks) {
998
+ const consumersByBlocker = /* @__PURE__ */ new Map();
999
+ const depsByBlocker = /* @__PURE__ */ new Map();
1000
+ for (const blocker of blockerSet) {
1001
+ consumersByBlocker.set(blocker, /* @__PURE__ */ new Map());
1002
+ depsByBlocker.set(blocker, /* @__PURE__ */ new Set());
1003
+ }
1004
+ for (const [consumer, edges] of graph) for (const edge of edges) {
1005
+ if (!blockerSet.has(edge.dep) || edge.dep === consumer) continue;
1006
+ addConsumer(consumersByBlocker.get(edge.dep), edge.exportName, consumer);
1007
+ if (blockerSet.has(consumer)) depsByBlocker.get(consumer).add(edge.dep);
1008
+ }
1009
+ const blockersByConsumer = /* @__PURE__ */ new Map();
1010
+ for (const [blocker, consumers] of cascadeUnblocks) for (const consumer of consumers) {
1011
+ const set = blockersByConsumer.get(consumer) ?? /* @__PURE__ */ new Set();
1012
+ set.add(blocker);
1013
+ blockersByConsumer.set(consumer, set);
1014
+ }
1015
+ const consumerCountByBlocker = /* @__PURE__ */ new Map();
1016
+ const blockedCountByBlocker = /* @__PURE__ */ new Map();
1017
+ const soleBlockerCountByBlocker = /* @__PURE__ */ new Map();
1018
+ const weightByBlocker = /* @__PURE__ */ new Map();
1019
+ for (const blocker of blockerSet) {
1020
+ const importers = /* @__PURE__ */ new Set();
1021
+ for (const set of consumersByBlocker.get(blocker).values()) for (const consumer of set) importers.add(consumer);
1022
+ const blocked = [...cascadeUnblocks.get(blocker) ?? []];
1023
+ const soleBlocked = blocked.filter((consumer) => !blockerSet.has(consumer) && (blockersByConsumer.get(consumer)?.size ?? 0) === 1);
1024
+ consumerCountByBlocker.set(blocker, importers.size);
1025
+ blockedCountByBlocker.set(blocker, blocked.length);
1026
+ soleBlockerCountByBlocker.set(blocker, soleBlocked.length);
1027
+ weightByBlocker.set(blocker, soleBlocked.length * 1e6 + blocked.length);
1028
+ }
1029
+ return {
1030
+ consumersByBlocker,
1031
+ depsByBlocker,
1032
+ consumerCountByBlocker,
1033
+ blockedCountByBlocker,
1034
+ soleBlockerCountByBlocker,
1035
+ weightByBlocker
1036
+ };
1037
+ }
1038
+ /**
1039
+ * Files to surface in the ordered focus list: any file that unblocks automatic
1040
+ * migration, plus any file involved in an in-plan dependency relationship (it
1041
+ * depends on another listed file, or another listed file depends on it). Only
1042
+ * fully isolated blockers fall through to the standalone summary, so the ordered
1043
+ * list never loses a real dependency chain — even when nothing unlocks a wrapper.
1044
+ */
1045
+ function collectFocusPaths(files) {
1046
+ const dependedUpon = /* @__PURE__ */ new Set();
1047
+ for (const file of files) for (const dependency of file.dependsOn) dependedUpon.add(dependency);
1048
+ const focus = /* @__PURE__ */ new Set();
1049
+ for (const file of files) if (file.blockedFileCount > 0 || file.dependsOn.length > 0 || dependedUpon.has(file.filePath)) focus.add(file.filePath);
1050
+ return focus;
1051
+ }
1052
+ function appendFileEntry(lines, file, position, positionByPath) {
1053
+ lines.push(`${position}. ${file.filePath}`);
1054
+ const impact = [];
1055
+ if (file.soleBlockerFileCount > 0) impact.push(`sole blocker for ${file.soleBlockerFileCount} file(s)`);
1056
+ if (file.blockedFileCount > 0) impact.push(`in blocker chain for ${file.blockedFileCount} file(s)`);
1057
+ if (file.consumerCount > 0) impact.push(`imported by ${file.consumerCount} file(s)`);
1058
+ if (impact.length > 0) lines.push(` → ${impact.join(" · ")}`);
1059
+ if (file.dependsOn.length > 0) {
1060
+ const deps = file.dependsOn.map((dep) => {
1061
+ const depPosition = positionByPath.get(dep);
1062
+ return depPosition === void 0 ? dep : `#${depPosition} ${dep}`;
1063
+ }).sort();
1064
+ lines.push(` Requires first: ${deps.join(", ")}`);
1065
+ }
1066
+ if (file.importedExports.length > 0) {
1067
+ const exportList = file.importedExports.map((usage) => `${formatExportName(usage.exportName)} (used by ${usage.consumerCount})`).join(", ");
1068
+ lines.push(` Convert these exports: ${exportList}`);
1069
+ }
1070
+ lines.push(" Blocked by:");
1071
+ for (const reason of file.reasons) {
1072
+ lines.push(` • ${reason.message}`);
1073
+ for (const loc of reason.locations.slice(0, MAX_REASON_LOCATIONS)) lines.push(` ${loc.filePath}:${loc.line}:${loc.column}`);
1074
+ const remaining = reason.locations.length - MAX_REASON_LOCATIONS;
1075
+ if (remaining > 0) lines.push(` ... and ${remaining} more location(s)`);
1076
+ }
1077
+ lines.push("");
1078
+ }
1079
+ /** Group standalone blockers by reason so the long tail stays scannable. */
1080
+ function appendStandaloneSummary(lines, standalone) {
1081
+ const filesByReason = /* @__PURE__ */ new Map();
1082
+ for (const file of standalone) {
1083
+ const reasonMessage = file.reasons[0]?.message ?? "Unsupported pattern";
1084
+ const files = filesByReason.get(reasonMessage) ?? [];
1085
+ files.push(file.filePath);
1086
+ filesByReason.set(reasonMessage, files);
1087
+ }
1088
+ const grouped = [...filesByReason.entries()].sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]));
1089
+ for (const [reasonMessage, files] of grouped) {
1090
+ lines.push(` • ${reasonMessage} (${files.length} file(s))`);
1091
+ for (const filePath of files.slice(0, MAX_STANDALONE_FILES_PER_REASON)) lines.push(` ${filePath}`);
1092
+ const remaining = files.length - MAX_STANDALONE_FILES_PER_REASON;
1093
+ if (remaining > 0) lines.push(` ... and ${remaining} more file(s)`);
1094
+ }
1095
+ lines.push("");
1096
+ }
1097
+ function formatExportName(exportName) {
1098
+ if (exportName === "default") return "default export";
1099
+ if (exportName === "*") return "* (namespace import)";
1100
+ return exportName;
1101
+ }
1102
+ const MAX_REASON_LOCATIONS = 3;
1103
+ const MAX_STANDALONE_FILES_PER_REASON = 5;
1104
+ 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";
1105
+ /** Safety cap on fixpoint passes; each pass reveals at least one new blocker until stable. */
1106
+ const MAX_ANALYSIS_PASSES = 50;
1107
+ /**
1108
+ * A file is a genuine blocker when the codemod did not convert it and the reason
1109
+ * is something other than a dependency-order cascade conflict (which resolves on
1110
+ * its own once the depended-on file is converted), or when it threw outright.
1111
+ */
1112
+ function collectGenuineBlockers(warnings, fileResults, cwd) {
1113
+ const reasonsByFile = /* @__PURE__ */ new Map();
1114
+ const ensureReason = (fileNorm, message) => {
1115
+ let reasons = reasonsByFile.get(fileNorm);
1116
+ if (!reasons) {
1117
+ reasons = /* @__PURE__ */ new Map();
1118
+ reasonsByFile.set(fileNorm, reasons);
1119
+ }
1120
+ let reason = reasons.get(message);
1121
+ if (!reason) {
1122
+ reason = {
1123
+ message,
1124
+ locations: []
1125
+ };
1126
+ reasons.set(message, reason);
1127
+ }
1128
+ return reason;
1129
+ };
1130
+ const unconverted = /* @__PURE__ */ new Set();
1131
+ const erroredFiles = /* @__PURE__ */ new Set();
1132
+ for (const fileResult of fileResults) {
1133
+ if (fileResult.status === "skipped" || fileResult.status === "error") unconverted.add(norm(fileResult.filePath, cwd));
1134
+ if (fileResult.status === "error") erroredFiles.add(norm(fileResult.filePath, cwd));
1135
+ }
1136
+ for (const warning of warnings) {
1137
+ 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;
1138
+ const fileNorm = norm(warning.filePath, cwd);
1139
+ if (!unconverted.has(fileNorm)) continue;
1140
+ const reason = ensureReason(fileNorm, warning.type);
1141
+ if (warning.loc) reason.locations.push({
1142
+ filePath: warning.filePath,
1143
+ line: warning.loc.line,
1144
+ column: warning.loc.column
1145
+ });
1146
+ }
1147
+ for (const fileNorm of erroredFiles) ensureReason(fileNorm, "The codemod threw an error while transforming this file");
1148
+ const result = /* @__PURE__ */ new Map();
1149
+ for (const [fileNorm, reasons] of reasonsByFile) result.set(fileNorm, [...reasons.values()]);
1150
+ return result;
1151
+ }
1152
+ /** Map each blocker file to the set of files that cascade-bail because of it. */
1153
+ function collectCascadeUnblocks(warnings, cwd) {
1154
+ const unblocks = /* @__PURE__ */ new Map();
1155
+ for (const warning of warnings) {
1156
+ 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;
1157
+ const target = getCascadeDependedFilePath(warning);
1158
+ if (!target) continue;
1159
+ const targetNorm = norm(target, cwd);
1160
+ let consumers = unblocks.get(targetNorm);
1161
+ if (!consumers) {
1162
+ consumers = /* @__PURE__ */ new Set();
1163
+ unblocks.set(targetNorm, consumers);
1164
+ }
1165
+ consumers.add(norm(warning.filePath, cwd));
1166
+ }
1167
+ return unblocks;
1168
+ }
1169
+ /** Build a `consumer -> [{ dep, exportName }]` import graph over all scanned files. */
1170
+ function buildImportGraph(scanFiles, cwd, parserName) {
1171
+ const graph = /* @__PURE__ */ new Map();
1172
+ const resolver = createModuleResolver();
1173
+ const parser = createPrepassParser(parserName);
1174
+ const read = (filePath) => {
1175
+ try {
1176
+ return readFileSync(filePath, "utf-8");
1177
+ } catch {
1178
+ return "";
1179
+ }
1180
+ };
1181
+ const resolveForBarrel = (specifier, fromFile) => resolver.resolve(fromFile, specifier) ?? null;
1182
+ for (const relPath of scanFiles) {
1183
+ const absFrom = resolve(cwd, relPath);
1184
+ const source = read(absFrom);
1185
+ if (!source) continue;
1186
+ const program = parseProgram(parser, source);
1187
+ if (!program) continue;
1188
+ const importNodes = [];
1189
+ walkForImportsAndTemplates(program, importNodes, []);
1190
+ const importMap = buildImportMapFromNodes(importNodes.map(stripTypeOnlyImports));
1191
+ const edges = [];
1192
+ for (const entry of importMap.values()) {
1193
+ const resolved = resolver.resolve(absFrom, entry.source);
1194
+ if (!resolved) continue;
1195
+ const resolvedReal = toRealPath(resolved);
1196
+ const binding = resolveBarrelReExportBinding(resolvedReal, entry.importedName, resolveForBarrel, read);
1197
+ const definitionPath = binding?.filePath ?? resolvedReal;
1198
+ const exportName = binding?.exportedName ?? entry.importedName;
1199
+ edges.push({
1200
+ dep: toRealPath(definitionPath),
1201
+ exportName
1202
+ });
1203
+ }
1204
+ graph.set(toRealPath(absFrom), edges);
1205
+ }
1206
+ return graph;
1207
+ }
1208
+ /** Remove type-only specifiers (or the whole declaration) so only runtime imports remain. */
1209
+ function stripTypeOnlyImports(node) {
1210
+ if (isTypeOnlyImportKind(node.importKind)) return {
1211
+ ...node,
1212
+ specifiers: []
1213
+ };
1214
+ const specifiers = node.specifiers;
1215
+ if (!specifiers) return node;
1216
+ const valueSpecifiers = specifiers.filter((spec) => !isTypeOnlyImportKind(spec.importKind));
1217
+ return valueSpecifiers.length === specifiers.length ? node : {
1218
+ ...node,
1219
+ specifiers: valueSpecifiers
1220
+ };
1221
+ }
1222
+ function isTypeOnlyImportKind(importKind) {
1223
+ return importKind === "type" || importKind === "typeof";
1224
+ }
1225
+ function parseProgram(parser, source) {
1226
+ try {
1227
+ const ast = parser.parse(source);
1228
+ return ast.program ?? ast;
1229
+ } catch {
1230
+ return null;
1231
+ }
1232
+ }
1233
+ /**
1234
+ * Topologically order blockers so that dependencies come before the blockers
1235
+ * that depend on them (bottom-up). Ties are broken by impact (consumer count)
1236
+ * then path for stable output.
1237
+ */
1238
+ function orderBottomUp(blockers, depsByBlocker, weight) {
1239
+ const byImpact = (a, b) => weight(b) - weight(a) || a.localeCompare(b);
1240
+ const seeds = [...blockers].sort(byImpact);
1241
+ const ordered = [];
1242
+ const visited = /* @__PURE__ */ new Set();
1243
+ const visiting = /* @__PURE__ */ new Set();
1244
+ const visit = (blocker) => {
1245
+ if (visited.has(blocker) || visiting.has(blocker)) return;
1246
+ visiting.add(blocker);
1247
+ for (const dep of [...depsByBlocker.get(blocker) ?? []].sort(byImpact)) visit(dep);
1248
+ visiting.delete(blocker);
1249
+ visited.add(blocker);
1250
+ ordered.push(blocker);
1251
+ };
1252
+ for (const seed of seeds) visit(seed);
1253
+ return ordered;
1254
+ }
1255
+ function buildDisplayMap(scanFiles, cwd) {
1256
+ const displayByNorm = /* @__PURE__ */ new Map();
1257
+ for (const relPath of scanFiles) displayByNorm.set(norm(relPath, cwd), relPath);
1258
+ return displayByNorm;
1259
+ }
1260
+ function addConsumer(exportUsage, exportName, consumer) {
1261
+ let consumers = exportUsage.get(exportName);
1262
+ if (!consumers) {
1263
+ consumers = /* @__PURE__ */ new Set();
1264
+ exportUsage.set(exportName, consumers);
1265
+ }
1266
+ consumers.add(consumer);
1267
+ }
1268
+ function norm(filePath, cwd) {
1269
+ return toRealPath(resolve(cwd, filePath));
1270
+ }
1271
+ function unique(values) {
1272
+ return [...new Set(values)];
1273
+ }
1274
+ //#endregion
1275
+ export { analyzeMigrationPlan, defineAdapter, formatMigrationPlan, runTransform };