styled-components-to-stylex-codemod 0.0.54 → 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/README.md CHANGED
@@ -37,6 +37,8 @@ The adapter maps your project's `props.theme.*` access, CSS variables, and helpe
37
37
 
38
38
  When a component wraps another component that internally uses styled-components (e.g. `styled(GroupHeader)` where `GroupHeader` renders a `StyledHeader`), CSS cascade conflicts can arise after migration. Convert leaf files — the ones that don't wrap other styled-components — first, then work your way up. The codemod will bail with a warning if it detects this pattern.
39
39
 
40
+ Run [`analyzeMigrationPlan`](#planning-manual-conversions) to get the ordered, bottom-up list of files to convert by hand first.
41
+
40
42
  ### 4. Verify, iterate, clean up
41
43
 
42
44
  Build and test your project. Review warnings — they tell you which files were skipped and why. Fix adapter gaps, re-run on remaining files, and repeat until done. [Report issues](https://github.com/skovhus/styled-components-to-stylex-codemod/issues) with input/output examples if the codemod produces incorrect results.
@@ -380,6 +382,24 @@ await runTransform({
380
382
 
381
383
  </details>
382
384
 
385
+ ### Planning manual conversions
386
+
387
+ `analyzeMigrationPlan` runs the codemod in analysis-only mode (it never writes files) and returns the bottom-up ordered list of files you must convert by hand — the genuine blockers the codemod can't convert — each with its consumer count, the exports to convert, how many files it unblocks, and the bail reasons. `formatMigrationPlan` renders it as a report (high-impact files first).
388
+
389
+ ```ts
390
+ import { analyzeMigrationPlan, formatMigrationPlan } from "styled-components-to-stylex-codemod";
391
+
392
+ const plan = await analyzeMigrationPlan({
393
+ files: "src/**/*.tsx",
394
+ consumerPaths: "src/**/*.tsx",
395
+ adapter, // the same adapter you pass to runTransform
396
+ });
397
+
398
+ console.log(formatMigrationPlan(plan));
399
+ ```
400
+
401
+ Try it against this repo's own fixtures with `node scripts/migration-plan.mts`.
402
+
383
403
  ### Adapter
384
404
 
385
405
  Adapters are the main extension point, see full example above. They let you control:
@@ -1,4 +1,5 @@
1
- import path from "node:path";
1
+ import { n as isTemplatePlaceholderInSelectorContext, r as PLACEHOLDER_RE } from "./selector-context-heuristic-Dptd93Xe.mjs";
2
+ import path, { resolve } from "node:path";
2
3
  import { readFileSync } from "node:fs";
3
4
  import { parse } from "@babel/parser";
4
5
  //#region src/internal/logger.ts
@@ -89,6 +90,14 @@ var Logger = class Logger {
89
90
  static createReport() {
90
91
  return new LoggerReport([...Logger.collected], Logger.fileCount, Logger.maxExamples);
91
92
  }
93
+ /**
94
+ * Restore the collected warnings to a previous snapshot (from
95
+ * `createReport().getWarnings()`). Used to undo the side effects of an
96
+ * analysis-only dry run on the process-global logger.
97
+ */
98
+ static restoreWarnings(warnings) {
99
+ Logger.collected = [...warnings];
100
+ }
92
101
  /** @internal - for testing only */
93
102
  static _clearCollected() {
94
103
  Logger.collected = [];
@@ -273,14 +282,16 @@ var LoggerReport = class {
273
282
  }
274
283
  }
275
284
  };
285
+ /**
286
+ * Extract the depended-on file path from a cascade-conflict warning's context
287
+ * (the base component's defining file that must be converted first). Owned here
288
+ * alongside the warning-type definition so consumers share one invariant.
289
+ */
276
290
  function getCascadeDependedFilePath(warning) {
277
291
  const context = warning.context;
278
- if (!context || typeof context !== "object") return;
279
- const record = context;
280
- const definitionPath = record.definitionPath;
281
- if (typeof definitionPath === "string") return definitionPath;
282
- const importedPath = record.importedPath;
283
- return typeof importedPath === "string" ? importedPath : void 0;
292
+ if (!context) return;
293
+ if (typeof context.definitionPath === "string") return context.definitionPath;
294
+ return typeof context.importedPath === "string" ? context.importedPath : void 0;
284
295
  }
285
296
  function uniqueSorted(values) {
286
297
  return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
@@ -522,6 +533,300 @@ const FLOW_PLUGINS = [
522
533
  "throwExpressions"
523
534
  ];
524
535
  //#endregion
536
+ //#region src/internal/utilities/collection-utils.ts
537
+ /** Add a value to a Set stored in a Map, creating the Set if it doesn't exist. */
538
+ function addToSetMap(map, key, value) {
539
+ let set = map.get(key);
540
+ if (!set) {
541
+ set = /* @__PURE__ */ new Set();
542
+ map.set(key, set);
543
+ }
544
+ set.add(value);
545
+ }
546
+ //#endregion
547
+ //#region src/internal/prepass/scan-cross-file-selectors.ts
548
+ /**
549
+ * Pre-filter: matches any bare `${Identifier}` template expression.
550
+ * Used to skip files that only contain arrow functions or member expressions
551
+ * in template literals (e.g. `${props => ...}`, `${theme.color}`).
552
+ */
553
+ const BARE_TEMPLATE_IDENTIFIER_RE = /\$\{\s*[a-zA-Z_$][\w$]*\s*\}/;
554
+ /**
555
+ * Categorize cross-file selector usages into marker sidecar and global selector bridge maps.
556
+ *
557
+ * Bridge usages (from already-converted files) are skipped — the consumer handles marker
558
+ * generation via the forward selector handler, so no sidecar/bridge is needed on the target.
559
+ */
560
+ function categorizeSelectorUsages(usages, componentsNeedingMarkerSidecar, componentsNeedingGlobalSelectorBridge) {
561
+ for (const usage of usages) {
562
+ if (usage.bridgeComponentName) continue;
563
+ if (usage.consumerIsTransformed) addToSetMap(componentsNeedingMarkerSidecar, usage.resolvedPath, usage.importedName);
564
+ addToSetMap(componentsNeedingGlobalSelectorBridge, usage.resolvedPath, usage.importedName);
565
+ }
566
+ }
567
+ /**
568
+ * Regex matching bridge GlobalSelector export patterns (global for matchAll).
569
+ * Matches both:
570
+ * - Old format: `export const XGlobalSelector = ".sc2sx-..."`
571
+ * - New format: `` export const XGlobalSelector = `.${xBridgeClass}` ``
572
+ */
573
+ const BRIDGE_EXPORT_RE = /export\s+const\s+(\w+GlobalSelector)\s*=\s*(?:["']\.sc2sx-|`\.\$\{)/g;
574
+ /**
575
+ * Detect whether an imported name is a bridge GlobalSelector from an
576
+ * already-converted StyleX file.
577
+ *
578
+ * Detection criteria (hybrid fast + safe):
579
+ * 1. Variable name ends with "GlobalSelector" AND the stripped name starts uppercase
580
+ * 2. Target file contains "@stylexjs/stylex" (string check, no parse)
581
+ * 3. Target file has a matching `export const XGlobalSelector = ".sc2sx-"` pattern
582
+ *
583
+ * @returns The stripped component name (e.g., "CollapseArrowIcon" for
584
+ * "CollapseArrowIconGlobalSelector"), or null if not a bridge.
585
+ */
586
+ function detectBridgeGlobalSelector(importedName, resolvedPath, readFile) {
587
+ if (!importedName.endsWith("GlobalSelector")) return null;
588
+ const stripped = importedName.slice(0, -14);
589
+ if (!stripped || !/^[A-Z]/.test(stripped)) return null;
590
+ const content = readFile(resolvedPath);
591
+ if (!content || !content.includes("@stylexjs/stylex")) return null;
592
+ let found = false;
593
+ for (const m of content.matchAll(BRIDGE_EXPORT_RE)) if (m[1] === importedName) {
594
+ found = true;
595
+ break;
596
+ }
597
+ if (!found) return null;
598
+ return stripped;
599
+ }
600
+ /**
601
+ * If `importedName` is a bridge GlobalSelector, populate bridge fields on `usage`
602
+ * and find the corresponding component import from the same source.
603
+ */
604
+ function applyBridgeFields(usage, importedName, localName, resolvedPath, importMap, readFile) {
605
+ const bridgeName = detectBridgeGlobalSelector(importedName, resolvedPath, readFile);
606
+ if (!bridgeName) return;
607
+ usage.bridgeComponentName = bridgeName;
608
+ const imp = importMap.get(localName);
609
+ if (!imp) return;
610
+ let defaultImportLocal;
611
+ for (const [otherLocal, otherImp] of importMap) {
612
+ if (otherImp.source !== imp.source || otherLocal === localName) continue;
613
+ if (otherImp.importedName === bridgeName) {
614
+ usage.bridgeComponentLocalName = otherLocal;
615
+ defaultImportLocal = void 0;
616
+ break;
617
+ }
618
+ if (otherImp.importedName === "default" && defaultImportLocal === void 0) defaultImportLocal = otherLocal;
619
+ }
620
+ if (defaultImportLocal !== void 0) usage.bridgeComponentLocalName = defaultImportLocal;
621
+ }
622
+ /** Global version for matchAll/replace operations */
623
+ const PLACEHOLDER_RE_G = new RegExp(PLACEHOLDER_RE.source, "g");
624
+ /**
625
+ * Walk the AST collecting ImportDeclaration and TaggedTemplateExpression nodes.
626
+ *
627
+ * Uses a targeted recursive walk — only descends into node types that can
628
+ * contain these targets (skips type annotations, comments, etc.).
629
+ */
630
+ function walkForImportsAndTemplates(node, imports, templates) {
631
+ if (!node || typeof node !== "object") return;
632
+ const n = node;
633
+ if (n.type === "ImportDeclaration") {
634
+ imports.push(n);
635
+ return;
636
+ }
637
+ if (n.type === "TaggedTemplateExpression") templates.push(n);
638
+ for (const key of Object.keys(n)) {
639
+ if (key === "type" || key === "start" || key === "end" || key === "loc") continue;
640
+ const val = n[key];
641
+ if (Array.isArray(val)) for (const child of val) walkForImportsAndTemplates(child, imports, templates);
642
+ else if (val && typeof val === "object" && val.type) walkForImportsAndTemplates(val, imports, templates);
643
+ }
644
+ }
645
+ /** Build a map of localName → import info from raw ImportDeclaration nodes. */
646
+ function buildImportMapFromNodes(importNodes) {
647
+ const map = /* @__PURE__ */ new Map();
648
+ for (const node of importNodes) {
649
+ const sourceValue = node.source?.value;
650
+ if (typeof sourceValue !== "string") continue;
651
+ const specifiers = node.specifiers;
652
+ if (!specifiers) continue;
653
+ for (const spec of specifiers) {
654
+ const localName = getNodeName(spec.local);
655
+ if (!localName) continue;
656
+ if (spec.type === "ImportDefaultSpecifier") map.set(localName, {
657
+ source: sourceValue,
658
+ importedName: "default"
659
+ });
660
+ else if (spec.type === "ImportNamespaceSpecifier") map.set(localName, {
661
+ source: sourceValue,
662
+ importedName: "*"
663
+ });
664
+ else if (spec.type === "ImportSpecifier") {
665
+ const importedName = getNodeName(spec.imported) ?? localName;
666
+ map.set(localName, {
667
+ source: sourceValue,
668
+ importedName
669
+ });
670
+ }
671
+ }
672
+ }
673
+ return map;
674
+ }
675
+ /**
676
+ * Local identifiers that refer to `styled` from `"styled-components"` (default and/or
677
+ * `import { styled }` / `import { styled as sc }`).
678
+ */
679
+ function collectStyledLocalBindingNames(importNodes) {
680
+ const names = /* @__PURE__ */ new Set();
681
+ for (const node of importNodes) {
682
+ if (node.source?.value !== "styled-components") continue;
683
+ const specifiers = node.specifiers;
684
+ if (!specifiers) continue;
685
+ for (const spec of specifiers) if (spec.type === "ImportDefaultSpecifier") {
686
+ const name = getNodeName(spec.local);
687
+ if (name) names.add(name);
688
+ } else if (spec.type === "ImportSpecifier") {
689
+ if (getNodeName(spec.imported) === "styled") {
690
+ const localName = getNodeName(spec.local);
691
+ if (localName) names.add(localName);
692
+ }
693
+ }
694
+ }
695
+ return names;
696
+ }
697
+ /** Find the local name for the styled-components default import. */
698
+ function findStyledImportNameFromNodes(importNodes) {
699
+ let namedStyledLocal;
700
+ for (const node of importNodes) {
701
+ if (node.source?.value !== "styled-components") continue;
702
+ const specifiers = node.specifiers;
703
+ if (!specifiers) continue;
704
+ for (const spec of specifiers) if (spec.type === "ImportDefaultSpecifier") {
705
+ const name = getNodeName(spec.local);
706
+ if (name) return name;
707
+ } else if (spec.type === "ImportSpecifier") {
708
+ if (getNodeName(spec.imported) === "styled") {
709
+ const localName = getNodeName(spec.local);
710
+ if (localName) namedStyledLocal = localName;
711
+ }
712
+ }
713
+ }
714
+ return namedStyledLocal;
715
+ }
716
+ /**
717
+ * Find local names of `css` imported from styled-components.
718
+ * Handles aliased imports like `import { css as sc } from "styled-components"`.
719
+ */
720
+ function findCssImportNamesFromNodes(importNodes) {
721
+ const names = /* @__PURE__ */ new Set();
722
+ for (const node of importNodes) {
723
+ if (node.source?.value !== "styled-components") continue;
724
+ const specifiers = node.specifiers;
725
+ if (!specifiers) continue;
726
+ for (const spec of specifiers) if (spec.type === "ImportSpecifier") {
727
+ if (getNodeName(spec.imported) === "css") {
728
+ const localName = getNodeName(spec.local);
729
+ if (localName) names.add(localName);
730
+ }
731
+ }
732
+ }
733
+ return names;
734
+ }
735
+ /**
736
+ * Find local names of imported components used as selectors inside
737
+ * styled-components template literals (both `styled` and `css` tagged templates).
738
+ */
739
+ function findComponentSelectorLocalsFromNodes(templateNodes, styledImportName, cssImportNames) {
740
+ const selectorLocals = /* @__PURE__ */ new Set();
741
+ for (const node of templateNodes) {
742
+ if (!isStyledTag(node.tag, styledImportName) && !isCssTag(node.tag, cssImportNames)) continue;
743
+ const quasi = node.quasi;
744
+ if (!quasi) continue;
745
+ const quasis = quasi.quasis;
746
+ const expressions = quasi.expressions;
747
+ if (!quasis || !expressions) continue;
748
+ const rawParts = [];
749
+ for (let i = 0; i < quasis.length; i++) {
750
+ const value = quasis[i]?.value;
751
+ rawParts.push(value?.raw ?? "");
752
+ if (i < expressions.length) rawParts.push(`__SC_EXPR_${i}__`);
753
+ }
754
+ const rawCss = rawParts.join("");
755
+ for (const match of rawCss.matchAll(PLACEHOLDER_RE_G)) {
756
+ const exprIndex = Number(match[1]);
757
+ const pos = match.index;
758
+ if (isTemplatePlaceholderInSelectorContext(rawCss, pos, match[0].length)) {
759
+ const expr = expressions[exprIndex];
760
+ if (expr?.type === "Identifier" && typeof expr.name === "string") selectorLocals.add(expr.name);
761
+ }
762
+ }
763
+ }
764
+ return selectorLocals;
765
+ }
766
+ /**
767
+ * Check whether a styled-components tag expression is a styled call.
768
+ * Matches: styled.div, styled(X), styled.div.attrs(...), styled(X).withConfig(...), etc.
769
+ */
770
+ function isStyledTag(tag, styledName) {
771
+ if (!tag || typeof tag !== "object") return false;
772
+ if (tag.type === "MemberExpression") {
773
+ const obj = tag.object;
774
+ if (obj?.type === "Identifier" && obj.name === styledName) return true;
775
+ }
776
+ if (tag.type === "CallExpression") {
777
+ const callee = tag.callee;
778
+ if (callee?.type === "Identifier" && callee.name === styledName) return true;
779
+ if (callee?.type === "MemberExpression" && callee.object) return isStyledTag(callee.object, styledName);
780
+ }
781
+ return false;
782
+ }
783
+ /** Check if a template tag is the `css` helper from styled-components. */
784
+ function isCssTag(tag, cssImportNames) {
785
+ if (!tag || !cssImportNames || cssImportNames.size === 0) return false;
786
+ return tag.type === "Identifier" && typeof tag.name === "string" && cssImportNames.has(tag.name);
787
+ }
788
+ /** Safely extract the name string from an AST identifier-like node. */
789
+ function getNodeName(node) {
790
+ if (!node || typeof node !== "object") return;
791
+ if (node.type === "Identifier" && typeof node.name === "string") return node.name;
792
+ }
793
+ /** Deduplicate and resolve two file lists into a single array of absolute paths. */
794
+ function deduplicateAndResolve(filesToTransform, consumerPaths) {
795
+ const seen = /* @__PURE__ */ new Set();
796
+ const result = [];
797
+ for (const f of filesToTransform) {
798
+ const abs = resolve(f);
799
+ if (!seen.has(abs)) {
800
+ seen.add(abs);
801
+ result.push(abs);
802
+ }
803
+ }
804
+ for (const f of consumerPaths) {
805
+ const abs = resolve(f);
806
+ if (!seen.has(abs)) {
807
+ seen.add(abs);
808
+ result.push(abs);
809
+ }
810
+ }
811
+ return result;
812
+ }
813
+ //#endregion
814
+ //#region src/internal/utilities/default-export-name.ts
815
+ /**
816
+ * Regex helpers for inspecting a module's default export by source text.
817
+ *
818
+ * These operate on raw source strings (not the AST) so they can be shared by
819
+ * both prepass and transform-step layers without import-graph coupling.
820
+ */
821
+ /**
822
+ * Returns the local name of a PascalCase default export, supporting both
823
+ * `export default Name` and `export { Name as default }` forms. Returns
824
+ * `undefined` when no PascalCase default export is found.
825
+ */
826
+ function findDefaultExportedLocalName(source) {
827
+ return source.match(/\bexport\s+default\s+([A-Z][A-Za-z0-9]*)\b/)?.[1] ?? source.match(/\bexport\s*\{[^}]*\b([A-Z][A-Za-z0-9]*)\s+as\s+default\b[^}]*\}/)?.[1];
828
+ }
829
+ //#endregion
525
830
  //#region src/internal/utilities/ast-walk.ts
526
831
  const SKIPPED_KEYS = new Set([
527
832
  "loc",
@@ -547,4 +852,4 @@ function walkAst(root, visitor) {
547
852
  visit(root);
548
853
  }
549
854
  //#endregion
550
- export { findImportSource as a, resolveBarrelReExportBinding as c, PARTIAL_MIGRATION_INCOMPLETE_WARNING as d, UNSUPPORTED_SHOULD_FORWARD_PROP_WARNING as f, fileImportsFrom as i, CASCADE_CONFLICT_WARNING as l, createPrepassParser as n, getReExportedSourceName as o, fileExports as r, resolveBarrelReExport as s, walkAst as t, Logger as u };
855
+ export { PARTIAL_MIGRATION_INCOMPLETE_WARNING as C, Logger as S, getCascadeDependedFilePath as T, findImportSource as _, buildImportMapFromNodes as a, resolveBarrelReExportBinding as b, deduplicateAndResolve as c, findStyledImportNameFromNodes as d, walkForImportsAndTemplates as f, fileImportsFrom as g, fileExports as h, applyBridgeFields as i, findComponentSelectorLocalsFromNodes as l, createPrepassParser as m, findDefaultExportedLocalName as n, categorizeSelectorUsages as o, addToSetMap as p, BARE_TEMPLATE_IDENTIFIER_RE as r, collectStyledLocalBindingNames as s, walkAst as t, findCssImportNamesFromNodes as u, getReExportedSourceName as v, UNSUPPORTED_SHOULD_FORWARD_PROP_WARNING as w, CASCADE_CONFLICT_WARNING as x, resolveBarrelReExport as y };
@@ -1,6 +1,6 @@
1
- import { r as toRealPath } from "./path-utils-BC4U8X_q.mjs";
2
- import { r as escapeRegex } from "./string-utils-Bo3cWgss.mjs";
3
- import { t as isSelectorContext } from "./selector-context-heuristic-LVizWWOR.mjs";
1
+ import { t as isSelectorContext } from "./selector-context-heuristic-Dptd93Xe.mjs";
2
+ import { r as toRealPath } from "./path-utils-ByFNVtHo.mjs";
3
+ import { r as escapeRegex } from "./string-utils-4eeXGa48.mjs";
4
4
  import { readFileSync } from "node:fs";
5
5
  //#region src/internal/bridge-consumer-patcher.ts
6
6
  /**
@@ -1,4 +1,4 @@
1
- import { a as findImportSource, c as resolveBarrelReExportBinding } from "./ast-walk-CCXrDCKY.mjs";
1
+ import { _ as findImportSource, b as resolveBarrelReExportBinding, f as walkForImportsAndTemplates, n as findDefaultExportedLocalName, s as collectStyledLocalBindingNames } from "./ast-walk-CLvMH7Lm.mjs";
2
2
  //#region src/internal/prepass/compute-leaf-set.ts
3
3
  /**
4
4
  * Computes which styled-component bindings are "leaves" for leaves-only mode:
@@ -44,6 +44,22 @@ function extractStyledDefBasesFromSource(filePath, source, into) {
44
44
  }
45
45
  }
46
46
  /**
47
+ * Regex baseline for styled defs, then an AST pass overrides/adds rows when the
48
+ * source parses. The AST pass understands aliased/named `styled` imports
49
+ * (`import { styled as sc }`) that the regexes (which assume the literal `styled`)
50
+ * miss, so callers that only ran the regex extractor under-report components.
51
+ */
52
+ function extractStyledDefBases(filePath, source, parser, into) {
53
+ extractStyledDefBasesFromSource(filePath, source, into);
54
+ try {
55
+ const ast = parser.parse(source);
56
+ const program = ast.program ?? ast;
57
+ const importNodes = [];
58
+ walkForImportsAndTemplates(program, importNodes, []);
59
+ extractStyledDefBasesFromAstProgram(filePath, program, collectStyledLocalBindingNames(importNodes), into);
60
+ } catch {}
61
+ }
62
+ /**
47
63
  * AST-based extraction: understands `let`/`var`, export blocks, named `styled` imports,
48
64
  * and `.attrs` / `.withConfig` chains before the tagged template.
49
65
  * Results merge into `into`; bindings found here override regex entries for the same name.
@@ -235,8 +251,5 @@ function leafKeyExists(defFile, exportedName, allowDefaultFallback, cachedRead,
235
251
  const defaultLocalName = findDefaultExportedLocalName(cachedRead(defFile));
236
252
  return defaultLocalName ? globalLeaves.get(defFile)?.has(defaultLocalName) ?? false : false;
237
253
  }
238
- function findDefaultExportedLocalName(source) {
239
- return source.match(/\bexport\s+default\s+([A-Z][A-Za-z0-9]*)\b/)?.[1] ?? source.match(/\bexport\s*\{[^}]*\b([A-Z][A-Za-z0-9]*)\s+as\s+default\b[^}]*\}/)?.[1];
240
- }
241
254
  //#endregion
242
- export { extractStyledDefBasesFromAstProgram as n, extractStyledDefBasesFromSource as r, computeGlobalLeafKeys as t };
255
+ export { extractStyledDefBases as n, computeGlobalLeafKeys as t };
@@ -1,5 +1,5 @@
1
- import { r as toRealPath } from "./path-utils-BC4U8X_q.mjs";
2
- import { r as escapeRegex } from "./string-utils-Bo3cWgss.mjs";
1
+ import { r as toRealPath } from "./path-utils-ByFNVtHo.mjs";
2
+ import { r as escapeRegex } from "./string-utils-4eeXGa48.mjs";
3
3
  import { readFileSync } from "node:fs";
4
4
  //#region src/internal/forwarded-as-consumer-patcher.ts
5
5
  /**
package/dist/index.d.mts CHANGED
@@ -64,7 +64,8 @@ interface RunTransformOptions {
64
64
  */
65
65
  maxExamples?: number;
66
66
  /**
67
- * Suppress jscodeshift runner output.
67
+ * Suppress console output: both the per-file runner messages and the final
68
+ * warning summary report. Warnings are still collected and returned.
68
69
  * @default false
69
70
  */
70
71
  silent?: boolean;
@@ -99,6 +100,13 @@ interface RunTransformOptions {
99
100
  * @default false
100
101
  */
101
102
  collectStandaloneFileResults?: boolean;
103
+ /**
104
+ * Absolute paths of files to treat as already converted to StyleX, even though
105
+ * they are not. Cascade-conflict checks then "see past" these files so a
106
+ * consumer's own unsupported patterns surface instead of being masked by the
107
+ * cascade bail. Intended for analysis-only/dry runs (e.g. the migration plan).
108
+ */
109
+ assumeConvertedFiles?: string[];
102
110
  }
103
111
  interface RunTransformResult {
104
112
  /** Number of files that had errors */
@@ -113,6 +121,8 @@ interface RunTransformResult {
113
121
  timeElapsed: number;
114
122
  /** Warnings emitted during transformation */
115
123
  warnings: CollectedWarning[];
124
+ /** Per-file outcomes from the dependency-ordered run. */
125
+ fileResults: TransformFileResult[];
116
126
  /** Per-file outcomes from isolated transforms, populated when requested. */
117
127
  standaloneFileResults?: TransformFileResult[];
118
128
  /** Warnings from isolated transforms, populated when requested. */
@@ -162,4 +172,83 @@ type TransformFileResult = {
162
172
  status: "error" | "skipped" | "unchanged" | "transformed";
163
173
  };
164
174
  //#endregion
165
- export { type AdapterInput, type ImportSource, type MarkerFileContext, defineAdapter, runTransform };
175
+ //#region src/migration-plan.d.ts
176
+ interface MigrationPlanOptions {
177
+ /** Glob pattern(s) for the files the codemod would transform. */
178
+ files: string | string[];
179
+ /** Glob pattern(s) of additional files to scan for consumers, or `null`. */
180
+ consumerPaths: string | string[] | null;
181
+ /** Adapter for the transform (same adapter you would pass to `runTransform`). */
182
+ adapter: AdapterInput;
183
+ /** jscodeshift parser to use. @default "tsx" */
184
+ parser?: "babel" | "babylon" | "flow" | "ts" | "tsx";
185
+ /**
186
+ * Safety cap on fixpoint analysis passes used to reveal cascade-masked
187
+ * blockers. Analysis throws if it doesn't stabilize within this many passes
188
+ * (rather than returning a partial plan). @default 50
189
+ */
190
+ maxAnalysisPasses?: number;
191
+ }
192
+ interface ImportedExportUsage {
193
+ /** Exported name consumers import (`"default"` for a default import, `"*"` for a namespace). */
194
+ exportName: string;
195
+ /** Number of distinct files importing this export. */
196
+ consumerCount: number;
197
+ }
198
+ interface ManualConversionReason {
199
+ /** The bail message explaining why the codemod cannot convert the file. */
200
+ message: string;
201
+ /** Source locations where the unsupported pattern occurs. */
202
+ locations: Array<{
203
+ filePath: string;
204
+ line: number;
205
+ column: number;
206
+ }>;
207
+ }
208
+ interface ManualConversionFile {
209
+ /** Path of the file that must be converted by hand. */
210
+ filePath: string;
211
+ /** 1-based position in the recommended bottom-up conversion order. */
212
+ order: number;
213
+ /** Number of distinct files that import from this file. */
214
+ consumerCount: number;
215
+ /**
216
+ * Number of files that auto-convert once this file is converted, because this
217
+ * file is their ONLY remaining blocker (single cascade blocker, no unsupported
218
+ * patterns of their own). Converting this file is sufficient for these.
219
+ */
220
+ soleBlockerFileCount: number;
221
+ /**
222
+ * Number of files that cascade-bail on this file (raw chain involvement).
223
+ * Includes files that also have other blockers, so converting this file alone
224
+ * does not necessarily unblock all of them. Always ≥ `soleBlockerFileCount`.
225
+ */
226
+ blockedFileCount: number;
227
+ /** Which exports consumers import, so you know what to convert first. */
228
+ importedExports: ImportedExportUsage[];
229
+ /** Why the codemod cannot convert this file automatically. */
230
+ reasons: ManualConversionReason[];
231
+ /** Other files in this plan that this file imports (must be converted first). */
232
+ dependsOn: string[];
233
+ }
234
+ interface MigrationPlan {
235
+ /** Files to convert by hand, ordered by unblock impact (dependencies first). */
236
+ manualConversionFiles: ManualConversionFile[];
237
+ /** Total number of files matched by the `files` glob. */
238
+ totalFiles: number;
239
+ /**
240
+ * Number of distinct files that auto-convert once ALL listed files are
241
+ * converted (cascade-blocked files that have no unsupported patterns of their
242
+ * own). This is the whole-plan payoff, not attributable to any single file.
243
+ */
244
+ unlocksFileCount: number;
245
+ }
246
+ /**
247
+ * Run the codemod in analysis-only (dry) mode and compute the ordered list of
248
+ * files that block the rest of the migration and must be converted manually.
249
+ */
250
+ declare function analyzeMigrationPlan(options: MigrationPlanOptions): Promise<MigrationPlan>;
251
+ /** Render a {@link MigrationPlan} as a human-readable, actionable report. */
252
+ declare function formatMigrationPlan(plan: MigrationPlan): string;
253
+ //#endregion
254
+ export { type AdapterInput, type ImportSource, type ImportedExportUsage, type ManualConversionFile, type ManualConversionReason, type MarkerFileContext, type MigrationPlan, type MigrationPlanOptions, analyzeMigrationPlan, defineAdapter, formatMigrationPlan, runTransform };