@mittwald/flow-codemods 1.1.26 → 1.1.28

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
- for (const attribute of attributes) {
75
- if (isNamed(attribute, "action") && attribute.type === "JSXAttribute") {
76
- attribute.name.name = "onAction";
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();
@@ -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}`. Check every site where you passed a reference rather than an inline arrow; the codemod renames the prop but cannot decide this one from the source.",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/flow-codemods",
3
- "version": "1.1.26",
3
+ "version": "1.1.28",
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": "76f0ae2c5750965b7d0d2057dedecae70474c26f"
40
+ "gitHead": "620d70dc1c876b78cd6a7e693f79ce315088f757"
41
41
  }