@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.
- package/README.md +35 -6
- package/dist/cli/codemod.d.ts +16 -0
- package/dist/cli/codemod.d.ts.map +1 -1
- package/dist/cli/codemod.js +21 -0
- package/dist/cli/list.d.ts +30 -1
- package/dist/cli/list.d.ts.map +1 -1
- package/dist/cli/list.js +15 -4
- package/dist/cli/upgrade.d.ts +7 -0
- package/dist/cli/upgrade.d.ts.map +1 -1
- package/dist/cli/upgrade.js +31 -20
- package/dist/cli.js +16 -1
- package/dist/install.d.ts +110 -11
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +163 -50
- package/dist/migrations/accent-box-color-to-background-color/transform.js +175 -0
- package/dist/migrations/action-prop-to-on-action/transform.js +82 -0
- package/dist/migrations/align-to-combine/transform.js +166 -0
- package/dist/migrations/button-color-accent-to-success/transform.js +126 -0
- package/dist/migrations/button-props-interfaces/transform.js +178 -0
- package/dist/migrations/color-primary-to-default/transform.js +135 -0
- package/dist/migrations/imports-to-package-root/transform.js +93 -0
- package/dist/migrations/muted-action-error-to-abort-action-error/transform.js +198 -0
- package/dist/migrations/package.json +3 -0
- package/dist/migrations/password-tools-rule/transform.js +173 -0
- package/dist/migrations/password-tools-subpath-renamed/transform.js +51 -0
- package/dist/migrations.generated.d.ts.map +1 -1
- package/dist/migrations.generated.js +12 -3
- package/dist/run/jscodeshift.d.ts +4 -1
- package/dist/run/jscodeshift.d.ts.map +1 -1
- package/dist/run/jscodeshift.js +60 -22
- package/dist/tools/package.json +3 -0
- package/dist/tools/to-remote-package.js +29 -0
- package/package.json +5 -6
- package/src/migrations/accent-box-color-to-background-color/transform.ts +0 -218
- package/src/migrations/action-prop-to-on-action/transform.ts +0 -110
- package/src/migrations/align-to-combine/transform.ts +0 -212
- package/src/migrations/button-color-accent-to-success/transform.ts +0 -164
- package/src/migrations/button-props-interfaces/transform.ts +0 -229
- package/src/migrations/color-primary-to-default/transform.ts +0 -173
- package/src/migrations/imports-to-package-root/transform.ts +0 -107
- package/src/migrations/muted-action-error-to-abort-action-error/transform.ts +0 -260
- package/src/migrations/password-tools-rule/transform.ts +0 -219
- package/src/migrations/password-tools-subpath-renamed/transform.ts +0 -61
- package/src/tools/to-remote-package.ts +0 -37
package/dist/install.js
CHANGED
|
@@ -1,38 +1,45 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
20
|
+
* The package manager for the project at `cwd`.
|
|
18
21
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
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
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Moves the background value of `AccentBox` from `color` to `backgroundColor`
|
|
5
|
+
* (alpha.786).
|
|
6
|
+
*
|
|
7
|
+
* `color` did not go away, it changed meaning: it used to set the background
|
|
8
|
+
* (`"blue" | "green" | "gradient" | "neutral"`) and now sets the content color
|
|
9
|
+
* (`"default" | "dark" | "light" | "dark-static" | "light-static"`). A blanket
|
|
10
|
+
* rename would therefore break every element that already uses the new meaning.
|
|
11
|
+
* The transform decides per value instead: a value from the new content-color
|
|
12
|
+
* union stays on `color`, every other literal moves to `backgroundColor`.
|
|
13
|
+
*
|
|
14
|
+
* The scope is deliberately narrow. Only JSX elements that resolve to
|
|
15
|
+
* `AccentBox` — imported (named or as a namespace) from
|
|
16
|
+
* `@mittwald/flow-react-components` or
|
|
17
|
+
* `@mittwald/flow-remote-react-components`, including their subpath entries —
|
|
18
|
+
* are touched.
|
|
19
|
+
*
|
|
20
|
+
* Two cases are left alone, because both are undecidable without knowing the
|
|
21
|
+
* value:
|
|
22
|
+
*
|
|
23
|
+
* - Values it cannot read (`color={expression}`), and expressions that mix both
|
|
24
|
+
* meanings (`color={flag ? "blue" : "dark"}`). The same expression means the
|
|
25
|
+
* background in old code and the content color in new code, and a mix has no
|
|
26
|
+
* single correct answer. An expression whose every value position is a
|
|
27
|
+
* literal on the same side of the split _is_ decidable and gets renamed:
|
|
28
|
+
* `color={flag ? "blue" : "green"}`.
|
|
29
|
+
* - An element that already carries `backgroundColor`. Moving `color` there would
|
|
30
|
+
* silently overwrite the explicit value.
|
|
31
|
+
*
|
|
32
|
+
* Both keep their `color` prop and need a look by hand.
|
|
33
|
+
*/
|
|
34
|
+
const accentBoxColorToBackgroundColorTransform = (fileInfo, { j }) => {
|
|
35
|
+
const flowPackages = [
|
|
36
|
+
"@mittwald/flow-react-components",
|
|
37
|
+
"@mittwald/flow-remote-react-components",
|
|
38
|
+
];
|
|
39
|
+
const affectedComponents = new Set(["AccentBox"]);
|
|
40
|
+
/** Values `color` still accepts — everything else was a background color. */
|
|
41
|
+
const contentColors = new Set([
|
|
42
|
+
"default",
|
|
43
|
+
"dark",
|
|
44
|
+
"light",
|
|
45
|
+
"dark-static",
|
|
46
|
+
"light-static",
|
|
47
|
+
]);
|
|
48
|
+
const isFlowImport = (source) => flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
|
|
49
|
+
/** The value of a string literal, or `undefined` for anything dynamic. */
|
|
50
|
+
const literalValue = (node) => {
|
|
51
|
+
if (!node || typeof node !== "object" || !("type" in node)) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const { type } = node;
|
|
55
|
+
if (type !== "StringLiteral" && type !== "Literal") {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const { value } = node;
|
|
59
|
+
return typeof value === "string" ? value : undefined;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Every literal that could become this attribute's value, or `undefined` when
|
|
63
|
+
* any of those positions is something we cannot read. The positions are the
|
|
64
|
+
* expression itself, both ternary branches, and the operands of `??`/`||` —
|
|
65
|
+
* plus only the right operand of `&&`, since `&&` yields its left operand
|
|
66
|
+
* only when that operand is falsy and a non-empty string never is.
|
|
67
|
+
*
|
|
68
|
+
* A list, not a single value, because this transform decides per attribute,
|
|
69
|
+
* not per literal: `color={flag ? "blue" : "dark"}` mixes a background with a
|
|
70
|
+
* content colour, and no single rename is right for both.
|
|
71
|
+
*/
|
|
72
|
+
const valueLiterals = (node) => {
|
|
73
|
+
const literal = literalValue(node);
|
|
74
|
+
if (literal !== undefined) {
|
|
75
|
+
return [literal];
|
|
76
|
+
}
|
|
77
|
+
if (!node || typeof node !== "object" || !("type" in node)) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const typed = node;
|
|
81
|
+
if (typed.type === "ConditionalExpression") {
|
|
82
|
+
const consequent = valueLiterals(typed.consequent);
|
|
83
|
+
const alternate = valueLiterals(typed.alternate);
|
|
84
|
+
return consequent && alternate
|
|
85
|
+
? [...consequent, ...alternate]
|
|
86
|
+
: undefined;
|
|
87
|
+
}
|
|
88
|
+
if (typed.type === "LogicalExpression") {
|
|
89
|
+
const right = valueLiterals(typed.right);
|
|
90
|
+
if (right === undefined) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
if (typed.operator === "&&") {
|
|
94
|
+
return right;
|
|
95
|
+
}
|
|
96
|
+
const left = valueLiterals(typed.left);
|
|
97
|
+
return left ? [...left, ...right] : undefined;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
};
|
|
101
|
+
const root = j(fileInfo.source, { parser: "tsx" });
|
|
102
|
+
// Local JSX identifier -> canonical component name (resolves `as` aliases).
|
|
103
|
+
const localToComponent = new Map();
|
|
104
|
+
// Local names of `import * as Flow` namespace imports from a Flow package.
|
|
105
|
+
const flowNamespaces = new Set();
|
|
106
|
+
root
|
|
107
|
+
.find(j.ImportDeclaration)
|
|
108
|
+
.filter((path) => isFlowImport(String(path.node.source.value)))
|
|
109
|
+
.forEach((path) => {
|
|
110
|
+
for (const specifier of path.node.specifiers ?? []) {
|
|
111
|
+
if (specifier.type === "ImportSpecifier" &&
|
|
112
|
+
specifier.imported.type === "Identifier" &&
|
|
113
|
+
affectedComponents.has(specifier.imported.name)) {
|
|
114
|
+
localToComponent.set(String(specifier.local?.name ?? specifier.imported.name), String(specifier.imported.name));
|
|
115
|
+
}
|
|
116
|
+
else if (specifier.type === "ImportNamespaceSpecifier" &&
|
|
117
|
+
specifier.local) {
|
|
118
|
+
flowNamespaces.add(String(specifier.local.name));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
if (localToComponent.size === 0 && flowNamespaces.size === 0) {
|
|
123
|
+
return fileInfo.source;
|
|
124
|
+
}
|
|
125
|
+
root.find(j.JSXOpeningElement).forEach((path) => {
|
|
126
|
+
const name = path.node.name;
|
|
127
|
+
let isAffected = false;
|
|
128
|
+
if (name.type === "JSXIdentifier") {
|
|
129
|
+
isAffected = localToComponent.has(name.name);
|
|
130
|
+
}
|
|
131
|
+
else if (name.type === "JSXMemberExpression" &&
|
|
132
|
+
name.object.type === "JSXIdentifier" &&
|
|
133
|
+
name.property.type === "JSXIdentifier") {
|
|
134
|
+
isAffected =
|
|
135
|
+
flowNamespaces.has(name.object.name) &&
|
|
136
|
+
affectedComponents.has(name.property.name);
|
|
137
|
+
}
|
|
138
|
+
if (!isAffected) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const attributes = path.node.attributes ?? [];
|
|
142
|
+
const isNamed = (attribute, key) => !!attribute &&
|
|
143
|
+
typeof attribute === "object" &&
|
|
144
|
+
attribute.type === "JSXAttribute" &&
|
|
145
|
+
attribute.name?.type ===
|
|
146
|
+
"JSXIdentifier" &&
|
|
147
|
+
attribute.name?.name === key;
|
|
148
|
+
// An explicit `backgroundColor` is the new API already — never overwrite it.
|
|
149
|
+
if (attributes.some((attribute) => isNamed(attribute, "backgroundColor"))) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
for (const attribute of attributes) {
|
|
153
|
+
if (!isNamed(attribute, "color") || attribute.type !== "JSXAttribute") {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const value = attribute.value;
|
|
157
|
+
const literals = value?.type === "JSXExpressionContainer"
|
|
158
|
+
? valueLiterals(value.expression)
|
|
159
|
+
: valueLiterals(value);
|
|
160
|
+
if (literals === undefined || literals.length === 0) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// All-or-nothing. Every value has to be a background colour for the
|
|
164
|
+
// rename to be right; all content colours means the element already uses
|
|
165
|
+
// the new meaning, and a mix has no single correct answer.
|
|
166
|
+
const backgrounds = literals.filter((literal) => !contentColors.has(literal));
|
|
167
|
+
if (backgrounds.length !== literals.length) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
attribute.name = j.jsxIdentifier("backgroundColor");
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
return root.toSource();
|
|
174
|
+
};
|
|
175
|
+
exports.default = accentBoxColorToBackgroundColorTransform;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Renames the `action` prop to `onAction` on `Action`.
|
|
5
|
+
*
|
|
6
|
+
* The scope is deliberately narrow. Only JSX elements that resolve to `Action`
|
|
7
|
+
* — imported (named or as a namespace) from `@mittwald/flow-react-components`
|
|
8
|
+
* or `@mittwald/flow-remote-react-components`, including their subpath entries
|
|
9
|
+
* — are touched. `Action` is not one of the generated remote components, but
|
|
10
|
+
* the remote package re-exports the `flr-universal` surface, which carries it.
|
|
11
|
+
* Same-named components from other packages are left untouched, and so is the
|
|
12
|
+
* `action` attribute of a plain `<form>`.
|
|
13
|
+
*
|
|
14
|
+
* An element that already carries `onAction` keeps it and only loses the stale
|
|
15
|
+
* `action` prop, which mirrors what the runtime fallback does: an explicit
|
|
16
|
+
* `onAction` wins.
|
|
17
|
+
*/
|
|
18
|
+
const actionPropToOnActionTransform = (fileInfo, { j }) => {
|
|
19
|
+
const flowPackages = [
|
|
20
|
+
"@mittwald/flow-react-components",
|
|
21
|
+
"@mittwald/flow-remote-react-components",
|
|
22
|
+
];
|
|
23
|
+
const affectedComponents = new Set(["Action"]);
|
|
24
|
+
const isFlowImport = (source) => flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
|
|
25
|
+
const root = j(fileInfo.source, { parser: "tsx" });
|
|
26
|
+
// Local JSX identifier -> canonical component name (resolves `as` aliases).
|
|
27
|
+
const localToComponent = new Map();
|
|
28
|
+
// Local names of `import * as Flow` namespace imports from a Flow package.
|
|
29
|
+
const flowNamespaces = new Set();
|
|
30
|
+
root
|
|
31
|
+
.find(j.ImportDeclaration)
|
|
32
|
+
.filter((path) => isFlowImport(String(path.node.source.value)))
|
|
33
|
+
.forEach((path) => {
|
|
34
|
+
for (const specifier of path.node.specifiers ?? []) {
|
|
35
|
+
if (specifier.type === "ImportSpecifier" &&
|
|
36
|
+
specifier.imported.type === "Identifier" &&
|
|
37
|
+
affectedComponents.has(specifier.imported.name)) {
|
|
38
|
+
localToComponent.set(String(specifier.local?.name ?? specifier.imported.name), String(specifier.imported.name));
|
|
39
|
+
}
|
|
40
|
+
else if (specifier.type === "ImportNamespaceSpecifier" &&
|
|
41
|
+
specifier.local) {
|
|
42
|
+
flowNamespaces.add(String(specifier.local.name));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (localToComponent.size === 0 && flowNamespaces.size === 0) {
|
|
47
|
+
return fileInfo.source;
|
|
48
|
+
}
|
|
49
|
+
root.find(j.JSXOpeningElement).forEach((path) => {
|
|
50
|
+
const name = path.node.name;
|
|
51
|
+
let isAffected = false;
|
|
52
|
+
if (name.type === "JSXIdentifier") {
|
|
53
|
+
isAffected = localToComponent.has(name.name);
|
|
54
|
+
}
|
|
55
|
+
else if (name.type === "JSXMemberExpression" &&
|
|
56
|
+
name.object.type === "JSXIdentifier" &&
|
|
57
|
+
name.property.type === "JSXIdentifier") {
|
|
58
|
+
isAffected =
|
|
59
|
+
flowNamespaces.has(name.object.name) &&
|
|
60
|
+
affectedComponents.has(name.property.name);
|
|
61
|
+
}
|
|
62
|
+
if (!isAffected) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const attributes = path.node.attributes ?? [];
|
|
66
|
+
const isNamed = (attribute, key) => attribute.type === "JSXAttribute" &&
|
|
67
|
+
attribute.name.type === "JSXIdentifier" &&
|
|
68
|
+
attribute.name.name === key;
|
|
69
|
+
const hasOnAction = attributes.some((attribute) => isNamed(attribute, "onAction"));
|
|
70
|
+
if (hasOnAction) {
|
|
71
|
+
path.node.attributes = attributes.filter((attribute) => !isNamed(attribute, "action"));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (const attribute of attributes) {
|
|
75
|
+
if (isNamed(attribute, "action") && attribute.type === "JSXAttribute") {
|
|
76
|
+
attribute.name.name = "onAction";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return root.toSource();
|
|
81
|
+
};
|
|
82
|
+
exports.default = actionPropToOnActionTransform;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Renames the `Align` component to `Combine`, and `AlignProps` to
|
|
5
|
+
* `CombineProps`.
|
|
6
|
+
*
|
|
7
|
+
* Only names imported from `@mittwald/flow-react-components` or
|
|
8
|
+
* `@mittwald/flow-remote-react-components` (including their subpath entries)
|
|
9
|
+
* are touched, so a same-named import from another package is left alone. An
|
|
10
|
+
* `Align` imported under a local alias (`import { Align as Row }`) keeps its
|
|
11
|
+
* alias — only the imported name changes. Namespace usages (`<Flow.Align />`,
|
|
12
|
+
* `Flow.AlignProps`) are rewritten as well.
|
|
13
|
+
*/
|
|
14
|
+
const alignToCombineTransform = (fileInfo, { j }) => {
|
|
15
|
+
const flowPackages = [
|
|
16
|
+
"@mittwald/flow-react-components",
|
|
17
|
+
"@mittwald/flow-remote-react-components",
|
|
18
|
+
];
|
|
19
|
+
const renames = new Map([
|
|
20
|
+
["Align", "Combine"],
|
|
21
|
+
["AlignProps", "CombineProps"],
|
|
22
|
+
]);
|
|
23
|
+
const isFlowImport = (source) => flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
|
|
24
|
+
// ast-types models `importKind` on the declaration only, while babel also
|
|
25
|
+
// puts it on the specifier — which is where a per-specifier `type X` lives.
|
|
26
|
+
const isTypeOnly = (specifier) => specifier.importKind === "type";
|
|
27
|
+
const makeValueImport = (specifier) => {
|
|
28
|
+
specifier.importKind = "value";
|
|
29
|
+
};
|
|
30
|
+
const root = j(fileInfo.source, { parser: "tsx" });
|
|
31
|
+
/** Local identifiers that refer to a renamed export, mapped to the new name. */
|
|
32
|
+
const localRenames = new Map();
|
|
33
|
+
/**
|
|
34
|
+
* Whether any import specifier was rewritten. An aliased `Align as Row` adds
|
|
35
|
+
* nothing to `localRenames` — its local name stays `Row` — so counting those
|
|
36
|
+
* alone would treat an alias-only file as untouched and discard the rewrite.
|
|
37
|
+
*/
|
|
38
|
+
let renamedAnImport = false;
|
|
39
|
+
/** Local names of `import * as Flow` namespace imports from a Flow package. */
|
|
40
|
+
const flowNamespaces = new Set();
|
|
41
|
+
const flowImports = root
|
|
42
|
+
.find(j.ImportDeclaration)
|
|
43
|
+
.filter((path) => isFlowImport(String(path.node.source.value)));
|
|
44
|
+
/**
|
|
45
|
+
* `imported:local` pairs the file already binds without a rename. A rename
|
|
46
|
+
* that lands on one of these must not add a second specifier for it — the
|
|
47
|
+
* name is available from the import that already carries it, which is also
|
|
48
|
+
* the entry that really exports it.
|
|
49
|
+
*/
|
|
50
|
+
const claimed = new Set();
|
|
51
|
+
flowImports.forEach((path) => {
|
|
52
|
+
for (const specifier of path.node.specifiers ?? []) {
|
|
53
|
+
if (specifier.type === "ImportNamespaceSpecifier" &&
|
|
54
|
+
specifier.local?.name) {
|
|
55
|
+
flowNamespaces.add(String(specifier.local.name));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (specifier.type !== "ImportSpecifier" ||
|
|
59
|
+
specifier.imported.type !== "Identifier" ||
|
|
60
|
+
renames.has(specifier.imported.name)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
claimed.add(`${specifier.imported.name}:${String(specifier.local?.name ?? specifier.imported.name)}`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
flowImports.forEach((path) => {
|
|
67
|
+
const specifiers = path.node.specifiers ?? [];
|
|
68
|
+
const survivors = [];
|
|
69
|
+
for (const specifier of specifiers) {
|
|
70
|
+
if (specifier.type !== "ImportSpecifier" ||
|
|
71
|
+
specifier.imported.type !== "Identifier") {
|
|
72
|
+
survivors.push(specifier);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const imported = specifier.imported.name;
|
|
76
|
+
const renamed = renames.get(imported);
|
|
77
|
+
if (!renamed) {
|
|
78
|
+
survivors.push(specifier);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const local = String(specifier.local?.name ?? imported);
|
|
82
|
+
const isAlias = local !== imported;
|
|
83
|
+
const newLocal = isAlias ? local : renamed;
|
|
84
|
+
const key = `${renamed}:${newLocal}`;
|
|
85
|
+
renamedAnImport = true;
|
|
86
|
+
if (!isAlias) {
|
|
87
|
+
// An aliased import keeps its local name and needs no further change.
|
|
88
|
+
localRenames.set(local, renamed);
|
|
89
|
+
}
|
|
90
|
+
// Renaming can collide with a name the file already binds — directly, or
|
|
91
|
+
// because a second old name maps onto the same new one. Either way the
|
|
92
|
+
// binding already exists, so this specifier goes away instead of
|
|
93
|
+
// producing a duplicate declaration, which would not parse.
|
|
94
|
+
const collision = claimed.has(key);
|
|
95
|
+
claimed.add(key);
|
|
96
|
+
if (collision) {
|
|
97
|
+
// A value import must not lose out to a type-only one.
|
|
98
|
+
if (!isTypeOnly(specifier)) {
|
|
99
|
+
for (const kept of survivors) {
|
|
100
|
+
if (kept.type === "ImportSpecifier" &&
|
|
101
|
+
kept.imported.type === "Identifier" &&
|
|
102
|
+
`${kept.imported.name}:${String(kept.local?.name ?? kept.imported.name)}` === key) {
|
|
103
|
+
makeValueImport(kept);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Mutate in place so an `import type` / `type X` modifier survives.
|
|
110
|
+
specifier.imported.name = renamed;
|
|
111
|
+
if (!isAlias && specifier.local) {
|
|
112
|
+
specifier.local.name = renamed;
|
|
113
|
+
}
|
|
114
|
+
survivors.push(specifier);
|
|
115
|
+
}
|
|
116
|
+
if (survivors.length === specifiers.length) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Every specifier moved to an import that already binds the name. Leaving
|
|
120
|
+
// the declaration behind would turn it into a side-effect import of an
|
|
121
|
+
// entry the file no longer uses.
|
|
122
|
+
if (survivors.length === 0) {
|
|
123
|
+
j(path).remove();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
path.node.specifiers = survivors;
|
|
127
|
+
});
|
|
128
|
+
if (!renamedAnImport && flowNamespaces.size === 0) {
|
|
129
|
+
return fileInfo.source;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* `JSXIdentifier` extends `Identifier`, so this one pass covers JSX tags,
|
|
133
|
+
* value references and type references alike. Positions where the name is not
|
|
134
|
+
* a reference to the import — an object key, a member's property, a JSX
|
|
135
|
+
* attribute name — are skipped.
|
|
136
|
+
*/
|
|
137
|
+
root.find(j.Identifier).forEach((path) => {
|
|
138
|
+
const parent = path.parent.node;
|
|
139
|
+
const isNamespaceMember = (parent.type === "MemberExpression" ||
|
|
140
|
+
parent.type === "JSXMemberExpression") &&
|
|
141
|
+
parent.property === path.node;
|
|
142
|
+
if (isNamespaceMember) {
|
|
143
|
+
const object = parent.object;
|
|
144
|
+
if (object.type === "Identifier" || object.type === "JSXIdentifier") {
|
|
145
|
+
const renamed = renames.get(path.node.name);
|
|
146
|
+
if (renamed && flowNamespaces.has(String(object.name))) {
|
|
147
|
+
path.node.name = renamed;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (parent.type === "ImportSpecifier" ||
|
|
153
|
+
parent.type === "JSXAttribute" ||
|
|
154
|
+
((parent.type === "ObjectProperty" || parent.type === "Property") &&
|
|
155
|
+
parent.key === path.node &&
|
|
156
|
+
!parent.computed)) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const renamed = localRenames.get(path.node.name);
|
|
160
|
+
if (renamed) {
|
|
161
|
+
path.node.name = renamed;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return root.toSource();
|
|
165
|
+
};
|
|
166
|
+
exports.default = alignToCombineTransform;
|