@mittwald/flow-codemods 1.1.0-next.10
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 +106 -0
- package/dist/catalog/entries.d.ts +17 -0
- package/dist/catalog/entries.d.ts.map +1 -0
- package/dist/catalog/entries.js +9 -0
- package/dist/catalog/select.d.ts +28 -0
- package/dist/catalog/select.d.ts.map +1 -0
- package/dist/catalog/select.js +27 -0
- package/dist/catalog/types.d.ts +48 -0
- package/dist/catalog/types.d.ts.map +1 -0
- package/dist/catalog/types.js +0 -0
- package/dist/checks/context.d.ts +16 -0
- package/dist/checks/context.d.ts.map +1 -0
- package/dist/checks/context.js +72 -0
- package/dist/checks/load.d.ts +13 -0
- package/dist/checks/load.d.ts.map +1 -0
- package/dist/checks/load.js +53 -0
- package/dist/checks/types.d.ts +49 -0
- package/dist/checks/types.d.ts.map +1 -0
- package/dist/checks/types.js +0 -0
- package/dist/cli/args.d.ts +22 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +56 -0
- package/dist/cli/choose.d.ts +31 -0
- package/dist/cli/choose.d.ts.map +1 -0
- package/dist/cli/choose.js +31 -0
- package/dist/cli/codemod.d.ts +24 -0
- package/dist/cli/codemod.d.ts.map +1 -0
- package/dist/cli/codemod.js +76 -0
- package/dist/cli/detect.d.ts +35 -0
- package/dist/cli/detect.d.ts.map +1 -0
- package/dist/cli/detect.js +62 -0
- package/dist/cli/list.d.ts +71 -0
- package/dist/cli/list.d.ts.map +1 -0
- package/dist/cli/list.js +293 -0
- package/dist/cli/upgrade.d.ts +34 -0
- package/dist/cli/upgrade.d.ts.map +1 -0
- package/dist/cli/upgrade.js +217 -0
- package/dist/cli/verify.d.ts +77 -0
- package/dist/cli/verify.d.ts.map +1 -0
- package/dist/cli/verify.js +148 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +117 -0
- package/dist/detect/align-to-combine.d.ts +8 -0
- package/dist/detect/align-to-combine.d.ts.map +1 -0
- package/dist/detect/align-to-combine.js +9 -0
- package/dist/detect/overlay-controller-add-on-close-return-type.d.ts +9 -0
- package/dist/detect/overlay-controller-add-on-close-return-type.d.ts.map +1 -0
- package/dist/detect/overlay-controller-add-on-close-return-type.js +10 -0
- package/dist/detect/tooltip-trigger-delay-type.d.ts +11 -0
- package/dist/detect/tooltip-trigger-delay-type.d.ts.map +1 -0
- package/dist/detect/tooltip-trigger-delay-type.js +12 -0
- package/dist/flowPackages.generated.d.ts +3 -0
- package/dist/flowPackages.generated.d.ts.map +1 -0
- package/dist/flowPackages.generated.js +18 -0
- package/dist/git.d.ts +13 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +41 -0
- package/dist/install.d.ts +37 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +71 -0
- package/dist/manifest.d.ts +45 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +88 -0
- package/dist/migrations.generated.d.ts +4 -0
- package/dist/migrations.generated.d.ts.map +1 -0
- package/dist/migrations.generated.js +213 -0
- package/dist/resolve/range.d.ts +50 -0
- package/dist/resolve/range.d.ts.map +1 -0
- package/dist/resolve/range.js +173 -0
- package/dist/resolve/registry.d.ts +33 -0
- package/dist/resolve/registry.d.ts.map +1 -0
- package/dist/resolve/registry.js +69 -0
- package/dist/resolve/target.d.ts +63 -0
- package/dist/resolve/target.d.ts.map +1 -0
- package/dist/resolve/target.js +115 -0
- package/dist/run/jscodeshift.d.ts +42 -0
- package/dist/run/jscodeshift.d.ts.map +1 -0
- package/dist/run/jscodeshift.js +88 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/verify/align-to-combine.d.ts +11 -0
- package/dist/verify/align-to-combine.d.ts.map +1 -0
- package/dist/verify/align-to-combine.js +22 -0
- package/dist/verify/overlay-controller-add-on-close-return-type.d.ts +14 -0
- package/dist/verify/overlay-controller-add-on-close-return-type.d.ts.map +1 -0
- package/dist/verify/overlay-controller-add-on-close-return-type.js +21 -0
- package/dist/verify/tooltip-trigger-delay-type.d.ts +13 -0
- package/dist/verify/tooltip-trigger-delay-type.d.ts.map +1 -0
- package/dist/verify/tooltip-trigger-delay-type.js +18 -0
- package/package.json +41 -0
- package/src/migrations/accent-box-color-to-background-color/transform.ts +218 -0
- package/src/migrations/action-prop-to-on-action/transform.ts +110 -0
- package/src/migrations/align-to-combine/transform.ts +212 -0
- package/src/migrations/button-color-accent-to-success/transform.ts +164 -0
- package/src/migrations/button-props-interfaces/transform.ts +229 -0
- package/src/migrations/color-primary-to-default/transform.ts +173 -0
- package/src/migrations/imports-to-package-root/transform.ts +107 -0
- package/src/migrations/muted-action-error-to-abort-action-error/transform.ts +260 -0
- package/src/migrations/password-tools-rule/transform.ts +219 -0
- package/src/migrations/password-tools-subpath-renamed/transform.ts +61 -0
- package/src/tools/to-remote-package.ts +37 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { Transform } from "jscodeshift";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renames the `Align` component to `Combine`, and `AlignProps` to
|
|
5
|
+
* `CombineProps`.
|
|
6
|
+
*
|
|
7
|
+
* Only names imported from `@mittwald/flow-react-components` or
|
|
8
|
+
* `@mittwald/flow-remote-react-components` (including their subpath entries)
|
|
9
|
+
* are touched, so a same-named import from another package is left alone. An
|
|
10
|
+
* `Align` imported under a local alias (`import { Align as Row }`) keeps its
|
|
11
|
+
* alias — only the imported name changes. Namespace usages (`<Flow.Align />`,
|
|
12
|
+
* `Flow.AlignProps`) are rewritten as well.
|
|
13
|
+
*/
|
|
14
|
+
const alignToCombineTransform: Transform = (fileInfo, { j }) => {
|
|
15
|
+
const flowPackages = [
|
|
16
|
+
"@mittwald/flow-react-components",
|
|
17
|
+
"@mittwald/flow-remote-react-components",
|
|
18
|
+
];
|
|
19
|
+
const renames = new Map([
|
|
20
|
+
["Align", "Combine"],
|
|
21
|
+
["AlignProps", "CombineProps"],
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const isFlowImport = (source: string): boolean =>
|
|
25
|
+
flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
|
|
26
|
+
|
|
27
|
+
// ast-types models `importKind` on the declaration only, while babel also
|
|
28
|
+
// puts it on the specifier — which is where a per-specifier `type X` lives.
|
|
29
|
+
const isTypeOnly = (specifier: object): boolean =>
|
|
30
|
+
(specifier as { importKind?: string }).importKind === "type";
|
|
31
|
+
const makeValueImport = (specifier: object): void => {
|
|
32
|
+
(specifier as { importKind?: string }).importKind = "value";
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const root = j(fileInfo.source, { parser: "tsx" });
|
|
36
|
+
|
|
37
|
+
/** Local identifiers that refer to a renamed export, mapped to the new name. */
|
|
38
|
+
const localRenames = new Map<string, string>();
|
|
39
|
+
/**
|
|
40
|
+
* Whether any import specifier was rewritten. An aliased `Align as Row` adds
|
|
41
|
+
* nothing to `localRenames` — its local name stays `Row` — so counting those
|
|
42
|
+
* alone would treat an alias-only file as untouched and discard the rewrite.
|
|
43
|
+
*/
|
|
44
|
+
let renamedAnImport = false;
|
|
45
|
+
/** Local names of `import * as Flow` namespace imports from a Flow package. */
|
|
46
|
+
const flowNamespaces = new Set<string>();
|
|
47
|
+
|
|
48
|
+
const flowImports = root
|
|
49
|
+
.find(j.ImportDeclaration)
|
|
50
|
+
.filter((path) => isFlowImport(String(path.node.source.value)));
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `imported:local` pairs the file already binds without a rename. A rename
|
|
54
|
+
* that lands on one of these must not add a second specifier for it — the
|
|
55
|
+
* name is available from the import that already carries it, which is also
|
|
56
|
+
* the entry that really exports it.
|
|
57
|
+
*/
|
|
58
|
+
const claimed = new Set<string>();
|
|
59
|
+
|
|
60
|
+
flowImports.forEach((path) => {
|
|
61
|
+
for (const specifier of path.node.specifiers ?? []) {
|
|
62
|
+
if (
|
|
63
|
+
specifier.type === "ImportNamespaceSpecifier" &&
|
|
64
|
+
specifier.local?.name
|
|
65
|
+
) {
|
|
66
|
+
flowNamespaces.add(String(specifier.local.name));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (
|
|
71
|
+
specifier.type !== "ImportSpecifier" ||
|
|
72
|
+
specifier.imported.type !== "Identifier" ||
|
|
73
|
+
renames.has(specifier.imported.name)
|
|
74
|
+
) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
claimed.add(
|
|
79
|
+
`${specifier.imported.name}:${String(
|
|
80
|
+
specifier.local?.name ?? specifier.imported.name,
|
|
81
|
+
)}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
flowImports.forEach((path) => {
|
|
87
|
+
const specifiers = path.node.specifiers ?? [];
|
|
88
|
+
const survivors: typeof specifiers = [];
|
|
89
|
+
|
|
90
|
+
for (const specifier of specifiers) {
|
|
91
|
+
if (
|
|
92
|
+
specifier.type !== "ImportSpecifier" ||
|
|
93
|
+
specifier.imported.type !== "Identifier"
|
|
94
|
+
) {
|
|
95
|
+
survivors.push(specifier);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const imported = specifier.imported.name;
|
|
100
|
+
const renamed = renames.get(imported);
|
|
101
|
+
if (!renamed) {
|
|
102
|
+
survivors.push(specifier);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const local = String(specifier.local?.name ?? imported);
|
|
107
|
+
const isAlias = local !== imported;
|
|
108
|
+
const newLocal = isAlias ? local : renamed;
|
|
109
|
+
const key = `${renamed}:${newLocal}`;
|
|
110
|
+
|
|
111
|
+
renamedAnImport = true;
|
|
112
|
+
if (!isAlias) {
|
|
113
|
+
// An aliased import keeps its local name and needs no further change.
|
|
114
|
+
localRenames.set(local, renamed);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Renaming can collide with a name the file already binds — directly, or
|
|
118
|
+
// because a second old name maps onto the same new one. Either way the
|
|
119
|
+
// binding already exists, so this specifier goes away instead of
|
|
120
|
+
// producing a duplicate declaration, which would not parse.
|
|
121
|
+
const collision = claimed.has(key);
|
|
122
|
+
claimed.add(key);
|
|
123
|
+
if (collision) {
|
|
124
|
+
// A value import must not lose out to a type-only one.
|
|
125
|
+
if (!isTypeOnly(specifier)) {
|
|
126
|
+
for (const kept of survivors) {
|
|
127
|
+
if (
|
|
128
|
+
kept.type === "ImportSpecifier" &&
|
|
129
|
+
kept.imported.type === "Identifier" &&
|
|
130
|
+
`${kept.imported.name}:${String(
|
|
131
|
+
kept.local?.name ?? kept.imported.name,
|
|
132
|
+
)}` === key
|
|
133
|
+
) {
|
|
134
|
+
makeValueImport(kept);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Mutate in place so an `import type` / `type X` modifier survives.
|
|
142
|
+
specifier.imported.name = renamed;
|
|
143
|
+
if (!isAlias && specifier.local) {
|
|
144
|
+
specifier.local.name = renamed;
|
|
145
|
+
}
|
|
146
|
+
survivors.push(specifier);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (survivors.length === specifiers.length) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Every specifier moved to an import that already binds the name. Leaving
|
|
154
|
+
// the declaration behind would turn it into a side-effect import of an
|
|
155
|
+
// entry the file no longer uses.
|
|
156
|
+
if (survivors.length === 0) {
|
|
157
|
+
j(path).remove();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
path.node.specifiers = survivors;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (!renamedAnImport && flowNamespaces.size === 0) {
|
|
165
|
+
return fileInfo.source;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* `JSXIdentifier` extends `Identifier`, so this one pass covers JSX tags,
|
|
170
|
+
* value references and type references alike. Positions where the name is not
|
|
171
|
+
* a reference to the import — an object key, a member's property, a JSX
|
|
172
|
+
* attribute name — are skipped.
|
|
173
|
+
*/
|
|
174
|
+
root.find(j.Identifier).forEach((path) => {
|
|
175
|
+
const parent = path.parent.node;
|
|
176
|
+
|
|
177
|
+
const isNamespaceMember =
|
|
178
|
+
(parent.type === "MemberExpression" ||
|
|
179
|
+
parent.type === "JSXMemberExpression") &&
|
|
180
|
+
parent.property === path.node;
|
|
181
|
+
|
|
182
|
+
if (isNamespaceMember) {
|
|
183
|
+
const object = parent.object;
|
|
184
|
+
if (object.type === "Identifier" || object.type === "JSXIdentifier") {
|
|
185
|
+
const renamed = renames.get(path.node.name);
|
|
186
|
+
if (renamed && flowNamespaces.has(String(object.name))) {
|
|
187
|
+
path.node.name = renamed;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
parent.type === "ImportSpecifier" ||
|
|
195
|
+
parent.type === "JSXAttribute" ||
|
|
196
|
+
((parent.type === "ObjectProperty" || parent.type === "Property") &&
|
|
197
|
+
parent.key === path.node &&
|
|
198
|
+
!parent.computed)
|
|
199
|
+
) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const renamed = localRenames.get(path.node.name);
|
|
204
|
+
if (renamed) {
|
|
205
|
+
path.node.name = renamed;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return root.toSource();
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export default alignToCombineTransform;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { ConditionalExpression, Transform } from "jscodeshift";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An expression that can be a prop's value. Deliberately not
|
|
5
|
+
* `JSXExpressionContainer["expression"]`: that also admits `JSXEmptyExpression`
|
|
6
|
+
* (`color={/* a comment *\/}`), which cannot be written back into a ternary
|
|
7
|
+
* branch. The call site filters it out instead.
|
|
8
|
+
*/
|
|
9
|
+
type ValueNode = ConditionalExpression["consequent"];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Renames the `color="accent"` prop value to `color="success"` on `Button` and
|
|
13
|
+
* `SubmitButton`.
|
|
14
|
+
*
|
|
15
|
+
* The scope is deliberately narrow. Only JSX elements that resolve to one of
|
|
16
|
+
* those components — imported (named or as a namespace) from
|
|
17
|
+
* `@mittwald/flow-react-components` or
|
|
18
|
+
* `@mittwald/flow-remote-react-components`, including their subpath entries —
|
|
19
|
+
* are touched. Same-named components from other packages are left untouched.
|
|
20
|
+
*
|
|
21
|
+
* Only literal `"accent"` values are rewritten — either as the whole value
|
|
22
|
+
* (`color="accent"`, `color={"accent"}`) or in a value position inside a
|
|
23
|
+
* dynamic one (`color={cond ? "secondary" : "accent"}`, `color={override ??
|
|
24
|
+
* "accent"}`). A value the transform cannot see into (`color={someVariable}`)
|
|
25
|
+
* is left alone.
|
|
26
|
+
*/
|
|
27
|
+
const buttonColorAccentToSuccessTransform: Transform = (fileInfo, { j }) => {
|
|
28
|
+
const flowPackages = [
|
|
29
|
+
"@mittwald/flow-react-components",
|
|
30
|
+
"@mittwald/flow-remote-react-components",
|
|
31
|
+
];
|
|
32
|
+
const affectedComponents = new Set(["Button", "SubmitButton"]);
|
|
33
|
+
|
|
34
|
+
const isFlowImport = (source: string): boolean =>
|
|
35
|
+
flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
|
|
36
|
+
|
|
37
|
+
const isAccentLiteral = (node: unknown): boolean => {
|
|
38
|
+
if (!node || typeof node !== "object" || !("type" in node)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const { type } = node as { type: string };
|
|
42
|
+
return (
|
|
43
|
+
(type === "StringLiteral" || type === "Literal") &&
|
|
44
|
+
(node as { value?: unknown }).value === "accent"
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Rewrites every `"accent"` sitting in a position whose value can reach the
|
|
50
|
+
* prop: the expression itself, both branches of a ternary, and the operands
|
|
51
|
+
* of `??`/`||` — plus the right operand of `&&`, since `&&` yields its left
|
|
52
|
+
* operand only when that operand is falsy and a non-empty string never is. A
|
|
53
|
+
* `"accent"` anywhere else is not a value that reaches the prop (an object
|
|
54
|
+
* key, an index into a lookup table) and stays as it is.
|
|
55
|
+
*
|
|
56
|
+
* This cannot move into a shared module: the CLI loads this file straight out
|
|
57
|
+
* of the published package, which ships only
|
|
58
|
+
* `src/migrations/**\/transform.ts` — see this package's AGENTS.md.
|
|
59
|
+
*/
|
|
60
|
+
const rewriteValuePositions = (node: ValueNode): ValueNode => {
|
|
61
|
+
if (isAccentLiteral(node)) {
|
|
62
|
+
return j.stringLiteral("success");
|
|
63
|
+
}
|
|
64
|
+
if (node?.type === "ConditionalExpression") {
|
|
65
|
+
node.consequent = rewriteValuePositions(node.consequent);
|
|
66
|
+
node.alternate = rewriteValuePositions(node.alternate);
|
|
67
|
+
return node;
|
|
68
|
+
}
|
|
69
|
+
if (node?.type === "LogicalExpression") {
|
|
70
|
+
if (node.operator !== "&&") {
|
|
71
|
+
node.left = rewriteValuePositions(node.left);
|
|
72
|
+
}
|
|
73
|
+
node.right = rewriteValuePositions(node.right);
|
|
74
|
+
return node;
|
|
75
|
+
}
|
|
76
|
+
return node;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const root = j(fileInfo.source, { parser: "tsx" });
|
|
80
|
+
|
|
81
|
+
// Local JSX identifier -> canonical component name (resolves `as` aliases).
|
|
82
|
+
const localToComponent = new Map<string, string>();
|
|
83
|
+
// Local names of `import * as Flow` namespace imports from a Flow package.
|
|
84
|
+
const flowNamespaces = new Set<string>();
|
|
85
|
+
|
|
86
|
+
root
|
|
87
|
+
.find(j.ImportDeclaration)
|
|
88
|
+
.filter((path) => isFlowImport(String(path.node.source.value)))
|
|
89
|
+
.forEach((path) => {
|
|
90
|
+
for (const specifier of path.node.specifiers ?? []) {
|
|
91
|
+
if (
|
|
92
|
+
specifier.type === "ImportSpecifier" &&
|
|
93
|
+
specifier.imported.type === "Identifier" &&
|
|
94
|
+
affectedComponents.has(specifier.imported.name)
|
|
95
|
+
) {
|
|
96
|
+
localToComponent.set(
|
|
97
|
+
String(specifier.local?.name ?? specifier.imported.name),
|
|
98
|
+
String(specifier.imported.name),
|
|
99
|
+
);
|
|
100
|
+
} else if (
|
|
101
|
+
specifier.type === "ImportNamespaceSpecifier" &&
|
|
102
|
+
specifier.local
|
|
103
|
+
) {
|
|
104
|
+
flowNamespaces.add(String(specifier.local.name));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (localToComponent.size === 0 && flowNamespaces.size === 0) {
|
|
110
|
+
return fileInfo.source;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
root.find(j.JSXOpeningElement).forEach((path) => {
|
|
114
|
+
const name = path.node.name;
|
|
115
|
+
|
|
116
|
+
let isAffected = false;
|
|
117
|
+
if (name.type === "JSXIdentifier") {
|
|
118
|
+
isAffected = localToComponent.has(name.name);
|
|
119
|
+
} else if (
|
|
120
|
+
name.type === "JSXMemberExpression" &&
|
|
121
|
+
name.object.type === "JSXIdentifier" &&
|
|
122
|
+
name.property.type === "JSXIdentifier"
|
|
123
|
+
) {
|
|
124
|
+
isAffected =
|
|
125
|
+
flowNamespaces.has(name.object.name) &&
|
|
126
|
+
affectedComponents.has(name.property.name);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!isAffected) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const attribute of path.node.attributes ?? []) {
|
|
134
|
+
if (
|
|
135
|
+
attribute.type !== "JSXAttribute" ||
|
|
136
|
+
attribute.name.type !== "JSXIdentifier" ||
|
|
137
|
+
attribute.name.name !== "color"
|
|
138
|
+
) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const value = attribute.value;
|
|
143
|
+
|
|
144
|
+
// color="accent"
|
|
145
|
+
if (isAccentLiteral(value)) {
|
|
146
|
+
attribute.value = j.stringLiteral("success");
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// color={"accent"}, and every value position inside a dynamic value:
|
|
151
|
+
// color={cond ? "accent" : "x"}, color={fallback ?? "accent"}.
|
|
152
|
+
if (
|
|
153
|
+
value?.type === "JSXExpressionContainer" &&
|
|
154
|
+
value.expression.type !== "JSXEmptyExpression"
|
|
155
|
+
) {
|
|
156
|
+
value.expression = rewriteValuePositions(value.expression);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return root.toSource();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export default buttonColorAccentToSuccessTransform;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { Transform } from "jscodeshift";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Replaces the removed `Button` props interfaces with `ButtonProps`
|
|
5
|
+
* (alpha.646).
|
|
6
|
+
*
|
|
7
|
+
* `RemoteButtonElementProps`, `ResetButtonProps` and `SubmitButtonProps` were
|
|
8
|
+
* removed; all three are `ButtonProps`. There is no alias for the old names, so
|
|
9
|
+
* a codebase still using them does not compile.
|
|
10
|
+
*
|
|
11
|
+
* This is more than a rename, because the names do not share an entry.
|
|
12
|
+
* `SubmitButtonProps` and `ResetButtonProps` came from the `react-hook-form`
|
|
13
|
+
* entry, which does not export `ButtonProps` — renaming them in place would
|
|
14
|
+
* swap one import error for another. The specifier therefore moves to the
|
|
15
|
+
* package root of whichever Flow package it came from, joining an existing
|
|
16
|
+
* import from that root or getting a new one.
|
|
17
|
+
*
|
|
18
|
+
* The scope is `@mittwald/flow-react-components` and its subpath entries only.
|
|
19
|
+
* The remote package re-exports the `flr-universal` surface, so it does carry
|
|
20
|
+
* some prop types — `ActionProps`, `ModalProps` and the like — but
|
|
21
|
+
* `ButtonProps` is not among them. There is nothing to move a remote import
|
|
22
|
+
* onto, so a remote codebase has to pick its own source for the type.
|
|
23
|
+
* `RemoteButtonElementProps` is left alone for the same reason, and because
|
|
24
|
+
* `@mittwald/flow-remote-elements` still exports that name today.
|
|
25
|
+
*
|
|
26
|
+
* A same-named import from another package is left alone. A name imported under
|
|
27
|
+
* a local alias (`import { SubmitButtonProps as P }`) keeps its alias — it
|
|
28
|
+
* becomes `ButtonProps as P`. Namespace usages (`Flow.SubmitButtonProps`) are
|
|
29
|
+
* rewritten as well.
|
|
30
|
+
*
|
|
31
|
+
* All three collapse onto `ButtonProps`, so a file importing more than one of
|
|
32
|
+
* them — or one of them next to `ButtonProps` itself — ends up with a single
|
|
33
|
+
* specifier instead of a duplicate declaration, which would not parse.
|
|
34
|
+
*/
|
|
35
|
+
const buttonPropsInterfacesTransform: Transform = (fileInfo, { j }) => {
|
|
36
|
+
const flowPackages = ["@mittwald/flow-react-components"];
|
|
37
|
+
const removed = new Set(["ResetButtonProps", "SubmitButtonProps"]);
|
|
38
|
+
const newName = "ButtonProps";
|
|
39
|
+
|
|
40
|
+
/** The Flow package a module specifier belongs to, or `undefined`. */
|
|
41
|
+
const rootPackageOf = (source: string): string | undefined =>
|
|
42
|
+
flowPackages.find((pkg) => source === pkg || source.startsWith(`${pkg}/`));
|
|
43
|
+
|
|
44
|
+
// ast-types models `importKind` on the declaration only, while babel also
|
|
45
|
+
// puts it on the specifier — which is where a per-specifier `type X` lives.
|
|
46
|
+
const isTypeOnly = (node: object): boolean =>
|
|
47
|
+
(node as { importKind?: string }).importKind === "type";
|
|
48
|
+
|
|
49
|
+
const root = j(fileInfo.source, { parser: "tsx" });
|
|
50
|
+
|
|
51
|
+
/** Local identifiers that have to be renamed (no alias in play). */
|
|
52
|
+
const localRenames = new Set<string>();
|
|
53
|
+
/** Local names of `import * as Flow` namespace imports from a Flow package. */
|
|
54
|
+
const flowNamespaces = new Set<string>();
|
|
55
|
+
/** `package:local` pairs that already import `ButtonProps` from a root. */
|
|
56
|
+
const bound = new Set<string>();
|
|
57
|
+
/** `package:local` pairs that still need importing, and whether type-only. */
|
|
58
|
+
const wanted = new Map<
|
|
59
|
+
string,
|
|
60
|
+
{ pkg: string; local: string; type: boolean }
|
|
61
|
+
>();
|
|
62
|
+
|
|
63
|
+
const flowImports = root
|
|
64
|
+
.find(j.ImportDeclaration)
|
|
65
|
+
.filter((path) => !!rootPackageOf(String(path.node.source.value)));
|
|
66
|
+
|
|
67
|
+
flowImports.forEach((path) => {
|
|
68
|
+
const source = String(path.node.source.value);
|
|
69
|
+
for (const specifier of path.node.specifiers ?? []) {
|
|
70
|
+
if (
|
|
71
|
+
specifier.type === "ImportNamespaceSpecifier" &&
|
|
72
|
+
specifier.local?.name
|
|
73
|
+
) {
|
|
74
|
+
flowNamespaces.add(String(specifier.local.name));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
specifier.type === "ImportSpecifier" &&
|
|
80
|
+
specifier.imported.type === "Identifier" &&
|
|
81
|
+
specifier.imported.name === newName &&
|
|
82
|
+
source === rootPackageOf(source)
|
|
83
|
+
) {
|
|
84
|
+
bound.add(`${source}:${String(specifier.local?.name ?? newName)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
flowImports.forEach((path) => {
|
|
90
|
+
const source = String(path.node.source.value);
|
|
91
|
+
const pkg = rootPackageOf(source) ?? source;
|
|
92
|
+
const declarationIsType = isTypeOnly(path.node);
|
|
93
|
+
const specifiers = path.node.specifiers ?? [];
|
|
94
|
+
const survivors: typeof specifiers = [];
|
|
95
|
+
|
|
96
|
+
for (const specifier of specifiers) {
|
|
97
|
+
if (
|
|
98
|
+
specifier.type !== "ImportSpecifier" ||
|
|
99
|
+
specifier.imported.type !== "Identifier" ||
|
|
100
|
+
!removed.has(specifier.imported.name)
|
|
101
|
+
) {
|
|
102
|
+
survivors.push(specifier);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const imported = specifier.imported.name;
|
|
107
|
+
const local = String(specifier.local?.name ?? imported);
|
|
108
|
+
const isAlias = local !== imported;
|
|
109
|
+
const newLocal = isAlias ? local : newName;
|
|
110
|
+
const key = `${pkg}:${newLocal}`;
|
|
111
|
+
|
|
112
|
+
if (!isAlias) {
|
|
113
|
+
localRenames.add(local);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!bound.has(key)) {
|
|
117
|
+
const existing = wanted.get(key);
|
|
118
|
+
const type = declarationIsType || isTypeOnly(specifier);
|
|
119
|
+
wanted.set(key, {
|
|
120
|
+
pkg,
|
|
121
|
+
local: newLocal,
|
|
122
|
+
// A value import must not lose out to a type-only one.
|
|
123
|
+
type: existing ? existing.type && type : type,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (survivors.length === specifiers.length) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Leaving an emptied declaration behind would turn it into a side-effect
|
|
133
|
+
// import of an entry the file no longer uses.
|
|
134
|
+
if (survivors.length === 0) {
|
|
135
|
+
j(path).remove();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
path.node.specifiers = survivors;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
wanted.size === 0 &&
|
|
144
|
+
localRenames.size === 0 &&
|
|
145
|
+
flowNamespaces.size === 0
|
|
146
|
+
) {
|
|
147
|
+
return fileInfo.source;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const { pkg, local, type } of wanted.values()) {
|
|
151
|
+
const specifier = j.importSpecifier(
|
|
152
|
+
j.identifier(newName),
|
|
153
|
+
j.identifier(local),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Join an import from the package root when the file already has one and
|
|
157
|
+
// it can carry the specifier; otherwise add a declaration of its own.
|
|
158
|
+
const host = root
|
|
159
|
+
.find(j.ImportDeclaration)
|
|
160
|
+
.filter(
|
|
161
|
+
(path) =>
|
|
162
|
+
String(path.node.source.value) === pkg &&
|
|
163
|
+
isTypeOnly(path.node) === type,
|
|
164
|
+
)
|
|
165
|
+
.paths()[0];
|
|
166
|
+
|
|
167
|
+
if (host) {
|
|
168
|
+
host.node.specifiers = [...(host.node.specifiers ?? []), specifier];
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const declaration = j.importDeclaration([specifier], j.literal(pkg));
|
|
173
|
+
if (type) {
|
|
174
|
+
declaration.importKind = "type";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const firstImport = root.find(j.ImportDeclaration).paths()[0];
|
|
178
|
+
if (firstImport) {
|
|
179
|
+
j(firstImport).insertBefore(declaration);
|
|
180
|
+
} else {
|
|
181
|
+
root.get().node.program.body.unshift(declaration);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* `JSXIdentifier` extends `Identifier`, so this one pass covers value
|
|
187
|
+
* references and type references alike. Positions where the name is not a
|
|
188
|
+
* reference to the import — an object key, a member's property, a JSX
|
|
189
|
+
* attribute name — are skipped.
|
|
190
|
+
*/
|
|
191
|
+
root.find(j.Identifier).forEach((path) => {
|
|
192
|
+
const parent = path.parent.node;
|
|
193
|
+
|
|
194
|
+
const isNamespaceMember =
|
|
195
|
+
(parent.type === "MemberExpression" ||
|
|
196
|
+
parent.type === "JSXMemberExpression") &&
|
|
197
|
+
parent.property === path.node;
|
|
198
|
+
|
|
199
|
+
if (isNamespaceMember) {
|
|
200
|
+
const object = parent.object;
|
|
201
|
+
if (
|
|
202
|
+
(object.type === "Identifier" || object.type === "JSXIdentifier") &&
|
|
203
|
+
removed.has(path.node.name) &&
|
|
204
|
+
flowNamespaces.has(String(object.name))
|
|
205
|
+
) {
|
|
206
|
+
path.node.name = newName;
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (
|
|
212
|
+
parent.type === "ImportSpecifier" ||
|
|
213
|
+
parent.type === "JSXAttribute" ||
|
|
214
|
+
((parent.type === "ObjectProperty" || parent.type === "Property") &&
|
|
215
|
+
parent.key === path.node &&
|
|
216
|
+
!parent.computed)
|
|
217
|
+
) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (localRenames.has(path.node.name)) {
|
|
222
|
+
path.node.name = newName;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return root.toSource();
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export default buttonPropsInterfacesTransform;
|