@mittwald/flow-codemods 1.2.0-next.5 → 1.2.0-next.51

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 (47) hide show
  1. package/README.md +35 -6
  2. package/dist/catalog/entries.d.ts +0 -1
  3. package/dist/catalog/select.d.ts +0 -1
  4. package/dist/catalog/types.d.ts +0 -1
  5. package/dist/cli/args.d.ts +0 -1
  6. package/dist/cli/choose.d.ts +0 -1
  7. package/dist/cli/codemod.d.ts +16 -1
  8. package/dist/cli/codemod.js +21 -0
  9. package/dist/cli/list.d.ts +30 -2
  10. package/dist/cli/list.js +15 -4
  11. package/dist/cli/upgrade.d.ts +7 -1
  12. package/dist/cli/upgrade.js +31 -20
  13. package/dist/cli.d.ts +0 -1
  14. package/dist/cli.js +16 -1
  15. package/dist/flowPackages.generated.d.ts +0 -1
  16. package/dist/git.d.ts +0 -1
  17. package/dist/install.d.ts +110 -12
  18. package/dist/install.js +163 -50
  19. package/dist/manifest.d.ts +0 -1
  20. package/dist/migrations/action-prop-to-on-action/transform.js +91 -5
  21. package/dist/migrations/renamed-css-export/transform.js +59 -0
  22. package/dist/migrations/table-column-width-props/transform.js +131 -0
  23. package/dist/migrations.generated.d.ts +0 -1
  24. package/dist/migrations.generated.js +32 -5
  25. package/dist/resolve/range.d.ts +0 -1
  26. package/dist/resolve/registry.d.ts +0 -1
  27. package/dist/resolve/target.d.ts +0 -1
  28. package/dist/run/jscodeshift.d.ts +0 -1
  29. package/package.json +4 -2
  30. package/dist/catalog/entries.d.ts.map +0 -1
  31. package/dist/catalog/select.d.ts.map +0 -1
  32. package/dist/catalog/types.d.ts.map +0 -1
  33. package/dist/cli/args.d.ts.map +0 -1
  34. package/dist/cli/choose.d.ts.map +0 -1
  35. package/dist/cli/codemod.d.ts.map +0 -1
  36. package/dist/cli/list.d.ts.map +0 -1
  37. package/dist/cli/upgrade.d.ts.map +0 -1
  38. package/dist/cli.d.ts.map +0 -1
  39. package/dist/flowPackages.generated.d.ts.map +0 -1
  40. package/dist/git.d.ts.map +0 -1
  41. package/dist/install.d.ts.map +0 -1
  42. package/dist/manifest.d.ts.map +0 -1
  43. package/dist/migrations.generated.d.ts.map +0 -1
  44. package/dist/resolve/range.d.ts.map +0 -1
  45. package/dist/resolve/registry.d.ts.map +0 -1
  46. package/dist/resolve/target.d.ts.map +0 -1
  47. package/dist/run/jscodeshift.d.ts.map +0 -1
package/dist/install.js CHANGED
@@ -1,38 +1,45 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import { join } from "node:path";
4
- /** Lockfile to manager, in precedence order. */
5
- const lockfiles = [
6
- ["pnpm-lock.yaml", "pnpm"],
7
- // Both Bun lockfile names: `bun.lock` (text) is the default only from Bun
8
- // 1.2; before that Bun writes the binary `bun.lockb`, which plenty of
9
- // projects still have. Missing it would fall through to the npm default and
10
- // run `npm install` on a Bun project, leaving a stray package-lock.json.
11
- ["bun.lock", "bun"],
12
- ["bun.lockb", "bun"],
13
- ["yarn.lock", "yarn"],
14
- ["package-lock.json", "npm"],
2
+ import { satisfies, validRange } from "semver";
3
+ import { resolveCommand } from "package-manager-detector/commands";
4
+ import { INSTALL_PAGE } from "package-manager-detector/constants";
5
+ import { detect } from "package-manager-detector/detect";
6
+ /** Every strategy the library offers, in the order it applies them. */
7
+ const strategies = [
8
+ "lockfile",
9
+ "packageManager-field",
10
+ "devEngines-field",
11
+ // Not in the library's default set. `upgrade` always runs on an installed
12
+ // project — it reads `node_modules` to learn the current Flow version — so the
13
+ // marker a manager leaves in there is a real signal, and the last one before
14
+ // the npm fallback.
15
+ "install-metadata",
15
16
  ];
17
+ /** What a project with no signal at all gets. */
18
+ const fallback = { name: "npm", agent: "npm" };
16
19
  /**
17
- * The package manager a project uses, from the lockfiles present.
20
+ * The package manager for the project at `cwd`.
18
21
  *
19
- * Npm is the fallback: it is the one manager that is always available next to
20
- * Node, so a project with no lockfile still gets an install rather than an
21
- * error.
22
+ * Detection walks **up** from `cwd`, which is the whole point: the previous
23
+ * implementation looked for lockfiles in `cwd` only, and a workspace package
24
+ * has none of its own. In a Yarn or pnpm monorepo it therefore fell through to
25
+ * the npm default and ran `npm install`, which on a workspace manifest dies
26
+ * with `EUNSUPPORTEDPROTOCOL Unsupported URL Type "workspace:"` — after
27
+ * `upgrade` had already rewritten `package.json`. It also ignored
28
+ * `packageManager` entirely.
29
+ *
30
+ * Per directory the strategies are applied in order, so a lockfile in `cwd`
31
+ * still beats a `packageManager` pin in a parent — and when the lockfile
32
+ * strategy hits, the library reads that same directory's `package.json` and
33
+ * lets its pin win. That is the precedence we want, and it is the library's,
34
+ * not ours.
35
+ *
36
+ * Npm on `null`, preserving the old fallback: it is the one manager always
37
+ * present next to Node, so a project with no signal gets an install rather than
38
+ * an error.
22
39
  */
23
- export const detectPackageManager = (present) => {
24
- for (const [lockfile, manager] of lockfiles) {
25
- if (present.includes(lockfile)) {
26
- return manager;
27
- }
28
- }
29
- return "npm";
30
- };
31
- export const detectPackageManagerIn = (cwd) => detectPackageManager(lockfiles
32
- .map(([lockfile]) => lockfile)
33
- .filter((lockfile) => existsSync(join(cwd, lockfile))));
40
+ export const detectPackageManagerIn = async (cwd) => (await detect({ cwd, strategies: [...strategies] })) ?? fallback;
34
41
  /**
35
- * How to install with a given manager, so that the install actually updates the
42
+ * How to install with `agent`, so that the install actually updates the
36
43
  * lockfile.
37
44
  *
38
45
  * `upgrade` has just rewritten `package.json`, so the lockfile is deliberately
@@ -40,32 +47,138 @@ export const detectPackageManagerIn = (cwd) => detectPackageManager(lockfiles
40
47
  * themselves when they detect CI, where an install then _fails_ instead of
41
48
  * updating — and CI is exactly where this runs unattended. pnpm takes a flag;
42
49
  * Yarn's flag differs between Classic and Berry, so it gets the environment
43
- * variable instead, which Classic ignores harmlessly.
50
+ * variable instead, which Classic ignores harmlessly. Keeping the env var also
51
+ * keeps this one code path rather than branching on a Berry detection being
52
+ * right.
53
+ *
54
+ * The command itself comes from the library. Its `"install"` is the plain
55
+ * install for every agent — the frozen variants are a separate command there —
56
+ * so these quirks stay ours.
57
+ */
58
+ export const installCommand = (agent) => {
59
+ const frozenOptOut = agent.startsWith("pnpm") ? ["--no-frozen-lockfile"] : [];
60
+ const resolved = resolveCommand(agent, "install", frozenOptOut);
61
+ if (resolved === null) {
62
+ throw new Error(`${agent} has no install command. Install the dependencies yourself, then re-run this command.`);
63
+ }
64
+ return {
65
+ command: resolved.command,
66
+ args: resolved.args,
67
+ env: agent.startsWith("yarn")
68
+ ? { YARN_ENABLE_IMMUTABLE_INSTALLS: "false" }
69
+ : {},
70
+ };
71
+ };
72
+ /**
73
+ * The pin as a semver range, or `undefined` when there is nothing to check.
74
+ *
75
+ * Two values have to be filtered out. `"berry"` is what the library reports for
76
+ * Yarn 2+ — a specifier, not a version — and for an unparseable
77
+ * `packageManager` it hands back the raw string. Neither is a range, and
78
+ * `satisfies` would throw on both.
79
+ *
80
+ * A full pin like `8.15.0` is a valid range meaning exactly that version, so
81
+ * one comparison covers both `pnpm@8` and `pnpm@8.15.0`. That is also why this
82
+ * is `satisfies` and not a string compare: the library reports the
83
+ * `\d+(\.\d+){0,2}` match out of the field, so `packageManager: "pnpm@8"`
84
+ * yields the pin `"8"` while the binary reports `8.15.0` — a string compare
85
+ * would send a perfectly fine setup through corepack.
44
86
  */
45
- export const installCommand = (manager) => {
46
- switch (manager) {
47
- case "pnpm":
48
- return {
49
- command: "pnpm",
50
- args: ["install", "--no-frozen-lockfile"],
51
- env: {},
52
- };
53
- case "yarn":
54
- return {
55
- command: "yarn",
56
- args: ["install"],
57
- env: { YARN_ENABLE_IMMUTABLE_INSTALLS: "false" },
58
- };
59
- case "npm":
60
- case "bun":
61
- return { command: manager, args: ["install"], env: {} };
87
+ export const pinnedRange = (version) => version !== undefined && version !== "berry" && validRange(version) !== null
88
+ ? version
89
+ : undefined;
90
+ export const runProbe = (command, args) => {
91
+ try {
92
+ return execFileSync(command, args, {
93
+ encoding: "utf8",
94
+ stdio: ["ignore", "pipe", "ignore"],
95
+ shell: process.platform === "win32",
96
+ }).trim();
97
+ }
98
+ catch {
99
+ return undefined;
62
100
  }
63
101
  };
102
+ /**
103
+ * What to run for `manager`, honouring a `packageManager` pin.
104
+ *
105
+ * The decision tree, and why it checks before reaching for corepack:
106
+ *
107
+ * | Situation | Behaviour |
108
+ * | ------------------------------------------------- | ------------------------- |
109
+ * | No pin, or the `"berry"` specifier | run directly |
110
+ * | Pin, binary present, version satisfies it | run directly |
111
+ * | Pin, mismatch or binary missing, corepack present | run under corepack |
112
+ * | Pin, mismatch or missing, no corepack | throw, before any install |
113
+ *
114
+ * Corepack ignores `PATH` and downloads into its own cache, so "always
115
+ * corepack" would send every project that has a `packageManager` field — most
116
+ * of them — through a download, offline CI included. pnpm 9.7+ additionally
117
+ * installs its own pinned version, so for pnpm the direct path is usually
118
+ * already right.
119
+ *
120
+ * `COREPACK_ENABLE_DOWNLOAD_PROMPT=0` is required rather than tidy: corepack
121
+ * prompts interactively before a first download, and `upgrade` runs unattended
122
+ * (`-y` is implied with no TTY). Only one `install` is spawned under corepack —
123
+ * no nested per-task spawns — so the corepack footgun in the repo's root
124
+ * `AGENTS.md` does not apply.
125
+ */
126
+ export const planInstall = (manager, probe = runProbe) => {
127
+ const { command, args, env } = installCommand(manager.agent);
128
+ const line = [command, ...args].join(" ");
129
+ const pin = pinnedRange(manager.version);
130
+ if (pin === undefined) {
131
+ return { command, args, env, description: `${manager.agent} — ${line}` };
132
+ }
133
+ const found = probe(command, ["--version"]);
134
+ if (found !== undefined && satisfies(found, pin)) {
135
+ return {
136
+ command,
137
+ args,
138
+ env,
139
+ description: `${manager.agent} ${found} — ${line}`,
140
+ };
141
+ }
142
+ if (probe("corepack", ["--version"]) === undefined) {
143
+ throw new Error(`This project pins ${manager.name}@${pin} in "packageManager", but ${found === undefined
144
+ ? `${command} is not on PATH`
145
+ : `the ${command} on PATH is ${found}`}, and corepack is not available to bridge the gap.\n\nInstall ${manager.name}@${pin} (${INSTALL_PAGE[manager.agent]}) or enable corepack, then re-run this command. Nothing has been installed.`);
146
+ }
147
+ return {
148
+ command: "corepack",
149
+ args: [command, ...args],
150
+ env: { ...env, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" },
151
+ description: `${manager.agent} ${pin} via corepack — corepack ${line}`,
152
+ };
153
+ };
64
154
  export const runInstall = (manager, cwd) => {
65
- const { command, args, env } = installCommand(manager);
66
- execFileSync(command, args, {
155
+ const plan = planInstall(manager);
156
+ execFileSync(plan.command, plan.args, {
67
157
  cwd,
68
158
  stdio: "inherit",
69
- env: { ...process.env, ...env },
159
+ env: { ...process.env, ...plan.env },
160
+ // Node has refused to spawn `.cmd`/`.bat` without a shell since 2024, which
161
+ // is why every non-npm manager died with ENOENT on Windows. Every argument
162
+ // here is our own literal or the library's, so there is no quoting hazard.
163
+ shell: process.platform === "win32",
70
164
  });
165
+ return plan.description;
166
+ };
167
+ /**
168
+ * How to tell a reader to invoke this package, for the manager they use.
169
+ *
170
+ * `npx …` for npm and Yarn Classic (which has no `dlx`), `pnpm dlx …`, `yarn
171
+ * dlx …`, `bun x …`. The package name is passed as an argument rather than
172
+ * baked in, so the library decides the whole prefix.
173
+ *
174
+ * Falls back to the `npx` form when the agent has no `execute` command: a
175
+ * printed hint is worth more than a gap, and npx is the one form every reader
176
+ * can run.
177
+ */
178
+ export const resolveInvoke = async (cwd, pkg = "@mittwald/flow-codemods@latest") => {
179
+ const { agent } = await detectPackageManagerIn(cwd);
180
+ const resolved = resolveCommand(agent, "execute", [pkg]);
181
+ return resolved === null
182
+ ? `npx ${pkg}`
183
+ : [resolved.command, ...resolved.args].join(" ");
71
184
  };
@@ -42,4 +42,3 @@ export declare const applyTarget: <T extends Manifest>(manifest: T, target: stri
42
42
  * `imports-to-package-root` needed an explicit leave-alone set to earn this.
43
43
  */
44
44
  export declare const detectCurrentVersion: (dependencies: FlowDependency[], readInstalledVersion: (name: string) => string | undefined) => string | undefined;
45
- //# sourceMappingURL=manifest.d.ts.map
@@ -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();
@@ -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;
@@ -1,4 +1,3 @@
1
1
  import type { MigrationEntry } from "./catalog/types.js";
2
2
  /** Every migration, newest first. Bodies live in `src/migrations`. */
3
3
  export declare const migrations: Omit<MigrationEntry, "body">[];
4
- //# sourceMappingURL=migrations.generated.d.ts.map
@@ -3,6 +3,24 @@
3
3
  // Source: packages/codemods/src/migrations/<id>/entry.md
4
4
  /** Every migration, newest first. Bodies live in `src/migrations`. */
5
5
  export const migrations = [
6
+ {
7
+ id: "tabler-icons-no-longer-transitive",
8
+ since: "1.1.40",
9
+ title: "@tabler/icons-react is no longer installed alongside Flow",
10
+ kind: "migration",
11
+ action: "manual",
12
+ remotePackage: true,
13
+ apply: "Only affects code that imports from `@tabler/icons-react` — typically a `<Icon><IconSomething /></Icon>` using an icon that is not in Flow's own set. Add the package to your own dependencies (`npm i @tabler/icons-react`); the imports themselves stay unchanged. Flow's own `Icon*` components are unaffected, keep their names, and render exactly as before.",
14
+ },
15
+ {
16
+ id: "option-value-inferred-from-mixed-children",
17
+ since: "1.1.12",
18
+ title: "Option: value is inferred from mixed children",
19
+ kind: "migration",
20
+ action: "none",
21
+ remotePackage: true,
22
+ apply: "No code change required for the option itself. An `Option` whose children are text plus an element — text and a `Badge`, text and an icon — now carries that text as its key, where it previously fell back to react-aria's generated `react-aria-N`. Check anywhere such a key was read back: a stored or server-side selection, a `defaultValue`/`value` matched against it, or a test asserting on it. Pass an explicit `value` to pin the key to something other than the display text.",
23
+ },
6
24
  {
7
25
  id: "use-design-tokens-build-metadata-removed",
8
26
  since: "1.0.16",
@@ -75,14 +93,23 @@ export const migrations = [
75
93
  remotePackage: false,
76
94
  apply: "Replace the import path `@mittwald/flow-react-components/password-tools` with `@mittwald/flow-react-components/mittwald-password-tools-js`.",
77
95
  },
96
+ {
97
+ id: "tabs-navigation-usage-to-tab-navigation",
98
+ since: "0.2.0-alpha.977",
99
+ title: "Tabs restyled; navigation usage moves to TabNavigation",
100
+ kind: "migration",
101
+ action: "manual",
102
+ remotePackage: true,
103
+ 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.",
104
+ },
78
105
  {
79
106
  id: "table-column-width-props",
80
107
  since: "0.2.0-alpha.956",
81
108
  title: "TableColumn: `maxWidth` removed, `width` and `minWidth` retyped",
82
109
  kind: "migration",
83
- action: "manual",
110
+ action: "codemod",
84
111
  remotePackage: true,
85
- 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`.",
112
+ 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.",
86
113
  },
87
114
  {
88
115
  id: "table-render-prop-removed",
@@ -190,7 +217,7 @@ export const migrations = [
190
217
  kind: "migration",
191
218
  action: "codemod",
192
219
  remotePackage: true,
193
- 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.",
220
+ 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.",
194
221
  },
195
222
  {
196
223
  id: "button-props-interfaces",
@@ -215,8 +242,8 @@ export const migrations = [
215
242
  since: "0.1.0-alpha.292",
216
243
  title: "Renamed CSS export",
217
244
  kind: "migration",
218
- action: "manual",
245
+ action: "codemod",
219
246
  remotePackage: false,
220
- apply: "Replace the import `@mittwald/flow-react-components/styles` with `@mittwald/flow-react-components/all.css`.",
247
+ 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.",
221
248
  },
222
249
  ];
@@ -47,4 +47,3 @@ export type RangeResult = ResolvedRange | UnresolvedRange;
47
47
  * fine; two callers computing the fact differently is how they drift.
48
48
  */
49
49
  export declare const resolveRange: (revision: string, deps: RangeDeps) => Promise<RangeResult>;
50
- //# sourceMappingURL=range.d.ts.map
@@ -30,4 +30,3 @@ export declare const fetchAllVersions: (names: string[], fetchOne: (name: string
30
30
  * the intersection, not any single package's list.
31
31
  */
32
32
  export declare const intersectVersions: (packages: PackageVersions[]) => string[];
33
- //# sourceMappingURL=registry.d.ts.map
@@ -60,4 +60,3 @@ export type ResolveTargetResult = {
60
60
  * stable line never drifts onto the collection branch.
61
61
  */
62
62
  export declare const resolveTarget: ({ revision, current, versions, distTags, }: ResolveTargetInput) => ResolveTargetResult;
63
- //# sourceMappingURL=target.d.ts.map