@mittwald/flow-codemods 1.1.0 → 1.1.2

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.
Files changed (29) hide show
  1. package/dist/migrations/accent-box-color-to-background-color/transform.js +175 -0
  2. package/dist/migrations/action-prop-to-on-action/transform.js +82 -0
  3. package/dist/migrations/align-to-combine/transform.js +166 -0
  4. package/dist/migrations/button-color-accent-to-success/transform.js +126 -0
  5. package/dist/migrations/button-props-interfaces/transform.js +178 -0
  6. package/dist/migrations/color-primary-to-default/transform.js +135 -0
  7. package/dist/migrations/imports-to-package-root/transform.js +93 -0
  8. package/dist/migrations/muted-action-error-to-abort-action-error/transform.js +198 -0
  9. package/dist/migrations/package.json +3 -0
  10. package/dist/migrations/password-tools-rule/transform.js +173 -0
  11. package/dist/migrations/password-tools-subpath-renamed/transform.js +51 -0
  12. package/dist/migrations.generated.js +1 -1
  13. package/dist/run/jscodeshift.d.ts +4 -1
  14. package/dist/run/jscodeshift.d.ts.map +1 -1
  15. package/dist/run/jscodeshift.js +60 -22
  16. package/dist/tools/package.json +3 -0
  17. package/dist/tools/to-remote-package.js +29 -0
  18. package/package.json +4 -6
  19. package/src/migrations/accent-box-color-to-background-color/transform.ts +0 -218
  20. package/src/migrations/action-prop-to-on-action/transform.ts +0 -110
  21. package/src/migrations/align-to-combine/transform.ts +0 -212
  22. package/src/migrations/button-color-accent-to-success/transform.ts +0 -164
  23. package/src/migrations/button-props-interfaces/transform.ts +0 -229
  24. package/src/migrations/color-primary-to-default/transform.ts +0 -173
  25. package/src/migrations/imports-to-package-root/transform.ts +0 -107
  26. package/src/migrations/muted-action-error-to-abort-action-error/transform.ts +0 -260
  27. package/src/migrations/password-tools-rule/transform.ts +0 -219
  28. package/src/migrations/password-tools-subpath-renamed/transform.ts +0 -61
  29. package/src/tools/to-remote-package.ts +0 -37
@@ -1,229 +0,0 @@
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;
@@ -1,173 +0,0 @@
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="primary"` prop value to `color="default"` on the five
13
- * components changed by the alpha.837 design update: `Breadcrumb`,
14
- * `HeaderNavigation`, `Heading`, `IllustratedMessage` and `Link`.
15
- *
16
- * The scope is deliberately narrow. Only JSX elements that resolve to one of
17
- * those components — imported (named or as a namespace) from
18
- * `@mittwald/flow-react-components` or
19
- * `@mittwald/flow-remote-react-components`, including their subpath entries —
20
- * are touched. Components that still accept `color="primary"` (e.g. `Button`)
21
- * and same-named elements from other packages (e.g. a router `Link`) are left
22
- * untouched.
23
- *
24
- * Only literal `"primary"` values are rewritten — either as the whole value
25
- * (`color="primary"`, `color={"primary"}`) or in a value position inside a
26
- * dynamic one (`color={cond ? "secondary" : "primary"}`, `color={override ??
27
- * "primary"}`). A value the transform cannot see into (`color={someVariable}`)
28
- * is left alone.
29
- */
30
- const colorPrimaryToDefaultTransform: Transform = (fileInfo, { j }) => {
31
- const flowPackages = [
32
- "@mittwald/flow-react-components",
33
- "@mittwald/flow-remote-react-components",
34
- ];
35
- const affectedComponents = new Set([
36
- "Breadcrumb",
37
- "HeaderNavigation",
38
- "Heading",
39
- "IllustratedMessage",
40
- "Link",
41
- ]);
42
-
43
- const isFlowImport = (source: string): boolean =>
44
- flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
45
-
46
- const isPrimaryLiteral = (node: unknown): boolean => {
47
- if (!node || typeof node !== "object" || !("type" in node)) {
48
- return false;
49
- }
50
- const { type } = node as { type: string };
51
- return (
52
- (type === "StringLiteral" || type === "Literal") &&
53
- (node as { value?: unknown }).value === "primary"
54
- );
55
- };
56
-
57
- /**
58
- * Rewrites every `"primary"` sitting in a position whose value can reach the
59
- * prop: the expression itself, both branches of a ternary, and the operands
60
- * of `??`/`||` — plus the right operand of `&&`, since `&&` yields its left
61
- * operand only when that operand is falsy and a non-empty string never is. A
62
- * `"primary"` anywhere else is not a value that reaches the prop (an object
63
- * key, an index into a lookup table) and stays as it is.
64
- *
65
- * This cannot move into a shared module: the CLI loads this file straight out
66
- * of the published package, which ships only
67
- * `src/migrations/**\/transform.ts` — see this package's AGENTS.md.
68
- */
69
- const rewriteValuePositions = (node: ValueNode): ValueNode => {
70
- if (isPrimaryLiteral(node)) {
71
- return j.stringLiteral("default");
72
- }
73
- if (node?.type === "ConditionalExpression") {
74
- node.consequent = rewriteValuePositions(node.consequent);
75
- node.alternate = rewriteValuePositions(node.alternate);
76
- return node;
77
- }
78
- if (node?.type === "LogicalExpression") {
79
- if (node.operator !== "&&") {
80
- node.left = rewriteValuePositions(node.left);
81
- }
82
- node.right = rewriteValuePositions(node.right);
83
- return node;
84
- }
85
- return node;
86
- };
87
-
88
- const root = j(fileInfo.source, { parser: "tsx" });
89
-
90
- // Local JSX identifier -> canonical component name (resolves `as` aliases).
91
- const localToComponent = new Map<string, string>();
92
- // Local names of `import * as Flow` namespace imports from a Flow package.
93
- const flowNamespaces = new Set<string>();
94
-
95
- root
96
- .find(j.ImportDeclaration)
97
- .filter((path) => isFlowImport(String(path.node.source.value)))
98
- .forEach((path) => {
99
- for (const specifier of path.node.specifiers ?? []) {
100
- if (
101
- specifier.type === "ImportSpecifier" &&
102
- specifier.imported.type === "Identifier" &&
103
- affectedComponents.has(specifier.imported.name)
104
- ) {
105
- localToComponent.set(
106
- String(specifier.local?.name ?? specifier.imported.name),
107
- String(specifier.imported.name),
108
- );
109
- } else if (
110
- specifier.type === "ImportNamespaceSpecifier" &&
111
- specifier.local
112
- ) {
113
- flowNamespaces.add(String(specifier.local.name));
114
- }
115
- }
116
- });
117
-
118
- if (localToComponent.size === 0 && flowNamespaces.size === 0) {
119
- return fileInfo.source;
120
- }
121
-
122
- root.find(j.JSXOpeningElement).forEach((path) => {
123
- const name = path.node.name;
124
-
125
- let isAffected = false;
126
- if (name.type === "JSXIdentifier") {
127
- isAffected = localToComponent.has(name.name);
128
- } else if (
129
- name.type === "JSXMemberExpression" &&
130
- name.object.type === "JSXIdentifier" &&
131
- name.property.type === "JSXIdentifier"
132
- ) {
133
- isAffected =
134
- flowNamespaces.has(name.object.name) &&
135
- affectedComponents.has(name.property.name);
136
- }
137
-
138
- if (!isAffected) {
139
- return;
140
- }
141
-
142
- for (const attribute of path.node.attributes ?? []) {
143
- if (
144
- attribute.type !== "JSXAttribute" ||
145
- attribute.name.type !== "JSXIdentifier" ||
146
- attribute.name.name !== "color"
147
- ) {
148
- continue;
149
- }
150
-
151
- const value = attribute.value;
152
-
153
- // color="primary"
154
- if (isPrimaryLiteral(value)) {
155
- attribute.value = j.stringLiteral("default");
156
- continue;
157
- }
158
-
159
- // color={"primary"}, and every value position inside a dynamic value:
160
- // color={cond ? "primary" : "x"}, color={fallback ?? "primary"}.
161
- if (
162
- value?.type === "JSXExpressionContainer" &&
163
- value.expression.type !== "JSXEmptyExpression"
164
- ) {
165
- value.expression = rewriteValuePositions(value.expression);
166
- }
167
- }
168
- });
169
-
170
- return root.toSource();
171
- };
172
-
173
- export default colorPrimaryToDefaultTransform;
@@ -1,107 +0,0 @@
1
- import type { Transform } from "jscodeshift";
2
-
3
- /**
4
- * Subpaths this transform must leave alone. Two groups, and they are different
5
- * kinds of fact.
6
- *
7
- * **Subpaths the package still exports.** alpha.28 collapsed 94 subpath exports
8
- * onto the package root, and performing that collapse is this transform's whole
9
- * job — but it did it with a catch-all `else`, so every subpath introduced
10
- * _after_ alpha.28 was collateral. That stayed invisible while selection had a
11
- * lower bound; it no longer has one (see `selectEntries`), so the transform now
12
- * reaches current code, where flattening `all-layered.css` silently turns a
13
- * stylesheet import into a JS one and flattening `mittwald-password-tools-js`
14
- * moves `Rule` onto a root that does not export it.
15
- *
16
- * **Subpaths another entry owns.** `password-tools` is renamed to
17
- * `mittwald-password-tools-js` by `password-tools-subpath-renamed`, whose
18
- * `since` sorts it _after_ this entry. Flattening it first leaves that
19
- * migration nothing to find, and both report success.
20
- *
21
- * Matched exactly, not by prefix: `react-hook-form/useFoo` still collapses onto
22
- * `react-hook-form`, which is what alpha.28 did to it.
23
- *
24
- * `transform.test.ts` beside this file holds the first group against the
25
- * package's real `exports` map, so a new subpath cannot quietly become
26
- * collateral again.
27
- */
28
- const keptSubpaths = new Set([
29
- // the current export surface
30
- "internal",
31
- "flr-universal",
32
- "nextjs",
33
- "react-hook-form",
34
- "mittwald-password-tools-js",
35
- "all.css",
36
- "all-layered.css",
37
- "component-index",
38
- "doc-properties",
39
- // owned by password-tools-subpath-renamed
40
- "password-tools",
41
- ]);
42
-
43
- const importsToPackageRootTransform: Transform = (fileInfo, { j }) => {
44
- const flowPackage = "@mittwald/flow-react-components";
45
-
46
- const root = j(fileInfo.source, {
47
- parser: "ts",
48
- });
49
-
50
- root
51
- .find(j.ImportDeclaration)
52
- .filter((i) => String(i.node.source.value).startsWith(`${flowPackage}/`))
53
- .forEach((i) => {
54
- const specifiers = i.node.specifiers ?? [];
55
- const importPath = String(i.node.source.value);
56
- const importRelativePath = importPath.slice(flowPackage.length + 1);
57
-
58
- if (keptSubpaths.has(importRelativePath)) {
59
- return;
60
- }
61
-
62
- if (
63
- importRelativePath === "all.css" ||
64
- importRelativePath === "globals.css" ||
65
- importRelativePath === "global.css"
66
- ) {
67
- i.node.source.value = `${flowPackage}/all.css`;
68
- return;
69
- }
70
-
71
- if (importRelativePath.startsWith("react-hook-form")) {
72
- i.node.source.value = `${flowPackage}/react-hook-form`;
73
- } else if (importRelativePath.startsWith("nextjs")) {
74
- i.node.source.value = `${flowPackage}/nextjs`;
75
- } else {
76
- i.node.source.value = flowPackage;
77
- }
78
-
79
- specifiers.forEach((s, i) => {
80
- if (
81
- s.type === "ImportDefaultSpecifier" ||
82
- s.type === "ImportSpecifier"
83
- ) {
84
- // `name` is typed `string | Identifier` in ast-types; for the
85
- // specifiers handled here it is always the plain name.
86
- const name = String(
87
- (s.type === "ImportDefaultSpecifier"
88
- ? s.local?.name
89
- : s.imported?.name) ?? "",
90
- );
91
-
92
- specifiers[i] = {
93
- ...s,
94
- type: "ImportSpecifier",
95
- imported: {
96
- type: "Identifier",
97
- name,
98
- },
99
- };
100
- }
101
- });
102
- });
103
-
104
- return root.toSource();
105
- };
106
-
107
- export default importsToPackageRootTransform;