@mittwald/flow-codemods 1.2.0-next.0 → 1.2.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.
Files changed (44) hide show
  1. package/README.md +35 -6
  2. package/dist/cli/codemod.d.ts +16 -0
  3. package/dist/cli/codemod.d.ts.map +1 -1
  4. package/dist/cli/codemod.js +21 -0
  5. package/dist/cli/list.d.ts +30 -1
  6. package/dist/cli/list.d.ts.map +1 -1
  7. package/dist/cli/list.js +15 -4
  8. package/dist/cli/upgrade.d.ts +7 -0
  9. package/dist/cli/upgrade.d.ts.map +1 -1
  10. package/dist/cli/upgrade.js +31 -20
  11. package/dist/cli.js +16 -1
  12. package/dist/install.d.ts +110 -11
  13. package/dist/install.d.ts.map +1 -1
  14. package/dist/install.js +163 -50
  15. package/dist/migrations/accent-box-color-to-background-color/transform.js +175 -0
  16. package/dist/migrations/action-prop-to-on-action/transform.js +82 -0
  17. package/dist/migrations/align-to-combine/transform.js +166 -0
  18. package/dist/migrations/button-color-accent-to-success/transform.js +126 -0
  19. package/dist/migrations/button-props-interfaces/transform.js +178 -0
  20. package/dist/migrations/color-primary-to-default/transform.js +135 -0
  21. package/dist/migrations/imports-to-package-root/transform.js +93 -0
  22. package/dist/migrations/muted-action-error-to-abort-action-error/transform.js +198 -0
  23. package/dist/migrations/package.json +3 -0
  24. package/dist/migrations/password-tools-rule/transform.js +173 -0
  25. package/dist/migrations/password-tools-subpath-renamed/transform.js +51 -0
  26. package/dist/migrations.generated.d.ts.map +1 -1
  27. package/dist/migrations.generated.js +12 -3
  28. package/dist/run/jscodeshift.d.ts +4 -1
  29. package/dist/run/jscodeshift.d.ts.map +1 -1
  30. package/dist/run/jscodeshift.js +60 -22
  31. package/dist/tools/package.json +3 -0
  32. package/dist/tools/to-remote-package.js +29 -0
  33. package/package.json +5 -6
  34. package/src/migrations/accent-box-color-to-background-color/transform.ts +0 -218
  35. package/src/migrations/action-prop-to-on-action/transform.ts +0 -110
  36. package/src/migrations/align-to-combine/transform.ts +0 -212
  37. package/src/migrations/button-color-accent-to-success/transform.ts +0 -164
  38. package/src/migrations/button-props-interfaces/transform.ts +0 -229
  39. package/src/migrations/color-primary-to-default/transform.ts +0 -173
  40. package/src/migrations/imports-to-package-root/transform.ts +0 -107
  41. package/src/migrations/muted-action-error-to-abort-action-error/transform.ts +0 -260
  42. package/src/migrations/password-tools-rule/transform.ts +0 -219
  43. package/src/migrations/password-tools-subpath-renamed/transform.ts +0 -61
  44. package/src/tools/to-remote-package.ts +0 -37
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Renames `MutedActionError` to `AbortActionError`, along with its two static
5
+ * helpers (alpha.712).
6
+ *
7
+ * ```diff
8
+ * -throw new MutedActionError();
9
+ * +throw new AbortActionError();
10
+ * -MutedActionError.isMutedActionError(error);
11
+ * +AbortActionError.isAbortActionError(error);
12
+ * -MutedActionError.rethrowIfNotMuted(error);
13
+ * +AbortActionError.rethrowIfNotAborted(error);
14
+ * ```
15
+ *
16
+ * There is no alias for the old name, so a codebase still using it does not
17
+ * compile.
18
+ *
19
+ * The thrown error's `name` changed too, so a `error.name ===
20
+ * "MutedActionError"` comparison is rewritten as well — but only in a file that
21
+ * imports the class, and only where the string is compared with `==`, `===`,
22
+ * `!=` or `!==`. A check living in a file that never imports the class cannot
23
+ * be recognised; grep for the string once when you are done.
24
+ *
25
+ * Only names imported from `@mittwald/flow-react-components` (including its
26
+ * subpath entries) are touched. `@mittwald/flow-remote-react-components`
27
+ * exports no error classes, so it cannot be in scope. A local alias (`import {
28
+ * MutedActionError as Muted }`) keeps its alias — the static helpers on it are
29
+ * renamed all the same. Namespace usages (`Flow.MutedActionError`) are
30
+ * rewritten too.
31
+ */
32
+ const mutedActionErrorToAbortActionErrorTransform = (fileInfo, { j }) => {
33
+ const flowPackages = ["@mittwald/flow-react-components"];
34
+ const oldName = "MutedActionError";
35
+ const newName = "AbortActionError";
36
+ const memberRenames = new Map([
37
+ ["isMutedActionError", "isAbortActionError"],
38
+ ["rethrowIfNotMuted", "rethrowIfNotAborted"],
39
+ ]);
40
+ const isFlowImport = (source) => flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
41
+ // ast-types models `importKind` on the declaration only, while babel also
42
+ // puts it on the specifier — which is where a per-specifier `type X` lives.
43
+ const isTypeOnly = (specifier) => specifier.importKind === "type";
44
+ const makeValueImport = (specifier) => {
45
+ specifier.importKind = "value";
46
+ };
47
+ const root = j(fileInfo.source, { parser: "tsx" });
48
+ /**
49
+ * Local names bound to the class, alias or not — the static helpers hang off
50
+ * these.
51
+ */
52
+ const classLocals = new Set();
53
+ /** Local names whose identifier itself has to be renamed (no alias in play). */
54
+ const localRenames = new Set();
55
+ /** Local names of `import * as Flow` namespace imports from a Flow package. */
56
+ const flowNamespaces = new Set();
57
+ const flowImports = root
58
+ .find(j.ImportDeclaration)
59
+ .filter((path) => isFlowImport(String(path.node.source.value)));
60
+ /**
61
+ * Local names the file already binds to the new name. Renaming onto one of
62
+ * them must not add a second specifier, which would not parse.
63
+ */
64
+ const claimed = new Set();
65
+ flowImports.forEach((path) => {
66
+ for (const specifier of path.node.specifiers ?? []) {
67
+ if (specifier.type === "ImportNamespaceSpecifier" &&
68
+ specifier.local?.name) {
69
+ flowNamespaces.add(String(specifier.local.name));
70
+ continue;
71
+ }
72
+ if (specifier.type === "ImportSpecifier" &&
73
+ specifier.imported.type === "Identifier" &&
74
+ specifier.imported.name === newName) {
75
+ claimed.add(String(specifier.local?.name ?? newName));
76
+ }
77
+ }
78
+ });
79
+ flowImports.forEach((path) => {
80
+ const specifiers = path.node.specifiers ?? [];
81
+ const survivors = [];
82
+ for (const specifier of specifiers) {
83
+ if (specifier.type !== "ImportSpecifier" ||
84
+ specifier.imported.type !== "Identifier" ||
85
+ specifier.imported.name !== oldName) {
86
+ survivors.push(specifier);
87
+ continue;
88
+ }
89
+ const local = String(specifier.local?.name ?? oldName);
90
+ const isAlias = local !== oldName;
91
+ const newLocal = isAlias ? local : newName;
92
+ classLocals.add(local);
93
+ if (!isAlias) {
94
+ localRenames.add(local);
95
+ }
96
+ if (claimed.has(newLocal)) {
97
+ // The name is already bound by another import — drop this specifier
98
+ // instead of declaring it twice.
99
+ if (!isTypeOnly(specifier)) {
100
+ for (const kept of survivors) {
101
+ if (kept.type === "ImportSpecifier" &&
102
+ kept.imported.type === "Identifier" &&
103
+ String(kept.local?.name ?? kept.imported.name) === newLocal) {
104
+ makeValueImport(kept);
105
+ }
106
+ }
107
+ }
108
+ continue;
109
+ }
110
+ claimed.add(newLocal);
111
+ // Mutate in place so an `import type` / `type X` modifier survives.
112
+ specifier.imported.name = newName;
113
+ if (!isAlias && specifier.local) {
114
+ specifier.local.name = newName;
115
+ }
116
+ survivors.push(specifier);
117
+ }
118
+ if (survivors.length === specifiers.length) {
119
+ return;
120
+ }
121
+ // Leaving an emptied declaration behind would turn it into a side-effect
122
+ // import of an entry the file no longer uses.
123
+ if (survivors.length === 0) {
124
+ j(path).remove();
125
+ return;
126
+ }
127
+ path.node.specifiers = survivors;
128
+ });
129
+ if (classLocals.size === 0 && flowNamespaces.size === 0) {
130
+ return fileInfo.source;
131
+ }
132
+ /** Does this expression denote the class — `Muted` or `Flow.MutedActionError`? */
133
+ const isClassReference = (node) => {
134
+ if (!node) {
135
+ return false;
136
+ }
137
+ if (node.type === "Identifier") {
138
+ return classLocals.has(String(node.name));
139
+ }
140
+ return (node.type === "MemberExpression" &&
141
+ node.object?.type === "Identifier" &&
142
+ flowNamespaces.has(String(node.object.name)) &&
143
+ node.property?.type === "Identifier" &&
144
+ String(node.property.name) === oldName);
145
+ };
146
+ // Static helpers first: the check reads the pre-rename object name.
147
+ root.find(j.MemberExpression).forEach((path) => {
148
+ const { object, property } = path.node;
149
+ if (property.type !== "Identifier" || !isClassReference(object)) {
150
+ return;
151
+ }
152
+ const renamed = memberRenames.get(property.name);
153
+ if (renamed) {
154
+ property.name = renamed;
155
+ }
156
+ });
157
+ root.find(j.Identifier).forEach((path) => {
158
+ const parent = path.parent.node;
159
+ const isNamespaceMember = (parent.type === "MemberExpression" ||
160
+ parent.type === "JSXMemberExpression") &&
161
+ parent.property === path.node;
162
+ if (isNamespaceMember) {
163
+ const object = parent.object;
164
+ if ((object.type === "Identifier" || object.type === "JSXIdentifier") &&
165
+ path.node.name === oldName &&
166
+ flowNamespaces.has(String(object.name))) {
167
+ path.node.name = newName;
168
+ }
169
+ return;
170
+ }
171
+ if (parent.type === "ImportSpecifier" ||
172
+ parent.type === "JSXAttribute" ||
173
+ ((parent.type === "ObjectProperty" || parent.type === "Property") &&
174
+ parent.key === path.node &&
175
+ !parent.computed)) {
176
+ return;
177
+ }
178
+ if (localRenames.has(path.node.name)) {
179
+ path.node.name = newName;
180
+ }
181
+ });
182
+ // `error.name === "MutedActionError"` — a comparison is the only place where
183
+ // the string can be recognised as the error's name rather than free text.
184
+ const comparisons = new Set(["===", "!==", "==", "!="]);
185
+ root.find(j.BinaryExpression).forEach((path) => {
186
+ if (!comparisons.has(path.node.operator)) {
187
+ return;
188
+ }
189
+ for (const side of [path.node.left, path.node.right]) {
190
+ if ((side.type === "StringLiteral" || side.type === "Literal") &&
191
+ side.value === oldName) {
192
+ side.value = newName;
193
+ }
194
+ }
195
+ });
196
+ return root.toSource();
197
+ };
198
+ exports.default = mutedActionErrorToAbortActionErrorTransform;
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Replaces `AsyncRule` and `SyncRule` with `Rule` (alpha.802).
5
+ *
6
+ * `@mittwald/password-tools-js` merged both classes into a single abstract
7
+ * `Rule`, and the `mittwald-password-tools-js` entry stopped re-exporting the
8
+ * old names. A custom rule extends `Rule` and may return its result
9
+ * synchronously or as a promise — the distinction the two classes encoded is
10
+ * gone, so both names collapse onto the same one.
11
+ *
12
+ * Only names imported from the `mittwald-password-tools-js` entry of
13
+ * `@mittwald/flow-react-components` are touched, so a same-named import from
14
+ * another package is left alone. `@mittwald/flow-remote-react-components` has
15
+ * no such entry. A name imported under a local alias (`import { AsyncRule as
16
+ * Base }`) keeps its alias — only the imported name changes. Namespace usages
17
+ * (`Pw.AsyncRule`) are rewritten as well.
18
+ *
19
+ * Because both names collapse onto `Rule`, a file importing more than one of
20
+ * them would end up with a duplicate specifier. Those collapse onto one.
21
+ */
22
+ const passwordToolsRuleTransform = (fileInfo, { j }) => {
23
+ const flowPackages = [
24
+ "@mittwald/flow-react-components/mittwald-password-tools-js",
25
+ ];
26
+ const renames = new Map([
27
+ ["AsyncRule", "Rule"],
28
+ ["SyncRule", "Rule"],
29
+ ]);
30
+ const isFlowImport = (source) => flowPackages.includes(source);
31
+ // ast-types models `importKind` on the declaration only, while babel also
32
+ // puts it on the specifier — which is where a per-specifier `type X` lives.
33
+ const isTypeOnly = (specifier) => specifier.importKind === "type";
34
+ const makeValueImport = (specifier) => {
35
+ specifier.importKind = "value";
36
+ };
37
+ const root = j(fileInfo.source, { parser: "tsx" });
38
+ /** Local identifiers that refer to a renamed export, mapped to the new name. */
39
+ const localRenames = new Map();
40
+ /**
41
+ * Whether any import specifier was rewritten. An aliased `Align as Row` adds
42
+ * nothing to `localRenames` — its local name stays `Row` — so counting those
43
+ * alone would treat an alias-only file as untouched and discard the rewrite.
44
+ */
45
+ let renamedAnImport = false;
46
+ /** Local names of `import * as Flow` namespace imports from a Flow package. */
47
+ const flowNamespaces = new Set();
48
+ const flowImports = root
49
+ .find(j.ImportDeclaration)
50
+ .filter((path) => isFlowImport(String(path.node.source.value)));
51
+ /**
52
+ * `imported:local` pairs the file already binds without a rename. A rename
53
+ * that lands on one of these must not add a second specifier for it — the
54
+ * name is available from the import that already carries it, which is also
55
+ * the entry that really exports it.
56
+ */
57
+ const claimed = new Set();
58
+ flowImports.forEach((path) => {
59
+ for (const specifier of path.node.specifiers ?? []) {
60
+ if (specifier.type === "ImportNamespaceSpecifier" &&
61
+ specifier.local?.name) {
62
+ flowNamespaces.add(String(specifier.local.name));
63
+ continue;
64
+ }
65
+ if (specifier.type !== "ImportSpecifier" ||
66
+ specifier.imported.type !== "Identifier" ||
67
+ renames.has(specifier.imported.name)) {
68
+ continue;
69
+ }
70
+ claimed.add(`${specifier.imported.name}:${String(specifier.local?.name ?? specifier.imported.name)}`);
71
+ }
72
+ });
73
+ flowImports.forEach((path) => {
74
+ const specifiers = path.node.specifiers ?? [];
75
+ const survivors = [];
76
+ for (const specifier of specifiers) {
77
+ if (specifier.type !== "ImportSpecifier" ||
78
+ specifier.imported.type !== "Identifier") {
79
+ survivors.push(specifier);
80
+ continue;
81
+ }
82
+ const imported = specifier.imported.name;
83
+ const renamed = renames.get(imported);
84
+ if (!renamed) {
85
+ survivors.push(specifier);
86
+ continue;
87
+ }
88
+ const local = String(specifier.local?.name ?? imported);
89
+ const isAlias = local !== imported;
90
+ const newLocal = isAlias ? local : renamed;
91
+ const key = `${renamed}:${newLocal}`;
92
+ renamedAnImport = true;
93
+ if (!isAlias) {
94
+ // An aliased import keeps its local name and needs no further change.
95
+ localRenames.set(local, renamed);
96
+ }
97
+ // Renaming can collide with a name the file already binds — directly, or
98
+ // because a second old name maps onto the same new one. Either way the
99
+ // binding already exists, so this specifier goes away instead of
100
+ // producing a duplicate declaration, which would not parse.
101
+ const collision = claimed.has(key);
102
+ claimed.add(key);
103
+ if (collision) {
104
+ // A value import must not lose out to a type-only one.
105
+ if (!isTypeOnly(specifier)) {
106
+ for (const kept of survivors) {
107
+ if (kept.type === "ImportSpecifier" &&
108
+ kept.imported.type === "Identifier" &&
109
+ `${kept.imported.name}:${String(kept.local?.name ?? kept.imported.name)}` === key) {
110
+ makeValueImport(kept);
111
+ }
112
+ }
113
+ }
114
+ continue;
115
+ }
116
+ // Mutate in place so an `import type` / `type X` modifier survives.
117
+ specifier.imported.name = renamed;
118
+ if (!isAlias && specifier.local) {
119
+ specifier.local.name = renamed;
120
+ }
121
+ survivors.push(specifier);
122
+ }
123
+ if (survivors.length === specifiers.length) {
124
+ return;
125
+ }
126
+ // Every specifier moved to an import that already binds the name. Leaving
127
+ // the declaration behind would turn it into a side-effect import of an
128
+ // entry the file no longer uses.
129
+ if (survivors.length === 0) {
130
+ j(path).remove();
131
+ return;
132
+ }
133
+ path.node.specifiers = survivors;
134
+ });
135
+ if (!renamedAnImport && flowNamespaces.size === 0) {
136
+ return fileInfo.source;
137
+ }
138
+ /**
139
+ * `JSXIdentifier` extends `Identifier`, so this one pass covers JSX tags,
140
+ * value references and type references alike. Positions where the name is not
141
+ * a reference to the import — an object key, a member's property, a JSX
142
+ * attribute name — are skipped.
143
+ */
144
+ root.find(j.Identifier).forEach((path) => {
145
+ const parent = path.parent.node;
146
+ const isNamespaceMember = (parent.type === "MemberExpression" ||
147
+ parent.type === "JSXMemberExpression") &&
148
+ parent.property === path.node;
149
+ if (isNamespaceMember) {
150
+ const object = parent.object;
151
+ if (object.type === "Identifier" || object.type === "JSXIdentifier") {
152
+ const renamed = renames.get(path.node.name);
153
+ if (renamed && flowNamespaces.has(String(object.name))) {
154
+ path.node.name = renamed;
155
+ }
156
+ }
157
+ return;
158
+ }
159
+ if (parent.type === "ImportSpecifier" ||
160
+ parent.type === "JSXAttribute" ||
161
+ ((parent.type === "ObjectProperty" || parent.type === "Property") &&
162
+ parent.key === path.node &&
163
+ !parent.computed)) {
164
+ return;
165
+ }
166
+ const renamed = localRenames.get(path.node.name);
167
+ if (renamed) {
168
+ path.node.name = renamed;
169
+ }
170
+ });
171
+ return root.toSource();
172
+ };
173
+ exports.default = passwordToolsRuleTransform;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const oldPath = "@mittwald/flow-react-components/password-tools";
4
+ const newPath = "@mittwald/flow-react-components/mittwald-password-tools-js";
5
+ /**
6
+ * Rewrites the module specifier
7
+ * `@mittwald/flow-react-components/password-tools` to
8
+ * `@mittwald/flow-react-components/mittwald-password-tools-js` (alpha.1000).
9
+ *
10
+ * Every form that names a module is covered: `import`, `import type`, `export …
11
+ * from`, a side-effect import, `import()` and `require()`. Only that exact
12
+ * specifier is touched — a deeper path under it never existed, so there is
13
+ * nothing to prefix-match and no risk of rewriting an unrelated
14
+ * `password-tools` of someone else's.
15
+ */
16
+ const passwordToolsSubpathRenamedTransform = (fileInfo, { j }) => {
17
+ const root = j(fileInfo.source, { parser: "tsx" });
18
+ let changed = false;
19
+ const rewrite = (node) => {
20
+ if (node.value === oldPath) {
21
+ node.value = newPath;
22
+ changed = true;
23
+ }
24
+ };
25
+ // import … from "…", export … from "…", import "…"
26
+ root.find(j.ImportDeclaration).forEach((path) => rewrite(path.node.source));
27
+ root
28
+ .find(j.ExportNamedDeclaration)
29
+ .forEach((path) => path.node.source && rewrite(path.node.source));
30
+ root
31
+ .find(j.ExportAllDeclaration)
32
+ .forEach((path) => rewrite(path.node.source));
33
+ // import("…") and require("…")
34
+ root
35
+ .find(j.CallExpression)
36
+ .filter((path) => {
37
+ const callee = path.node.callee;
38
+ return (callee.type === "Import" ||
39
+ (callee.type === "Identifier" && callee.name === "require"));
40
+ })
41
+ .forEach((path) => {
42
+ const [argument] = path.node.arguments;
43
+ if (argument &&
44
+ (argument.type === "StringLiteral" || argument.type === "Literal")) {
45
+ rewrite(argument);
46
+ }
47
+ });
48
+ // Returning the source untouched keeps this file out of the "changed" count.
49
+ return changed ? root.toSource() : fileInfo.source;
50
+ };
51
+ exports.default = passwordToolsSubpathRenamedTransform;
@@ -1 +1 @@
1
- {"version":3,"file":"migrations.generated.d.ts","sourceRoot":"","sources":["../src/migrations.generated.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,sEAAsE;AACtE,eAAO,MAAM,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,EAmPpD,CAAC"}
1
+ {"version":3,"file":"migrations.generated.d.ts","sourceRoot":"","sources":["../src/migrations.generated.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,sEAAsE;AACtE,eAAO,MAAM,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,EA8PpD,CAAC"}
@@ -19,7 +19,7 @@ export const migrations = [
19
19
  kind: "deprecation",
20
20
  action: "manual",
21
21
  remotePackage: true,
22
- apply: "Replace `SegmentedControl` with `Tabs` when the selection switches displayed content, or with `RadioGroup` when it sets a value. Pick per usage this is a structural change, not a rename.",
22
+ apply: "Replace `SegmentedControl` with `Tabs` when the selection switches displayed content, or with `RadioGroup` when it sets a value. Pick per usage. The two directions cost very different amounts of work. Towards `RadioGroup` it is a prop-compatible rename: `SegmentedControl` → `RadioGroup` and `Segment` → `RadioButton` (always `RadioButton`, not `Radio`; it takes exactly `Segment`'s props), with `value`/`defaultValue`/`onChange` and a `Label` child all carrying over. Only `containerBreakpointSize` has no counterpart, and the joined row is not reproduced. Towards `Tabs` it is structural: the state props are `selectedKey`/`defaultSelectedKey` rather than `value`/`defaultValue`, there is no `Label` slot (the group label moves to the surrounding `Heading`, or to `aria-label` on `Tabs` when it should not be visible, or goes away), and the switched panels move inside the tabs — where they stay mounted, so form fields in them keep their registration.",
23
23
  },
24
24
  {
25
25
  id: "align-to-combine",
@@ -75,6 +75,15 @@ export const migrations = [
75
75
  remotePackage: false,
76
76
  apply: "Replace the import path `@mittwald/flow-react-components/password-tools` with `@mittwald/flow-react-components/mittwald-password-tools-js`.",
77
77
  },
78
+ {
79
+ id: "tabs-navigation-usage-to-tab-navigation",
80
+ since: "0.2.0-alpha.977",
81
+ title: "Tabs restyled; navigation usage moves to TabNavigation",
82
+ kind: "migration",
83
+ action: "manual",
84
+ remotePackage: true,
85
+ apply: "Tabs' rendering changed: the tab list now fills the available width with equal-size tabs and a shared, animated indicator that slides between them, replacing the previous fit-content tabs that each carried their own hover/pressed/selected background. Nothing else about `Tabs` changed, and a `Tabs` that switches content within a page needs no code change — the new look applies on upgrade. Where `Tabs` was used to fake real navigation instead — a `TabTitle` given an `href` (`Aria.Tab`'s routing props: `href`, `target`, `routerOptions`, …) so selecting it pushed a real route change — replace it with `TabNavigation`: plain `Link` children instead of `Tab`/`TabTitle`/panels, `aria-current=\"page\"` on the active `Link` instead of `selectedKey`/`defaultSelectedKey`/`onSelectionChange`. `TabNavigation` keeps the visual language the old `Tabs` used to have, because it now owns that use case.",
86
+ },
78
87
  {
79
88
  id: "table-column-width-props",
80
89
  since: "0.2.0-alpha.956",
@@ -145,7 +154,7 @@ export const migrations = [
145
154
  kind: "migration",
146
155
  action: "manual",
147
156
  remotePackage: true,
148
- apply: "Check every `CodeBlock` usage against the current props (see the [CodeBlock documentation](https://flow.mittwald.de/04-components/content/code-block/overview)) and remove or replace props the new implementation does not support.",
157
+ apply: "Check every `CodeBlock` usage against the current props (see the [CodeBlock documentation](https://flow.mittwald.de/components/content/code-block)) and remove or replace props the new implementation does not support.",
149
158
  },
150
159
  {
151
160
  id: "muted-action-error-to-abort-action-error",
@@ -190,7 +199,7 @@ export const migrations = [
190
199
  kind: "migration",
191
200
  action: "codemod",
192
201
  remotePackage: true,
193
- apply: "Rename the `action` prop on `Action` to `onAction`.",
202
+ 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.",
194
203
  },
195
204
  {
196
205
  id: "button-props-interfaces",
@@ -36,7 +36,10 @@ export interface CodemodResult {
36
36
  * `processedNothing` exists because jscodeshift reports a path with no matching
37
37
  * files and a worker that died before touching one the same way: every counter
38
38
  * zero, no error. The caller must not render that as "0 files changed", which
39
- * reads like success.
39
+ * reads like success. The load check below removes the most common cause of the
40
+ * second case, so `processedNothing` now means the path far more often than it
41
+ * used to — but not always: a worker can still die for a reason a successful
42
+ * load does not predict, so the flag keeps its deliberately vague name.
40
43
  */
41
44
  export declare const runCodemod: ({ id, path, dry, print, }: CodemodOptions) => Promise<CodemodResult>;
42
45
  //# sourceMappingURL=jscodeshift.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jscodeshift.d.ts","sourceRoot":"","sources":["../../src/run/jscodeshift.ts"],"names":[],"mappings":"AAuCA;;;;;GAKG;AACH,eAAO,MAAM,eAAe,OAAQ,MAAM,KAAG,OACd,CAAC;AAEhC,MAAM,WAAW,cAAc;IAC7B,sEAAsE;IACtE,EAAE,EAAE,MAAM,CAAC;IACX,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAUD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,UAAU,8BAKpB,cAAc,KAAG,OAAO,CAAC,aAAa,CAoCxC,CAAC"}
1
+ {"version":3,"file":"jscodeshift.d.ts","sourceRoot":"","sources":["../../src/run/jscodeshift.ts"],"names":[],"mappings":"AAwDA;;;;;GAKG;AACH,eAAO,MAAM,eAAe,OAAQ,MAAM,KAAG,OACd,CAAC;AAEhC,MAAM,WAAW,cAAc;IAC7B,sEAAsE;IACtE,EAAE,EAAE,MAAM,CAAC;IACX,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAUD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,UAAU,8BAKpB,cAAc,KAAG,OAAO,CAAC,aAAa,CA6DxC,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { fileURLToPath } from "node:url";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
3
  import { unknownCodemodMessage } from "../catalog/entries.js";
4
4
  // jscodeshift ships no types for its Runner — `allowJs` in this repo's shared
5
5
  // tsconfig (packages/typescript-config/base.json) lets a deep import of a
@@ -8,30 +8,45 @@ import { unknownCodemodMessage } from "../catalog/entries.js";
8
8
  // directive").
9
9
  import { run as runJscodeshift } from "jscodeshift/src/Runner.js";
10
10
  /**
11
- * `<packageRoot>/src/migrations` and `<packageRoot>/src/tools`, from either
12
- * `src/run` or `dist/run`.
13
- *
14
- * `dist` mirrors `src`'s directory depth, so one pair of relative paths serves
15
- * the test run and the published binary. The transforms are not compiled into
16
- * `dist`: jscodeshift puts a transform through its own babel pipeline, so it
17
- * wants the `.ts` file.
11
+ * The package root, from either `src/run` or `dist/run` both are two levels
12
+ * down, so one expression serves the test run and the published binary.
18
13
  */
19
- const migrationsDir = fileURLToPath(new URL("../../src/migrations", import.meta.url));
20
- const toolsDir = fileURLToPath(new URL("../../src/tools", import.meta.url));
14
+ const packageRoot = fileURLToPath(new URL("../../", import.meta.url));
21
15
  /**
22
- * The transform file for `id`: `src/migrations/<id>/transform.ts` when `id`
23
- * names a migration, otherwise `src/tools/<id>.ts`.
16
+ * Where a transform for `id` may live, most preferred first.
17
+ *
18
+ * The compiled CommonJS in `dist` comes first, and in a published install it is
19
+ * the only one that works. jscodeshift's worker `require()`s this path; it
20
+ * installs `@babel/register` beforehand, but babel-register's `only` defaults
21
+ * to the current working directory, so a transform inside the consumer's
22
+ * `node_modules` is never claimed by babel. Node's own `.ts` handler takes over
23
+ * and refuses — `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`, no opt-out — and
24
+ * every codemod dies before touching a file. See `tsconfig.transforms.json`.
25
+ *
26
+ * The `.ts` sources stay as a fallback for running out of the repo (`tsx`, the
27
+ * unit tests, `dist` not built yet), where cwd _is_ inside the package and
28
+ * babel-register does claim them. They are no longer published: `files` ships
29
+ * `dist` only, so the path that cannot work is absent from the tarball rather
30
+ * than merely deprioritised.
24
31
  *
25
- * Deliberately independent of the catalogue: `to-remote-package` is a transform
26
- * with no catalogue entry (it is a port, not a migration — see `notAMigration`
27
- * in `src/tests/remoteScope.test.ts`), and it still has to be runnable by id.
28
- * It lives in `src/tools` rather than `src/migrations` for exactly that reason
29
- * — there is no migration directory to put it beside.
32
+ * `src/tools` is searched alongside `src/migrations` because
33
+ * `to-remote-package` is a transform with no catalogue entry (a port, not a
34
+ * migration — see `notAMigration` in `src/tests/remoteScope.test.ts`) and still
35
+ * has to be runnable by id.
30
36
  */
31
- const transformPath = (id) => {
32
- const migrationPath = `${migrationsDir}/${id}/transform.ts`;
33
- return existsSync(migrationPath) ? migrationPath : `${toolsDir}/${id}.ts`;
34
- };
37
+ const candidatePaths = (id) => [
38
+ `${packageRoot}dist/migrations/${id}/transform.js`,
39
+ `${packageRoot}dist/tools/${id}.js`,
40
+ `${packageRoot}src/migrations/${id}/transform.ts`,
41
+ `${packageRoot}src/tools/${id}.ts`,
42
+ ];
43
+ /** The transform file for `id`, or `undefined` when no candidate exists. */
44
+ const findTransform = (id) => candidatePaths(id).find((candidate) => existsSync(candidate));
45
+ /**
46
+ * The transform file for `id`, falling back to the last candidate so callers
47
+ * that only report a path still have one to name.
48
+ */
49
+ const transformPath = (id) => findTransform(id) ?? `${packageRoot}src/tools/${id}.ts`;
35
50
  /**
36
51
  * Whether `id` names a transform file on disk.
37
52
  *
@@ -53,13 +68,36 @@ export const transformExists = (id) => existsSync(transformPath(id));
53
68
  * `processedNothing` exists because jscodeshift reports a path with no matching
54
69
  * files and a worker that died before touching one the same way: every counter
55
70
  * zero, no error. The caller must not render that as "0 files changed", which
56
- * reads like success.
71
+ * reads like success. The load check below removes the most common cause of the
72
+ * second case, so `processedNothing` now means the path far more often than it
73
+ * used to — but not always: a worker can still die for a reason a successful
74
+ * load does not predict, so the flag keeps its deliberately vague name.
57
75
  */
58
76
  export const runCodemod = async ({ id, path, dry = false, print = false, }) => {
59
77
  const transform = transformPath(id);
60
78
  if (!existsSync(transform)) {
61
79
  throw new Error(unknownCodemodMessage(id));
62
80
  }
81
+ // Load the transform here, in this process, before handing its path to
82
+ // jscodeshift.
83
+ //
84
+ // A worker that cannot load the transform dies before it touches a file, and
85
+ // the Runner then resolves with every counter at zero — indistinguishable
86
+ // from a path that matched nothing (see `processedNothing`). The stack trace
87
+ // goes to the worker's stderr, where the summary line the caller prints
88
+ // contradicts it by guessing at the path instead. Loading it up front turns
89
+ // that class of failure into a thrown error naming the real cause, which is
90
+ // the only way a caller can tell "this migration got no run at all" from
91
+ // "this migration had nothing to do".
92
+ //
93
+ // Safe to do: every shipped transform imports nothing but types, so loading
94
+ // one has no side effects and costs a file read.
95
+ try {
96
+ await import(pathToFileURL(transform).href);
97
+ }
98
+ catch (error) {
99
+ throw new Error(`${id} could not be loaded from ${transform}: ${error instanceof Error ? error.message : error}`, { cause: error });
100
+ }
63
101
  let stats;
64
102
  try {
65
103
  stats = (await runJscodeshift(transform, [path], {
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Ports imports from `@mittwald/flow-react-components` to
5
+ * `@mittwald/flow-remote-react-components`.
6
+ *
7
+ * Not a migration: no version range calls for it, so it has no catalogue entry
8
+ * (see `notAMigration` in `src/tests/remoteScope.test.ts`) and lives outside
9
+ * `src/migrations`, in `src/tools` alongside any other transform the CLI runs
10
+ * by id without a matching entry. It is still resolved and run the same way a
11
+ * migration's `transform.ts` is — `transformExists`/`runCodemod` in
12
+ * `src/run/jscodeshift.ts` fall back to this directory when an id names no
13
+ * migration.
14
+ */
15
+ const toRemotePackageTransform = (fileInfo, { j }) => {
16
+ const flowPackage = "@mittwald/flow-react-components";
17
+ const root = j(fileInfo.source, {
18
+ parser: "ts",
19
+ });
20
+ root
21
+ .find(j.ImportDeclaration)
22
+ .filter((i) => String(i.node.source.value).startsWith(flowPackage))
23
+ .forEach((i) => {
24
+ const importPath = String(i.node.source.value);
25
+ i.node.source.value = importPath.replace(flowPackage, "@mittwald/flow-remote-react-components");
26
+ });
27
+ return root.toSource();
28
+ };
29
+ exports.default = toRemotePackageTransform;