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/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, direct auto-migration payoff, secondary blocker-chain context, and the bail reasons. `formatMigrationPlan` renders it as a report with direct unlocks emphasized first so raw chain involvement is not mistaken for files unlocked by one blocker alone.
|
|
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,6 +1,60 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { n as isTemplatePlaceholderInSelectorContext, r as PLACEHOLDER_RE } from "./selector-context-heuristic-Dptd93Xe.mjs";
|
|
2
|
+
import path, { relative, resolve } from "node:path";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import { parse } from "@babel/parser";
|
|
5
|
+
//#region src/internal/utilities/ast-safety.ts
|
|
6
|
+
/**
|
|
7
|
+
* AST safety checks for null nodes and structural issues.
|
|
8
|
+
* Core concepts: recursive validation and error reporting.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* `JSON.stringify` replacer that drops noisy AST positional metadata
|
|
12
|
+
* (`loc`/`tokens`/`comments`/`start`/`end`) and replaces circular references
|
|
13
|
+
* with `"[Circular]"`, so AST nodes can be serialized for logging or as a
|
|
14
|
+
* stable structural key. Create a fresh replacer per `stringify` call — it
|
|
15
|
+
* keeps per-serialization state.
|
|
16
|
+
*/
|
|
17
|
+
function createAstSafeJsonReplacer() {
|
|
18
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
19
|
+
return (key, value) => {
|
|
20
|
+
if (key === "loc" || key === "tokens" || key === "comments" || key === "start" || key === "end") return;
|
|
21
|
+
if (value && typeof value === "object") {
|
|
22
|
+
if (seen.has(value)) return "[Circular]";
|
|
23
|
+
seen.add(value);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** AST node types whose `elements` array legitimately contains `null` (holes/elisions). */
|
|
29
|
+
const ARRAY_ELEMENT_HOLE_TYPES = new Set(["ArrayPattern", "ArrayExpression"]);
|
|
30
|
+
function assertNoNullNodesInArrays(node) {
|
|
31
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
32
|
+
const visit = (cur, path, allowNulls) => {
|
|
33
|
+
if (cur === null || cur === void 0) return;
|
|
34
|
+
if (Array.isArray(cur)) {
|
|
35
|
+
for (let i = 0; i < cur.length; i++) {
|
|
36
|
+
if (cur[i] === null) {
|
|
37
|
+
if (!allowNulls) throw new Error(`Null AST node in array at ${path}[${i}]`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
visit(cur[i], `${path}[${i}]`, false);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (typeof cur !== "object") return;
|
|
45
|
+
const curObj = cur;
|
|
46
|
+
if (seen.has(curObj)) return;
|
|
47
|
+
seen.add(curObj);
|
|
48
|
+
const nodeType = cur.type;
|
|
49
|
+
const hasValidElementHoles = typeof nodeType === "string" && ARRAY_ELEMENT_HOLE_TYPES.has(nodeType);
|
|
50
|
+
for (const [k, v] of Object.entries(cur)) {
|
|
51
|
+
if (v === null) continue;
|
|
52
|
+
if (typeof v === "object") visit(v, `${path}.${k}`, hasValidElementHoles && k === "elements");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
visit(node, "root", false);
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
4
58
|
//#region src/internal/logger.ts
|
|
5
59
|
/**
|
|
6
60
|
* Logger and warning types for transform diagnostics.
|
|
@@ -89,6 +143,14 @@ var Logger = class Logger {
|
|
|
89
143
|
static createReport() {
|
|
90
144
|
return new LoggerReport([...Logger.collected], Logger.fileCount, Logger.maxExamples);
|
|
91
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Restore the collected warnings to a previous snapshot (from
|
|
148
|
+
* `createReport().getWarnings()`). Used to undo the side effects of an
|
|
149
|
+
* analysis-only dry run on the process-global logger.
|
|
150
|
+
*/
|
|
151
|
+
static restoreWarnings(warnings) {
|
|
152
|
+
Logger.collected = [...warnings];
|
|
153
|
+
}
|
|
92
154
|
/** @internal - for testing only */
|
|
93
155
|
static _clearCollected() {
|
|
94
156
|
Logger.collected = [];
|
|
@@ -123,20 +185,9 @@ var Logger = class Logger {
|
|
|
123
185
|
}
|
|
124
186
|
static formatContext(context) {
|
|
125
187
|
if (typeof context === "undefined") return null;
|
|
126
|
-
return JSON.stringify(context,
|
|
188
|
+
return JSON.stringify(context, createAstSafeJsonReplacer(), 2);
|
|
127
189
|
}
|
|
128
190
|
};
|
|
129
|
-
function createContextReplacer() {
|
|
130
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
131
|
-
return (key, value) => {
|
|
132
|
-
if (key === "loc" || key === "tokens" || key === "comments" || key === "start" || key === "end") return;
|
|
133
|
-
if (value && typeof value === "object") {
|
|
134
|
-
if (seen.has(value)) return "[Circular]";
|
|
135
|
-
seen.add(value);
|
|
136
|
-
}
|
|
137
|
-
return value;
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
191
|
const MAX_DEPENDED_FILE_GROUPS = 15;
|
|
141
192
|
var LoggerReport = class {
|
|
142
193
|
warnings;
|
|
@@ -273,14 +324,16 @@ var LoggerReport = class {
|
|
|
273
324
|
}
|
|
274
325
|
}
|
|
275
326
|
};
|
|
327
|
+
/**
|
|
328
|
+
* Extract the depended-on file path from a cascade-conflict warning's context
|
|
329
|
+
* (the base component's defining file that must be converted first). Owned here
|
|
330
|
+
* alongside the warning-type definition so consumers share one invariant.
|
|
331
|
+
*/
|
|
276
332
|
function getCascadeDependedFilePath(warning) {
|
|
277
333
|
const context = warning.context;
|
|
278
|
-
if (!context
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (typeof definitionPath === "string") return definitionPath;
|
|
282
|
-
const importedPath = record.importedPath;
|
|
283
|
-
return typeof importedPath === "string" ? importedPath : void 0;
|
|
334
|
+
if (!context) return;
|
|
335
|
+
if (typeof context.definitionPath === "string") return context.definitionPath;
|
|
336
|
+
return typeof context.importedPath === "string" ? context.importedPath : void 0;
|
|
284
337
|
}
|
|
285
338
|
function uniqueSorted(values) {
|
|
286
339
|
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
@@ -425,22 +478,6 @@ function getFileImportsFromRes(name) {
|
|
|
425
478
|
return cached;
|
|
426
479
|
}
|
|
427
480
|
//#endregion
|
|
428
|
-
//#region src/internal/utilities/default-export-name.ts
|
|
429
|
-
/**
|
|
430
|
-
* Regex helpers for inspecting a module's default export by source text.
|
|
431
|
-
*
|
|
432
|
-
* These operate on raw source strings (not the AST) so they can be shared by
|
|
433
|
-
* both prepass and transform-step layers without import-graph coupling.
|
|
434
|
-
*/
|
|
435
|
-
/**
|
|
436
|
-
* Returns the local name of a PascalCase default export, supporting both
|
|
437
|
-
* `export default Name` and `export { Name as default }` forms. Returns
|
|
438
|
-
* `undefined` when no PascalCase default export is found.
|
|
439
|
-
*/
|
|
440
|
-
function findDefaultExportedLocalName(source) {
|
|
441
|
-
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];
|
|
442
|
-
}
|
|
443
|
-
//#endregion
|
|
444
481
|
//#region src/internal/prepass/prepass-parser.ts
|
|
445
482
|
/**
|
|
446
483
|
* Shared babel parser for prepass modules.
|
|
@@ -538,6 +575,309 @@ const FLOW_PLUGINS = [
|
|
|
538
575
|
"throwExpressions"
|
|
539
576
|
];
|
|
540
577
|
//#endregion
|
|
578
|
+
//#region src/internal/utilities/collection-utils.ts
|
|
579
|
+
/** Add a value to a Set stored in a Map, creating the Set if it doesn't exist. */
|
|
580
|
+
function addToSetMap(map, key, value) {
|
|
581
|
+
let set = map.get(key);
|
|
582
|
+
if (!set) {
|
|
583
|
+
set = /* @__PURE__ */ new Set();
|
|
584
|
+
map.set(key, set);
|
|
585
|
+
}
|
|
586
|
+
set.add(value);
|
|
587
|
+
}
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/internal/prepass/scan-cross-file-selectors.ts
|
|
590
|
+
/**
|
|
591
|
+
* Pre-filter: matches any bare `${Identifier}` template expression.
|
|
592
|
+
* Used to skip files that only contain arrow functions or member expressions
|
|
593
|
+
* in template literals (e.g. `${props => ...}`, `${theme.color}`).
|
|
594
|
+
*/
|
|
595
|
+
const BARE_TEMPLATE_IDENTIFIER_RE = /\$\{\s*[a-zA-Z_$][\w$]*\s*\}/;
|
|
596
|
+
/**
|
|
597
|
+
* Categorize cross-file selector usages into marker sidecar and global selector bridge maps.
|
|
598
|
+
*
|
|
599
|
+
* Bridge usages (from already-converted files) are skipped — the consumer handles marker
|
|
600
|
+
* generation via the forward selector handler, so no sidecar/bridge is needed on the target.
|
|
601
|
+
*/
|
|
602
|
+
function categorizeSelectorUsages(usages, componentsNeedingMarkerSidecar, componentsNeedingGlobalSelectorBridge) {
|
|
603
|
+
for (const usage of usages) {
|
|
604
|
+
if (usage.bridgeComponentName) continue;
|
|
605
|
+
if (usage.consumerIsTransformed) addToSetMap(componentsNeedingMarkerSidecar, usage.resolvedPath, usage.importedName);
|
|
606
|
+
addToSetMap(componentsNeedingGlobalSelectorBridge, usage.resolvedPath, usage.importedName);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Regex matching bridge GlobalSelector export patterns (global for matchAll).
|
|
611
|
+
* Matches both:
|
|
612
|
+
* - Old format: `export const XGlobalSelector = ".sc2sx-..."`
|
|
613
|
+
* - New format: `` export const XGlobalSelector = `.${xBridgeClass}` ``
|
|
614
|
+
*/
|
|
615
|
+
const BRIDGE_EXPORT_RE = /export\s+const\s+(\w+GlobalSelector)\s*=\s*(?:["']\.sc2sx-|`\.\$\{)/g;
|
|
616
|
+
/**
|
|
617
|
+
* Detect whether an imported name is a bridge GlobalSelector from an
|
|
618
|
+
* already-converted StyleX file.
|
|
619
|
+
*
|
|
620
|
+
* Detection criteria (hybrid fast + safe):
|
|
621
|
+
* 1. Variable name ends with "GlobalSelector" AND the stripped name starts uppercase
|
|
622
|
+
* 2. Target file contains "@stylexjs/stylex" (string check, no parse)
|
|
623
|
+
* 3. Target file has a matching `export const XGlobalSelector = ".sc2sx-"` pattern
|
|
624
|
+
*
|
|
625
|
+
* @returns The stripped component name (e.g., "CollapseArrowIcon" for
|
|
626
|
+
* "CollapseArrowIconGlobalSelector"), or null if not a bridge.
|
|
627
|
+
*/
|
|
628
|
+
function detectBridgeGlobalSelector(importedName, resolvedPath, readFile) {
|
|
629
|
+
if (!importedName.endsWith("GlobalSelector")) return null;
|
|
630
|
+
const stripped = importedName.slice(0, -14);
|
|
631
|
+
if (!stripped || !/^[A-Z]/.test(stripped)) return null;
|
|
632
|
+
const content = readFile(resolvedPath);
|
|
633
|
+
if (!content || !content.includes("@stylexjs/stylex")) return null;
|
|
634
|
+
let found = false;
|
|
635
|
+
for (const m of content.matchAll(BRIDGE_EXPORT_RE)) if (m[1] === importedName) {
|
|
636
|
+
found = true;
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
if (!found) return null;
|
|
640
|
+
return stripped;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* If `importedName` is a bridge GlobalSelector, populate bridge fields on `usage`
|
|
644
|
+
* and find the corresponding component import from the same source.
|
|
645
|
+
*/
|
|
646
|
+
function applyBridgeFields(usage, importedName, localName, resolvedPath, importMap, readFile) {
|
|
647
|
+
const bridgeName = detectBridgeGlobalSelector(importedName, resolvedPath, readFile);
|
|
648
|
+
if (!bridgeName) return;
|
|
649
|
+
usage.bridgeComponentName = bridgeName;
|
|
650
|
+
const imp = importMap.get(localName);
|
|
651
|
+
if (!imp) return;
|
|
652
|
+
let defaultImportLocal;
|
|
653
|
+
for (const [otherLocal, otherImp] of importMap) {
|
|
654
|
+
if (otherImp.source !== imp.source || otherLocal === localName) continue;
|
|
655
|
+
if (otherImp.importedName === bridgeName) {
|
|
656
|
+
usage.bridgeComponentLocalName = otherLocal;
|
|
657
|
+
defaultImportLocal = void 0;
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
if (otherImp.importedName === "default" && defaultImportLocal === void 0) defaultImportLocal = otherLocal;
|
|
661
|
+
}
|
|
662
|
+
if (defaultImportLocal !== void 0) usage.bridgeComponentLocalName = defaultImportLocal;
|
|
663
|
+
}
|
|
664
|
+
/** Global version for matchAll/replace operations */
|
|
665
|
+
const PLACEHOLDER_RE_G = new RegExp(PLACEHOLDER_RE.source, "g");
|
|
666
|
+
/**
|
|
667
|
+
* Walk the AST collecting ImportDeclaration and TaggedTemplateExpression nodes.
|
|
668
|
+
*
|
|
669
|
+
* Uses a targeted recursive walk — only descends into node types that can
|
|
670
|
+
* contain these targets (skips type annotations, comments, etc.).
|
|
671
|
+
*/
|
|
672
|
+
function walkForImportsAndTemplates(node, imports, templates) {
|
|
673
|
+
if (!node || typeof node !== "object") return;
|
|
674
|
+
const n = node;
|
|
675
|
+
if (n.type === "ImportDeclaration") {
|
|
676
|
+
imports.push(n);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (n.type === "TaggedTemplateExpression") templates.push(n);
|
|
680
|
+
for (const key of Object.keys(n)) {
|
|
681
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc") continue;
|
|
682
|
+
const val = n[key];
|
|
683
|
+
if (Array.isArray(val)) for (const child of val) walkForImportsAndTemplates(child, imports, templates);
|
|
684
|
+
else if (val && typeof val === "object" && val.type) walkForImportsAndTemplates(val, imports, templates);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/** Build a map of localName → import info from raw ImportDeclaration nodes. */
|
|
688
|
+
function buildImportMapFromNodes(importNodes) {
|
|
689
|
+
const map = /* @__PURE__ */ new Map();
|
|
690
|
+
for (const node of importNodes) {
|
|
691
|
+
const sourceValue = node.source?.value;
|
|
692
|
+
if (typeof sourceValue !== "string") continue;
|
|
693
|
+
const specifiers = node.specifiers;
|
|
694
|
+
if (!specifiers) continue;
|
|
695
|
+
for (const spec of specifiers) {
|
|
696
|
+
const localName = getNodeName(spec.local);
|
|
697
|
+
if (!localName) continue;
|
|
698
|
+
if (spec.type === "ImportDefaultSpecifier") map.set(localName, {
|
|
699
|
+
source: sourceValue,
|
|
700
|
+
importedName: "default"
|
|
701
|
+
});
|
|
702
|
+
else if (spec.type === "ImportNamespaceSpecifier") map.set(localName, {
|
|
703
|
+
source: sourceValue,
|
|
704
|
+
importedName: "*"
|
|
705
|
+
});
|
|
706
|
+
else if (spec.type === "ImportSpecifier") {
|
|
707
|
+
const importedName = getNodeName(spec.imported) ?? localName;
|
|
708
|
+
map.set(localName, {
|
|
709
|
+
source: sourceValue,
|
|
710
|
+
importedName
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return map;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Local identifiers that refer to `styled` from `"styled-components"` (default and/or
|
|
719
|
+
* `import { styled }` / `import { styled as sc }`).
|
|
720
|
+
*/
|
|
721
|
+
function collectStyledLocalBindingNames(importNodes) {
|
|
722
|
+
const names = /* @__PURE__ */ new Set();
|
|
723
|
+
for (const node of importNodes) {
|
|
724
|
+
if (node.source?.value !== "styled-components") continue;
|
|
725
|
+
const specifiers = node.specifiers;
|
|
726
|
+
if (!specifiers) continue;
|
|
727
|
+
for (const spec of specifiers) if (spec.type === "ImportDefaultSpecifier") {
|
|
728
|
+
const name = getNodeName(spec.local);
|
|
729
|
+
if (name) names.add(name);
|
|
730
|
+
} else if (spec.type === "ImportSpecifier") {
|
|
731
|
+
if (getNodeName(spec.imported) === "styled") {
|
|
732
|
+
const localName = getNodeName(spec.local);
|
|
733
|
+
if (localName) names.add(localName);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return names;
|
|
738
|
+
}
|
|
739
|
+
/** Find the local name for the styled-components default import. */
|
|
740
|
+
function findStyledImportNameFromNodes(importNodes) {
|
|
741
|
+
let namedStyledLocal;
|
|
742
|
+
for (const node of importNodes) {
|
|
743
|
+
if (node.source?.value !== "styled-components") continue;
|
|
744
|
+
const specifiers = node.specifiers;
|
|
745
|
+
if (!specifiers) continue;
|
|
746
|
+
for (const spec of specifiers) if (spec.type === "ImportDefaultSpecifier") {
|
|
747
|
+
const name = getNodeName(spec.local);
|
|
748
|
+
if (name) return name;
|
|
749
|
+
} else if (spec.type === "ImportSpecifier") {
|
|
750
|
+
if (getNodeName(spec.imported) === "styled") {
|
|
751
|
+
const localName = getNodeName(spec.local);
|
|
752
|
+
if (localName) namedStyledLocal = localName;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return namedStyledLocal;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Find local names of `css` imported from styled-components.
|
|
760
|
+
* Handles aliased imports like `import { css as sc } from "styled-components"`.
|
|
761
|
+
*/
|
|
762
|
+
function findCssImportNamesFromNodes(importNodes) {
|
|
763
|
+
const names = /* @__PURE__ */ new Set();
|
|
764
|
+
for (const node of importNodes) {
|
|
765
|
+
if (node.source?.value !== "styled-components") continue;
|
|
766
|
+
const specifiers = node.specifiers;
|
|
767
|
+
if (!specifiers) continue;
|
|
768
|
+
for (const spec of specifiers) if (spec.type === "ImportSpecifier") {
|
|
769
|
+
if (getNodeName(spec.imported) === "css") {
|
|
770
|
+
const localName = getNodeName(spec.local);
|
|
771
|
+
if (localName) names.add(localName);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return names;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Find local names of imported components used as selectors inside
|
|
779
|
+
* styled-components template literals (both `styled` and `css` tagged templates).
|
|
780
|
+
*/
|
|
781
|
+
function findComponentSelectorLocalsFromNodes(templateNodes, styledImportName, cssImportNames) {
|
|
782
|
+
const selectorLocals = /* @__PURE__ */ new Set();
|
|
783
|
+
for (const node of templateNodes) {
|
|
784
|
+
if (!isStyledTag(node.tag, styledImportName) && !isCssTag(node.tag, cssImportNames)) continue;
|
|
785
|
+
const quasi = node.quasi;
|
|
786
|
+
if (!quasi) continue;
|
|
787
|
+
const quasis = quasi.quasis;
|
|
788
|
+
const expressions = quasi.expressions;
|
|
789
|
+
if (!quasis || !expressions) continue;
|
|
790
|
+
const rawParts = [];
|
|
791
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
792
|
+
const value = quasis[i]?.value;
|
|
793
|
+
rawParts.push(value?.raw ?? "");
|
|
794
|
+
if (i < expressions.length) rawParts.push(`__SC_EXPR_${i}__`);
|
|
795
|
+
}
|
|
796
|
+
const rawCss = rawParts.join("");
|
|
797
|
+
for (const match of rawCss.matchAll(PLACEHOLDER_RE_G)) {
|
|
798
|
+
const exprIndex = Number(match[1]);
|
|
799
|
+
const pos = match.index;
|
|
800
|
+
if (isTemplatePlaceholderInSelectorContext(rawCss, pos, match[0].length)) {
|
|
801
|
+
const expr = expressions[exprIndex];
|
|
802
|
+
if (expr?.type === "Identifier" && typeof expr.name === "string") selectorLocals.add(expr.name);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return selectorLocals;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Check whether a styled-components tag expression is a styled call.
|
|
810
|
+
* Matches: styled.div, styled(X), styled.div.attrs(...), styled(X).withConfig(...), etc.
|
|
811
|
+
*/
|
|
812
|
+
function isStyledTag(tag, styledName) {
|
|
813
|
+
if (!tag || typeof tag !== "object") return false;
|
|
814
|
+
if (tag.type === "MemberExpression") {
|
|
815
|
+
const obj = tag.object;
|
|
816
|
+
if (obj?.type === "Identifier" && obj.name === styledName) return true;
|
|
817
|
+
}
|
|
818
|
+
if (tag.type === "CallExpression") {
|
|
819
|
+
const callee = tag.callee;
|
|
820
|
+
if (callee?.type === "Identifier" && callee.name === styledName) return true;
|
|
821
|
+
if (callee?.type === "MemberExpression" && callee.object) return isStyledTag(callee.object, styledName);
|
|
822
|
+
}
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
/** Check if a template tag is the `css` helper from styled-components. */
|
|
826
|
+
function isCssTag(tag, cssImportNames) {
|
|
827
|
+
if (!tag || !cssImportNames || cssImportNames.size === 0) return false;
|
|
828
|
+
return tag.type === "Identifier" && typeof tag.name === "string" && cssImportNames.has(tag.name);
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Build the shared `DEBUG_CODEMOD` report lines for cross-file prepass info.
|
|
832
|
+
* `header` distinguishes the standalone scan from the unified prepass; callers
|
|
833
|
+
* may append their own sections before writing.
|
|
834
|
+
*/
|
|
835
|
+
function buildCrossFileDebugLines(header, scannedFiles, info) {
|
|
836
|
+
const cwd = process.cwd();
|
|
837
|
+
const rel = (p) => relative(cwd, p);
|
|
838
|
+
const lines = [header];
|
|
839
|
+
lines.push(` Scanned ${scannedFiles.length} file(s)`);
|
|
840
|
+
if (info.selectorUsages.size === 0) lines.push(" No cross-file selector usages found.");
|
|
841
|
+
else {
|
|
842
|
+
lines.push(` Found cross-file selector usages in ${info.selectorUsages.size} file(s):`);
|
|
843
|
+
for (const [consumer, usages] of info.selectorUsages) for (const u of usages) lines.push(` ${rel(consumer)} → ${u.importedName} (from ${rel(u.resolvedPath)}, transformed=${u.consumerIsTransformed})`);
|
|
844
|
+
}
|
|
845
|
+
if (info.componentsNeedingMarkerSidecar.size > 0) {
|
|
846
|
+
lines.push(" Components needing marker sidecar (both consumer and target transformed):");
|
|
847
|
+
for (const [file, names] of info.componentsNeedingMarkerSidecar) lines.push(` ${rel(file)}: ${[...names].join(", ")}`);
|
|
848
|
+
}
|
|
849
|
+
if (info.componentsNeedingGlobalSelectorBridge.size > 0) {
|
|
850
|
+
lines.push(" Components needing global selector bridge className (consumer not transformed):");
|
|
851
|
+
for (const [file, names] of info.componentsNeedingGlobalSelectorBridge) lines.push(` ${rel(file)}: ${[...names].join(", ")}`);
|
|
852
|
+
}
|
|
853
|
+
return lines;
|
|
854
|
+
}
|
|
855
|
+
/** Safely extract the name string from an AST identifier-like node. */
|
|
856
|
+
function getNodeName(node) {
|
|
857
|
+
if (!node || typeof node !== "object") return;
|
|
858
|
+
if (node.type === "Identifier" && typeof node.name === "string") return node.name;
|
|
859
|
+
}
|
|
860
|
+
/** Deduplicate and resolve two file lists into a single array of absolute paths. */
|
|
861
|
+
function deduplicateAndResolve(filesToTransform, consumerPaths) {
|
|
862
|
+
const seen = /* @__PURE__ */ new Set();
|
|
863
|
+
const result = [];
|
|
864
|
+
for (const f of filesToTransform) {
|
|
865
|
+
const abs = resolve(f);
|
|
866
|
+
if (!seen.has(abs)) {
|
|
867
|
+
seen.add(abs);
|
|
868
|
+
result.push(abs);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
for (const f of consumerPaths) {
|
|
872
|
+
const abs = resolve(f);
|
|
873
|
+
if (!seen.has(abs)) {
|
|
874
|
+
seen.add(abs);
|
|
875
|
+
result.push(abs);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return result;
|
|
879
|
+
}
|
|
880
|
+
//#endregion
|
|
541
881
|
//#region src/internal/utilities/ast-walk.ts
|
|
542
882
|
const SKIPPED_KEYS = new Set([
|
|
543
883
|
"loc",
|
|
@@ -563,4 +903,4 @@ function walkAst(root, visitor) {
|
|
|
563
903
|
visit(root);
|
|
564
904
|
}
|
|
565
905
|
//#endregion
|
|
566
|
-
export {
|
|
906
|
+
export { PARTIAL_MIGRATION_INCOMPLETE_WARNING as C, createAstSafeJsonReplacer as D, assertNoNullNodesInArrays as E, 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, buildCrossFileDebugLines as i, findComponentSelectorLocalsFromNodes as l, createPrepassParser as m, BARE_TEMPLATE_IDENTIFIER_RE as n, categorizeSelectorUsages as o, addToSetMap as p, applyBridgeFields 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 { t as isSelectorContext } from "./selector-context-heuristic-Dptd93Xe.mjs";
|
|
1
2
|
import { r as toRealPath } from "./path-utils-BC4U8X_q.mjs";
|
|
2
3
|
import { r as escapeRegex } from "./string-utils-Bo3cWgss.mjs";
|
|
3
|
-
import { t as isSelectorContext } from "./selector-context-heuristic-LVizWWOR.mjs";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
//#region src/internal/bridge-consumer-patcher.ts
|
|
6
6
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as ImportSource, i as AdapterInput, o as MarkerFileContext, s as defineAdapter, t as CollectedWarning } from "./logger-ByYsVkrB.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/run.d.ts
|
|
4
4
|
interface RunTransformOptions {
|
|
@@ -64,21 +64,11 @@ interface RunTransformOptions {
|
|
|
64
64
|
*/
|
|
65
65
|
maxExamples?: number;
|
|
66
66
|
/**
|
|
67
|
-
* Suppress
|
|
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;
|
|
71
|
-
/**
|
|
72
|
-
* Controls which styled declarations are eligible for conversion.
|
|
73
|
-
*
|
|
74
|
-
* - `"all"` converts every supported styled declaration.
|
|
75
|
-
* - `"leavesOnly"` only converts declarations whose render base is intrinsic
|
|
76
|
-
* after adapter resolution, or that wrap another leaf styled declaration in
|
|
77
|
-
* the transform run (including cross-file imports).
|
|
78
|
-
*
|
|
79
|
-
* @default "all"
|
|
80
|
-
*/
|
|
81
|
-
transformMode?: TransformMode;
|
|
82
72
|
/**
|
|
83
73
|
* When true, allow the codemod to leave individual styled declarations as-is when
|
|
84
74
|
* they hit an unsupported pattern while transforming the rest of the file. This
|
|
@@ -99,6 +89,13 @@ interface RunTransformOptions {
|
|
|
99
89
|
* @default false
|
|
100
90
|
*/
|
|
101
91
|
collectStandaloneFileResults?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Absolute paths of files to treat as already converted to StyleX, even though
|
|
94
|
+
* they are not. Cascade-conflict checks then "see past" these files so a
|
|
95
|
+
* consumer's own unsupported patterns surface instead of being masked by the
|
|
96
|
+
* cascade bail. Intended for analysis-only/dry runs (e.g. the migration plan).
|
|
97
|
+
*/
|
|
98
|
+
assumeConvertedFiles?: string[];
|
|
102
99
|
}
|
|
103
100
|
interface RunTransformResult {
|
|
104
101
|
/** Number of files that had errors */
|
|
@@ -113,6 +110,8 @@ interface RunTransformResult {
|
|
|
113
110
|
timeElapsed: number;
|
|
114
111
|
/** Warnings emitted during transformation */
|
|
115
112
|
warnings: CollectedWarning[];
|
|
113
|
+
/** Per-file outcomes from the dependency-ordered run. */
|
|
114
|
+
fileResults: TransformFileResult[];
|
|
116
115
|
/** Per-file outcomes from isolated transforms, populated when requested. */
|
|
117
116
|
standaloneFileResults?: TransformFileResult[];
|
|
118
117
|
/** Warnings from isolated transforms, populated when requested. */
|
|
@@ -162,4 +161,83 @@ type TransformFileResult = {
|
|
|
162
161
|
status: "error" | "skipped" | "unchanged" | "transformed";
|
|
163
162
|
};
|
|
164
163
|
//#endregion
|
|
165
|
-
|
|
164
|
+
//#region src/migration-plan.d.ts
|
|
165
|
+
interface MigrationPlanOptions {
|
|
166
|
+
/** Glob pattern(s) for the files the codemod would transform. */
|
|
167
|
+
files: string | string[];
|
|
168
|
+
/** Glob pattern(s) of additional files to scan for consumers, or `null`. */
|
|
169
|
+
consumerPaths: string | string[] | null;
|
|
170
|
+
/** Adapter for the transform (same adapter you would pass to `runTransform`). */
|
|
171
|
+
adapter: AdapterInput;
|
|
172
|
+
/** jscodeshift parser to use. @default "tsx" */
|
|
173
|
+
parser?: "babel" | "babylon" | "flow" | "ts" | "tsx";
|
|
174
|
+
/**
|
|
175
|
+
* Safety cap on fixpoint analysis passes used to reveal cascade-masked
|
|
176
|
+
* blockers. Analysis throws if it doesn't stabilize within this many passes
|
|
177
|
+
* (rather than returning a partial plan). @default 50
|
|
178
|
+
*/
|
|
179
|
+
maxAnalysisPasses?: number;
|
|
180
|
+
}
|
|
181
|
+
interface ImportedExportUsage {
|
|
182
|
+
/** Exported name consumers import (`"default"` for a default import, `"*"` for a namespace). */
|
|
183
|
+
exportName: string;
|
|
184
|
+
/** Number of distinct files importing this export. */
|
|
185
|
+
consumerCount: number;
|
|
186
|
+
}
|
|
187
|
+
interface ManualConversionReason {
|
|
188
|
+
/** The bail message explaining why the codemod cannot convert the file. */
|
|
189
|
+
message: string;
|
|
190
|
+
/** Source locations where the unsupported pattern occurs. */
|
|
191
|
+
locations: Array<{
|
|
192
|
+
filePath: string;
|
|
193
|
+
line: number;
|
|
194
|
+
column: number;
|
|
195
|
+
}>;
|
|
196
|
+
}
|
|
197
|
+
interface ManualConversionFile {
|
|
198
|
+
/** Path of the file that must be converted by hand. */
|
|
199
|
+
filePath: string;
|
|
200
|
+
/** 1-based position in the recommended bottom-up conversion order. */
|
|
201
|
+
order: number;
|
|
202
|
+
/** Number of distinct files that import from this file. */
|
|
203
|
+
consumerCount: number;
|
|
204
|
+
/**
|
|
205
|
+
* Number of files that auto-convert once this file is converted, because this
|
|
206
|
+
* file is their ONLY remaining blocker (single cascade blocker, no unsupported
|
|
207
|
+
* patterns of their own). Converting this file is sufficient for these.
|
|
208
|
+
*/
|
|
209
|
+
soleBlockerFileCount: number;
|
|
210
|
+
/**
|
|
211
|
+
* Number of files that cascade-bail on this file (raw chain involvement).
|
|
212
|
+
* Includes files that also have other blockers, so converting this file alone
|
|
213
|
+
* does not necessarily unblock all of them. Always ≥ `soleBlockerFileCount`.
|
|
214
|
+
*/
|
|
215
|
+
blockedFileCount: number;
|
|
216
|
+
/** Which exports consumers import, so you know what to convert first. */
|
|
217
|
+
importedExports: ImportedExportUsage[];
|
|
218
|
+
/** Why the codemod cannot convert this file automatically. */
|
|
219
|
+
reasons: ManualConversionReason[];
|
|
220
|
+
/** Other files in this plan that this file imports (must be converted first). */
|
|
221
|
+
dependsOn: string[];
|
|
222
|
+
}
|
|
223
|
+
interface MigrationPlan {
|
|
224
|
+
/** Files to convert by hand, ordered by unblock impact (dependencies first). */
|
|
225
|
+
manualConversionFiles: ManualConversionFile[];
|
|
226
|
+
/** Total number of files matched by the `files` glob. */
|
|
227
|
+
totalFiles: number;
|
|
228
|
+
/**
|
|
229
|
+
* Number of distinct files that auto-convert once ALL listed files are
|
|
230
|
+
* converted (cascade-blocked files that have no unsupported patterns of their
|
|
231
|
+
* own). This is the whole-plan payoff, not attributable to any single file.
|
|
232
|
+
*/
|
|
233
|
+
unlocksFileCount: number;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Run the codemod in analysis-only (dry) mode and compute the ordered list of
|
|
237
|
+
* files that block the rest of the migration and must be converted manually.
|
|
238
|
+
*/
|
|
239
|
+
declare function analyzeMigrationPlan(options: MigrationPlanOptions): Promise<MigrationPlan>;
|
|
240
|
+
/** Render a {@link MigrationPlan} as a human-readable, actionable report. */
|
|
241
|
+
declare function formatMigrationPlan(plan: MigrationPlan): string;
|
|
242
|
+
//#endregion
|
|
243
|
+
export { type AdapterInput, type ImportSource, type ImportedExportUsage, type ManualConversionFile, type ManualConversionReason, type MarkerFileContext, type MigrationPlan, type MigrationPlanOptions, analyzeMigrationPlan, defineAdapter, formatMigrationPlan, runTransform };
|