@mittwald/flow-codemods 1.1.27 → 1.1.29
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.
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
/**
|
|
4
|
-
* Renames the `action` prop to `onAction` on `Action
|
|
4
|
+
* Renames the `action` prop to `onAction` on `Action`, and wraps a bare
|
|
5
|
+
* function reference passed to it in an inline call.
|
|
5
6
|
*
|
|
6
7
|
* The scope is deliberately narrow. Only JSX elements that resolve to `Action`
|
|
7
8
|
* — imported (named or as a namespace) from `@mittwald/flow-react-components`
|
|
@@ -14,6 +15,44 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
14
15
|
* An element that already carries `onAction` keeps it and only loses the stale
|
|
15
16
|
* `action` prop, which mirrors what the runtime fallback does: an explicit
|
|
16
17
|
* `onAction` wins.
|
|
18
|
+
*
|
|
19
|
+
* ## Why the wrap is unconditional
|
|
20
|
+
*
|
|
21
|
+
* `onAction` is typed `ActionFn` (`(...args: unknown[]) => unknown`), so a
|
|
22
|
+
* reference to a function declaring a parameter of any narrower type — the
|
|
23
|
+
* usual case being `controller.close`, typed `(options?: CloseOverlayOptions)
|
|
24
|
+
* => void` — does not type-check. Whether a given reference declares such a
|
|
25
|
+
* parameter needs type information and is not decidable from the source, which
|
|
26
|
+
* is why this transform used to leave every reference alone and `apply` asked
|
|
27
|
+
* for a manual pass over all of them.
|
|
28
|
+
*
|
|
29
|
+
* Deciding _whether_ to wrap needs the type. _Performing_ the wrap does not:
|
|
30
|
+
* `() => fn()` calls exactly what `fn` would have been called with by `Action`,
|
|
31
|
+
* minus the arguments — and `onAction` is documented as taking none. So the
|
|
32
|
+
* wrap fixes the reference that needed it and is a no-op for the rest, which
|
|
33
|
+
* makes the decision unnecessary.
|
|
34
|
+
*
|
|
35
|
+
* Two cases it can still change, neither decidable from the source:
|
|
36
|
+
*
|
|
37
|
+
* - A handler that reads the argument `Action` forwards (the trigger's event,
|
|
38
|
+
* e.g. a `PressEvent` from the `Button` inside) and declares it optional or
|
|
39
|
+
* as a rest parameter, so that it type-checks both before and after. It stops
|
|
40
|
+
* receiving that argument, silently. Nothing in Flow documents `onAction` as
|
|
41
|
+
* receiving one.
|
|
42
|
+
* - A possibly-undefined reference, `onAction={props.onAction}`. `undefined` is a
|
|
43
|
+
* valid value for the prop, but calling it is not, so the wrap turns
|
|
44
|
+
* something TypeScript accepted into something it rejects — loudly, at the
|
|
45
|
+
* call site, which is why this is a compile error to fix rather than a
|
|
46
|
+
* regression to find.
|
|
47
|
+
*
|
|
48
|
+
* `apply` names both.
|
|
49
|
+
*
|
|
50
|
+
* Only a bare reference is wrapped: a plain identifier or a member expression.
|
|
51
|
+
* An arrow function and a function expression already are the handler. A call
|
|
52
|
+
* (`makeHandler()`, `close.bind(controller)`) _produces_ the handler, so
|
|
53
|
+
* wrapping it would call the factory on every trigger and throw its result
|
|
54
|
+
* away. A conditional, a `??` chain or an optional member expression is not a
|
|
55
|
+
* reference to one function, so wrapping would be a guess.
|
|
17
56
|
*/
|
|
18
57
|
const actionPropToOnActionTransform = (fileInfo, { j }) => {
|
|
19
58
|
const flowPackages = [
|
|
@@ -22,6 +61,27 @@ const actionPropToOnActionTransform = (fileInfo, { j }) => {
|
|
|
22
61
|
];
|
|
23
62
|
const affectedComponents = new Set(["Action"]);
|
|
24
63
|
const isFlowImport = (source) => flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
|
|
64
|
+
/**
|
|
65
|
+
* Whether a member expression is rooted in a plain reference —
|
|
66
|
+
* `controller.close`, `this.handleSave`, `store.modal.controller.close` —
|
|
67
|
+
* rather than in something evaluated, as `makeController().close` is.
|
|
68
|
+
*
|
|
69
|
+
* An optional link disqualifies the chain. `controller?.close` parses as
|
|
70
|
+
* `OptionalMemberExpression` and never reaches here at all; walking the chain
|
|
71
|
+
* catches a mixed one like `(a?.b).c`. Declining is deliberate: `() =>
|
|
72
|
+
* controller?.close()` short-circuits differently from the value it would
|
|
73
|
+
* replace, and the spelling says the reference may not be there.
|
|
74
|
+
*/
|
|
75
|
+
const hasPlainObjectChain = (expression) => {
|
|
76
|
+
const object = expression.object;
|
|
77
|
+
if (!object || typeof object !== "object" || !("type" in object)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const typed = object;
|
|
81
|
+
return (typed.type === "ThisExpression" ||
|
|
82
|
+
typed.type === "Identifier" ||
|
|
83
|
+
(typed.type === "MemberExpression" && hasPlainObjectChain(typed)));
|
|
84
|
+
};
|
|
25
85
|
const root = j(fileInfo.source, { parser: "tsx" });
|
|
26
86
|
// Local JSX identifier -> canonical component name (resolves `as` aliases).
|
|
27
87
|
const localToComponent = new Map();
|
|
@@ -69,12 +129,38 @@ const actionPropToOnActionTransform = (fileInfo, { j }) => {
|
|
|
69
129
|
const hasOnAction = attributes.some((attribute) => isNamed(attribute, "onAction"));
|
|
70
130
|
if (hasOnAction) {
|
|
71
131
|
path.node.attributes = attributes.filter((attribute) => !isNamed(attribute, "action"));
|
|
72
|
-
return;
|
|
73
132
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
attribute.
|
|
133
|
+
else {
|
|
134
|
+
for (const attribute of attributes) {
|
|
135
|
+
if (isNamed(attribute, "action") && attribute.type === "JSXAttribute") {
|
|
136
|
+
attribute.name.name = "onAction";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Whatever the prop was called before, the surviving `onAction` is the one
|
|
141
|
+
// that needs the wrap — including one the consumer had already renamed by
|
|
142
|
+
// hand, which carries the same type error.
|
|
143
|
+
for (const attribute of path.node.attributes ?? []) {
|
|
144
|
+
if (attribute.type !== "JSXAttribute" ||
|
|
145
|
+
!isNamed(attribute, "onAction")) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const container = attribute.value;
|
|
149
|
+
if (container?.type !== "JSXExpressionContainer") {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
// A bare reference is an identifier or a member expression. Everything
|
|
153
|
+
// else either is the handler already or is a way of producing one.
|
|
154
|
+
const expression = container.expression;
|
|
155
|
+
if (expression.type !== "Identifier" &&
|
|
156
|
+
expression.type !== "MemberExpression") {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (expression.type === "MemberExpression" &&
|
|
160
|
+
!hasPlainObjectChain(expression)) {
|
|
161
|
+
continue;
|
|
77
162
|
}
|
|
163
|
+
attribute.value = j.jsxExpressionContainer(j.arrowFunctionExpression([], j.callExpression(expression, [])));
|
|
78
164
|
}
|
|
79
165
|
});
|
|
80
166
|
return root.toSource();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const oldPath = "@mittwald/flow-react-components/styles";
|
|
4
|
+
const newPath = "@mittwald/flow-react-components/all.css";
|
|
5
|
+
/**
|
|
6
|
+
* Rewrites the module specifier `@mittwald/flow-react-components/styles` to
|
|
7
|
+
* `@mittwald/flow-react-components/all.css` (alpha.292).
|
|
8
|
+
*
|
|
9
|
+
* The same shape as `password-tools-subpath-renamed`: one exact specifier, in
|
|
10
|
+
* every form that names a module — `import`, `import type`, `export … from`, a
|
|
11
|
+
* side-effect import, `import()` and `require()`. In practice it is nearly
|
|
12
|
+
* always the side-effect form (`import "…/styles"`), which is why that case is
|
|
13
|
+
* the one the fixtures lead with.
|
|
14
|
+
*
|
|
15
|
+
* Exact match, not a prefix: `./styles` was a single export with nothing
|
|
16
|
+
* underneath it, so there is no deeper path to rewrite and no risk of touching
|
|
17
|
+
* an unrelated `styles` of someone else's.
|
|
18
|
+
*
|
|
19
|
+
* What this cannot reach is an `@import` in a `.css` or `.scss` file —
|
|
20
|
+
* jscodeshift only walks the JavaScript and TypeScript extensions. `apply` says
|
|
21
|
+
* so.
|
|
22
|
+
*/
|
|
23
|
+
const renamedCssExportTransform = (fileInfo, { j }) => {
|
|
24
|
+
const root = j(fileInfo.source, { parser: "tsx" });
|
|
25
|
+
let changed = false;
|
|
26
|
+
const rewrite = (node) => {
|
|
27
|
+
if (node.value === oldPath) {
|
|
28
|
+
node.value = newPath;
|
|
29
|
+
changed = true;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
// import … from "…", import "…"
|
|
33
|
+
root.find(j.ImportDeclaration).forEach((path) => rewrite(path.node.source));
|
|
34
|
+
// export … from "…", export * from "…"
|
|
35
|
+
root
|
|
36
|
+
.find(j.ExportNamedDeclaration)
|
|
37
|
+
.forEach((path) => path.node.source && rewrite(path.node.source));
|
|
38
|
+
root
|
|
39
|
+
.find(j.ExportAllDeclaration)
|
|
40
|
+
.forEach((path) => rewrite(path.node.source));
|
|
41
|
+
// import("…") and require("…")
|
|
42
|
+
root
|
|
43
|
+
.find(j.CallExpression)
|
|
44
|
+
.filter((path) => {
|
|
45
|
+
const callee = path.node.callee;
|
|
46
|
+
return (callee.type === "Import" ||
|
|
47
|
+
(callee.type === "Identifier" && callee.name === "require"));
|
|
48
|
+
})
|
|
49
|
+
.forEach((path) => {
|
|
50
|
+
const [argument] = path.node.arguments;
|
|
51
|
+
if (argument &&
|
|
52
|
+
(argument.type === "StringLiteral" || argument.type === "Literal")) {
|
|
53
|
+
rewrite(argument);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
// Returning the source untouched keeps this file out of the "changed" count.
|
|
57
|
+
return changed ? root.toSource() : fileInfo.source;
|
|
58
|
+
};
|
|
59
|
+
exports.default = renamedCssExportTransform;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Drops `maxWidth` from `TableColumn`, and drops `width`/`minWidth` where they
|
|
5
|
+
* were `null` (alpha.956).
|
|
6
|
+
*
|
|
7
|
+
* Two changes with different decidability, which is why the transform covers
|
|
8
|
+
* both but not equally:
|
|
9
|
+
*
|
|
10
|
+
* - **`maxWidth` always goes.** The prop was removed from the type, so an
|
|
11
|
+
* explicit `maxWidth` attribute is wrong whatever its value is —
|
|
12
|
+
* `maxWidth={400}` and `maxWidth={computed}` alike. Removing it does change
|
|
13
|
+
* the rendered column (there is no maximum any more); that is the migration,
|
|
14
|
+
* not a side effect.
|
|
15
|
+
* - **`width` and `minWidth` only go when the source says `null`.** The type
|
|
16
|
+
* dropped `null`, and `null` meant "no explicit width", which is what
|
|
17
|
+
* omitting the prop means now. Only the literal is decidable:
|
|
18
|
+
* `width={maybeNull}` could be anything at runtime and is left alone, the way
|
|
19
|
+
* `accent-box-color-to-background-color` leaves `color={expression}`.
|
|
20
|
+
*
|
|
21
|
+
* The scope is the same as its neighbours': only JSX elements that resolve to
|
|
22
|
+
* `TableColumn` — imported named, aliased, or through a namespace, from
|
|
23
|
+
* `@mittwald/flow-react-components` or
|
|
24
|
+
* `@mittwald/flow-remote-react-components`, including their subpath entries.
|
|
25
|
+
*
|
|
26
|
+
* A spread (`<TableColumn {...props} />`) carrying `maxWidth` is invisible
|
|
27
|
+
* here. The explicit attributes beside it are still handled; `apply` names the
|
|
28
|
+
* gap.
|
|
29
|
+
*/
|
|
30
|
+
const tableColumnWidthPropsTransform = (fileInfo, { j }) => {
|
|
31
|
+
const flowPackages = [
|
|
32
|
+
"@mittwald/flow-react-components",
|
|
33
|
+
"@mittwald/flow-remote-react-components",
|
|
34
|
+
];
|
|
35
|
+
const affectedComponents = new Set(["TableColumn"]);
|
|
36
|
+
/** Removed from the type — an explicit attribute is wrong at any value. */
|
|
37
|
+
const removedProps = new Set(["maxWidth"]);
|
|
38
|
+
/** Retyped without `null` — remove only where the source literally says so. */
|
|
39
|
+
const nullableProps = new Set(["width", "minWidth"]);
|
|
40
|
+
const isFlowImport = (source) => flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
|
|
41
|
+
/**
|
|
42
|
+
* Whether this JSX attribute value is the literal `null`.
|
|
43
|
+
*
|
|
44
|
+
* Both spellings, because which one appears depends on the parser: babel
|
|
45
|
+
* emits `NullLiteral`, esprima-style parsers a `Literal` whose `value` is
|
|
46
|
+
* `null`. Anything else — an identifier, a call, a ternary — is not decidable
|
|
47
|
+
* from the source and stays.
|
|
48
|
+
*/
|
|
49
|
+
const isNullLiteral = (value) => {
|
|
50
|
+
if (!value || typeof value !== "object" || !("type" in value)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const container = value;
|
|
54
|
+
if (container.type !== "JSXExpressionContainer") {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const expression = container.expression;
|
|
58
|
+
if (!expression ||
|
|
59
|
+
typeof expression !== "object" ||
|
|
60
|
+
!("type" in expression)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const typed = expression;
|
|
64
|
+
return (typed.type === "NullLiteral" ||
|
|
65
|
+
(typed.type === "Literal" && typed.value === null));
|
|
66
|
+
};
|
|
67
|
+
const root = j(fileInfo.source, { parser: "tsx" });
|
|
68
|
+
// Local JSX identifier -> canonical component name (resolves `as` aliases).
|
|
69
|
+
const localToComponent = new Map();
|
|
70
|
+
// Local names of `import * as Flow` namespace imports from a Flow package.
|
|
71
|
+
const flowNamespaces = new Set();
|
|
72
|
+
root
|
|
73
|
+
.find(j.ImportDeclaration)
|
|
74
|
+
.filter((path) => isFlowImport(String(path.node.source.value)))
|
|
75
|
+
.forEach((path) => {
|
|
76
|
+
for (const specifier of path.node.specifiers ?? []) {
|
|
77
|
+
if (specifier.type === "ImportSpecifier" &&
|
|
78
|
+
specifier.imported.type === "Identifier" &&
|
|
79
|
+
affectedComponents.has(specifier.imported.name)) {
|
|
80
|
+
localToComponent.set(String(specifier.local?.name ?? specifier.imported.name), String(specifier.imported.name));
|
|
81
|
+
}
|
|
82
|
+
else if (specifier.type === "ImportNamespaceSpecifier" &&
|
|
83
|
+
specifier.local) {
|
|
84
|
+
flowNamespaces.add(String(specifier.local.name));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
if (localToComponent.size === 0 && flowNamespaces.size === 0) {
|
|
89
|
+
return fileInfo.source;
|
|
90
|
+
}
|
|
91
|
+
let changed = false;
|
|
92
|
+
root.find(j.JSXOpeningElement).forEach((path) => {
|
|
93
|
+
const name = path.node.name;
|
|
94
|
+
let isAffected = false;
|
|
95
|
+
if (name.type === "JSXIdentifier") {
|
|
96
|
+
isAffected = localToComponent.has(name.name);
|
|
97
|
+
}
|
|
98
|
+
else if (name.type === "JSXMemberExpression" &&
|
|
99
|
+
name.object.type === "JSXIdentifier" &&
|
|
100
|
+
name.property.type === "JSXIdentifier") {
|
|
101
|
+
isAffected =
|
|
102
|
+
flowNamespaces.has(name.object.name) &&
|
|
103
|
+
affectedComponents.has(name.property.name);
|
|
104
|
+
}
|
|
105
|
+
if (!isAffected) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const attributes = path.node.attributes ?? [];
|
|
109
|
+
const kept = attributes.filter((attribute) => {
|
|
110
|
+
if (attribute.type !== "JSXAttribute" ||
|
|
111
|
+
attribute.name.type !== "JSXIdentifier") {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
const attributeName = attribute.name.name;
|
|
115
|
+
if (removedProps.has(attributeName)) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (nullableProps.has(attributeName) && isNullLiteral(attribute.value)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
});
|
|
123
|
+
if (kept.length !== attributes.length) {
|
|
124
|
+
path.node.attributes = kept;
|
|
125
|
+
changed = true;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// Returning the source untouched keeps this file out of the "changed" count.
|
|
129
|
+
return changed ? root.toSource() : fileInfo.source;
|
|
130
|
+
};
|
|
131
|
+
exports.default = tableColumnWidthPropsTransform;
|
|
@@ -98,9 +98,9 @@ export const migrations = [
|
|
|
98
98
|
since: "0.2.0-alpha.956",
|
|
99
99
|
title: "TableColumn: `maxWidth` removed, `width` and `minWidth` retyped",
|
|
100
100
|
kind: "migration",
|
|
101
|
-
action: "
|
|
101
|
+
action: "codemod",
|
|
102
102
|
remotePackage: true,
|
|
103
|
-
apply: "Remove `maxWidth` from every `TableColumn`. Where `width` or `minWidth` was `null`, omit the prop instead — the type no longer accepts `null`, only `number | string`.",
|
|
103
|
+
apply: "Remove `maxWidth` from every `TableColumn`. Where `width` or `minWidth` was `null`, omit the prop instead — the type no longer accepts `null`, only `number | string`. A codemod does both for the cases it can decide from the source. Two it declines: a `width`/`minWidth` whose value is an expression (`width={maybeNull}`), and a spread that might carry `maxWidth` (`<TableColumn {...props} />`). Check those by hand.",
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
106
|
id: "table-render-prop-removed",
|
|
@@ -208,7 +208,7 @@ export const migrations = [
|
|
|
208
208
|
kind: "migration",
|
|
209
209
|
action: "codemod",
|
|
210
210
|
remotePackage: true,
|
|
211
|
-
apply: "Rename the `action` prop on `Action` to `onAction`. Not only a rename: the new prop is typed `ActionFn` (`(...args: unknown[]) => unknown`), so a function *reference* that declares a parameter no longer type-checks and needs wrapping — `onAction={() => controller.close()}` rather than `onAction={controller.close}`.
|
|
211
|
+
apply: "Rename the `action` prop on `Action` to `onAction`. Not only a rename: the new prop is typed `ActionFn` (`(...args: unknown[]) => unknown`), so a function *reference* that declares a parameter no longer type-checks and needs wrapping — `onAction={() => controller.close()}` rather than `onAction={controller.close}`. A codemod does both. It wraps every bare reference (`close`, `controller.close`), because the wrap is a no-op for a reference that did not need it. It leaves a value that already is the handler or produces one — an arrow function, a function expression, a call like `makeHandler()` or `close.bind(controller)` — and anything that is not one reference, such as `isOpen ? close : open` or `controller?.close`. Two wraps to look at afterwards: a handler that read the event `Action` forwards stops receiving it, and a possibly-undefined reference (`onAction={props.onAction}`) becomes a call TypeScript rejects — add the guard it asks for.",
|
|
212
212
|
},
|
|
213
213
|
{
|
|
214
214
|
id: "button-props-interfaces",
|
|
@@ -233,8 +233,8 @@ export const migrations = [
|
|
|
233
233
|
since: "0.1.0-alpha.292",
|
|
234
234
|
title: "Renamed CSS export",
|
|
235
235
|
kind: "migration",
|
|
236
|
-
action: "
|
|
236
|
+
action: "codemod",
|
|
237
237
|
remotePackage: false,
|
|
238
|
-
apply: "Replace the import `@mittwald/flow-react-components/styles` with `@mittwald/flow-react-components/all.css`.",
|
|
238
|
+
apply: "Replace the import `@mittwald/flow-react-components/styles` with `@mittwald/flow-react-components/all.css`. A codemod does this for JavaScript and TypeScript files. An `@import` of the old path inside a `.css` or `.scss` file is not covered — search for it by hand.",
|
|
239
239
|
},
|
|
240
240
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mittwald/flow-codemods",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.29",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Codemods and an upgrade CLI for consumers of Flow, mittwald's design system",
|
|
6
6
|
"homepage": "https://flow.mittwald.de",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"vitest": "^4.1.11",
|
|
38
38
|
"yaml": "^2.8.1"
|
|
39
39
|
},
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "9c65be4254837ed7a23e6ef79e07ca6717938d19"
|
|
41
41
|
}
|