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/README.md +20 -0
- package/dist/{ast-walk-C226poBl.mjs → ast-walk-CLvMH7Lm.mjs} +313 -24
- package/dist/{bridge-consumer-patcher-DDcYZM_G.mjs → bridge-consumer-patcher-B__X3jOg.mjs} +3 -3
- package/dist/{compute-leaf-set-Cu4lMMQ9.mjs → compute-leaf-set-D5GvkV-H.mjs} +18 -2
- package/dist/{forwarded-as-consumer-patcher-Bva_36Gy.mjs → forwarded-as-consumer-patcher-Bs9ymhBa.mjs} +2 -2
- package/dist/index.d.mts +91 -2
- package/dist/index.mjs +454 -19
- package/dist/{prop-usage-Bs2F3Wke.mjs → prop-usage-z-bcXTOD.mjs} +2 -283
- package/dist/{run-prepass-D3Ti1ryc.mjs → run-prepass-BueJvYyf.mjs} +7 -14
- package/dist/{sx-surface-Cth8EesU.mjs → sx-surface-_Hjc6ZDq.mjs} +1 -1
- package/dist/transform.mjs +7 -7
- package/dist/{transient-prop-consumer-patcher-DSd7uVA6.mjs → transient-prop-consumer-patcher-BDruM1OI.mjs} +2 -2
- package/dist/{typescript-analysis-BLyx4wAJ.mjs → typescript-analysis-BzsnorIV.mjs} +1 -1
- package/package.json +1 -1
- /package/dist/{path-utils-BC4U8X_q.mjs → path-utils-ByFNVtHo.mjs} +0 -0
- /package/dist/{selector-context-heuristic-LVizWWOR.mjs → selector-context-heuristic-Dptd93Xe.mjs} +0 -0
- /package/dist/{string-utils-Bo3cWgss.mjs → string-utils-4eeXGa48.mjs} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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 };
|