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