@mittwald/flow-codemods 1.2.0-next.6 → 1.2.0-next.61
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/catalog/entries.d.ts +0 -1
- package/dist/catalog/select.d.ts +0 -1
- package/dist/catalog/types.d.ts +0 -1
- package/dist/cli/args.d.ts +0 -1
- package/dist/cli/choose.d.ts +0 -1
- package/dist/cli/codemod.d.ts +16 -1
- package/dist/cli/codemod.js +21 -0
- package/dist/cli/list.d.ts +30 -2
- package/dist/cli/list.js +15 -4
- package/dist/cli/upgrade.d.ts +7 -1
- package/dist/cli/upgrade.js +31 -20
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +16 -1
- package/dist/flowPackages.generated.d.ts +0 -1
- package/dist/git.d.ts +0 -1
- package/dist/install.d.ts +110 -12
- package/dist/install.js +163 -50
- package/dist/manifest.d.ts +0 -1
- package/dist/migrations/action-prop-to-on-action/transform.js +91 -5
- package/dist/migrations/imports-to-package-root/transform.js +25 -2
- package/dist/migrations/password-tools-rule/transform.js +12 -1
- package/dist/migrations/popover-open-state-props/transform.js +101 -0
- package/dist/migrations/renamed-css-export/transform.js +73 -0
- package/dist/migrations/table-column-width-props/transform.js +131 -0
- package/dist/migrations.generated.d.ts +0 -1
- package/dist/migrations.generated.js +42 -6
- package/dist/resolve/range.d.ts +0 -1
- package/dist/resolve/registry.d.ts +0 -1
- package/dist/resolve/target.d.ts +0 -1
- package/dist/run/jscodeshift.d.ts +0 -1
- package/package.json +4 -2
- package/dist/catalog/entries.d.ts.map +0 -1
- package/dist/catalog/select.d.ts.map +0 -1
- package/dist/catalog/types.d.ts.map +0 -1
- package/dist/cli/args.d.ts.map +0 -1
- package/dist/cli/choose.d.ts.map +0 -1
- package/dist/cli/codemod.d.ts.map +0 -1
- package/dist/cli/list.d.ts.map +0 -1
- package/dist/cli/upgrade.d.ts.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/flowPackages.generated.d.ts.map +0 -1
- package/dist/git.d.ts.map +0 -1
- package/dist/install.d.ts.map +0 -1
- package/dist/manifest.d.ts.map +0 -1
- package/dist/migrations.generated.d.ts.map +0 -1
- package/dist/resolve/range.d.ts.map +0 -1
- package/dist/resolve/registry.d.ts.map +0 -1
- package/dist/resolve/target.d.ts.map +0 -1
- 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 {
|
|
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
|
};
|
package/dist/manifest.d.ts
CHANGED
|
@@ -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
|
-
|
|
75
|
-
|
|
76
|
-
attribute.
|
|
133
|
+
else {
|
|
134
|
+
for (const attribute of attributes) {
|
|
135
|
+
if (isNamed(attribute, "action") && attribute.type === "JSXAttribute") {
|
|
136
|
+
attribute.name.name = "onAction";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Whatever the prop was called before, the surviving `onAction` is the one
|
|
141
|
+
// that needs the wrap — including one the consumer had already renamed by
|
|
142
|
+
// hand, which carries the same type error.
|
|
143
|
+
for (const attribute of path.node.attributes ?? []) {
|
|
144
|
+
if (attribute.type !== "JSXAttribute" ||
|
|
145
|
+
!isNamed(attribute, "onAction")) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const container = attribute.value;
|
|
149
|
+
if (container?.type !== "JSXExpressionContainer") {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
// A bare reference is an identifier or a member expression. Everything
|
|
153
|
+
// else either is the handler already or is a way of producing one.
|
|
154
|
+
const expression = container.expression;
|
|
155
|
+
if (expression.type !== "Identifier" &&
|
|
156
|
+
expression.type !== "MemberExpression") {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (expression.type === "MemberExpression" &&
|
|
160
|
+
!hasPlainObjectChain(expression)) {
|
|
161
|
+
continue;
|
|
77
162
|
}
|
|
163
|
+
attribute.value = j.jsxExpressionContainer(j.arrowFunctionExpression([], j.callExpression(expression, [])));
|
|
78
164
|
}
|
|
79
165
|
});
|
|
80
166
|
return root.toSource();
|
|
@@ -29,6 +29,7 @@ const keptSubpaths = new Set([
|
|
|
29
29
|
// the current export surface
|
|
30
30
|
"internal",
|
|
31
31
|
"flr-universal",
|
|
32
|
+
"tunnel",
|
|
32
33
|
"nextjs",
|
|
33
34
|
"react-hook-form",
|
|
34
35
|
"mittwald-password-tools-js",
|
|
@@ -39,6 +40,21 @@ const keptSubpaths = new Set([
|
|
|
39
40
|
// owned by password-tools-subpath-renamed
|
|
40
41
|
"password-tools",
|
|
41
42
|
]);
|
|
43
|
+
/**
|
|
44
|
+
* Splits a bundler's asset query or fragment off a specifier. `all.css?url` is
|
|
45
|
+
* the subpath `all.css`, requested as a URL — not a subpath named
|
|
46
|
+
* `all.css?url`. Matching the whole string carried the suffix past the
|
|
47
|
+
* leave-alone set and into the catch-all below, which rewrote `import
|
|
48
|
+
* flowStyles from "…/all.css?url"` into a named import of a JS export that does
|
|
49
|
+
* not exist, and `import "…/all-layered.css?inline"` into a side-effect import
|
|
50
|
+
* of the package root — stylesheet gone, no error anywhere.
|
|
51
|
+
*/
|
|
52
|
+
const splitAssetSuffix = (subpath) => {
|
|
53
|
+
const suffixAt = subpath.search(/[?#]/);
|
|
54
|
+
return suffixAt === -1
|
|
55
|
+
? [subpath, ""]
|
|
56
|
+
: [subpath.slice(0, suffixAt), subpath.slice(suffixAt)];
|
|
57
|
+
};
|
|
42
58
|
const importsToPackageRootTransform = (fileInfo, { j }) => {
|
|
43
59
|
const flowPackage = "@mittwald/flow-react-components";
|
|
44
60
|
const root = j(fileInfo.source, {
|
|
@@ -50,14 +66,21 @@ const importsToPackageRootTransform = (fileInfo, { j }) => {
|
|
|
50
66
|
.forEach((i) => {
|
|
51
67
|
const specifiers = i.node.specifiers ?? [];
|
|
52
68
|
const importPath = String(i.node.source.value);
|
|
53
|
-
const importRelativePath = importPath.slice(flowPackage.length + 1);
|
|
69
|
+
const [importRelativePath, assetSuffix] = splitAssetSuffix(importPath.slice(flowPackage.length + 1));
|
|
54
70
|
if (keptSubpaths.has(importRelativePath)) {
|
|
55
71
|
return;
|
|
56
72
|
}
|
|
57
73
|
if (importRelativePath === "all.css" ||
|
|
58
74
|
importRelativePath === "globals.css" ||
|
|
59
75
|
importRelativePath === "global.css") {
|
|
60
|
-
i.node.source.value = `${flowPackage}/all.css`;
|
|
76
|
+
i.node.source.value = `${flowPackage}/all.css${assetSuffix}`;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Everything below rewrites a *module* import: it moves the specifier
|
|
80
|
+
// onto a JS entry and turns a default specifier into a named one. An
|
|
81
|
+
// asset import survives neither, and a suffix reaching this point names a
|
|
82
|
+
// path this transform cannot resolve to an export — leave it to a human.
|
|
83
|
+
if (assetSuffix !== "") {
|
|
61
84
|
return;
|
|
62
85
|
}
|
|
63
86
|
if (importRelativePath.startsWith("react-hook-form")) {
|
|
@@ -23,11 +23,22 @@ const passwordToolsRuleTransform = (fileInfo, { j }) => {
|
|
|
23
23
|
const flowPackages = [
|
|
24
24
|
"@mittwald/flow-react-components/mittwald-password-tools-js",
|
|
25
25
|
];
|
|
26
|
+
/**
|
|
27
|
+
* The same entry under the name it had when this change shipped. It was
|
|
28
|
+
* renamed later (alpha.1000, `password-tools-subpath-renamed`), and `since`
|
|
29
|
+
* puts that rename _after_ this transform — so a consumer coming from before
|
|
30
|
+
* alpha.802 still spells the import `password-tools` when this runs, and
|
|
31
|
+
* matching only the current name left `AsyncRule` untouched for the rename to
|
|
32
|
+
* carry onto an entry that does not export it. Kept out of `flowPackages`
|
|
33
|
+
* because `remoteScope.test.ts` holds every name in there against the export
|
|
34
|
+
* map the packages have _today_, where this one is gone.
|
|
35
|
+
*/
|
|
36
|
+
const renamedFlowPackage = "@mittwald/flow-react-components/password-tools";
|
|
26
37
|
const renames = new Map([
|
|
27
38
|
["AsyncRule", "Rule"],
|
|
28
39
|
["SyncRule", "Rule"],
|
|
29
40
|
]);
|
|
30
|
-
const isFlowImport = (source) => flowPackages.includes(source);
|
|
41
|
+
const isFlowImport = (source) => flowPackages.includes(source) || source === renamedFlowPackage;
|
|
31
42
|
// ast-types models `importKind` on the declaration only, while babel also
|
|
32
43
|
// puts it on the specifier — which is where a per-specifier `type X` lives.
|
|
33
44
|
const isTypeOnly = (specifier) => specifier.importKind === "type";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Renames the `defaultOpen` prop to `isDefaultOpen` on `Popover`,
|
|
5
|
+
* `ContextualHelp` and `ContextMenu`.
|
|
6
|
+
*
|
|
7
|
+
* The scope is deliberately narrow, and here that is not only about same-named
|
|
8
|
+
* components from other packages. `defaultOpen` is react-aria's own prop and
|
|
9
|
+
* Flow passes it straight through on `Select`, `MenuTrigger`, `Tooltip`,
|
|
10
|
+
* `TooltipTrigger`, `DialogTrigger`, `DatePicker` and `DateRangePicker`, where
|
|
11
|
+
* it did not change — renaming it there would remove a working prop.
|
|
12
|
+
*
|
|
13
|
+
* Only JSX elements that resolve to one of the three — imported (named or as a
|
|
14
|
+
* namespace) from `@mittwald/flow-react-components` or
|
|
15
|
+
* `@mittwald/flow-remote-react-components`, including their subpath entries —
|
|
16
|
+
* are touched.
|
|
17
|
+
*
|
|
18
|
+
* An element that already carries `isDefaultOpen` keeps it and only loses the
|
|
19
|
+
* stale `defaultOpen`, which mirrors the runtime: `isDefaultOpen` wins.
|
|
20
|
+
*
|
|
21
|
+
* The other two changes of this migration are not here. `isOpen` and
|
|
22
|
+
* `onOpenChange` changed behaviour, not spelling: whether a given `isOpen` is
|
|
23
|
+
* meant to control the popover, and whether an `onOpenChange` handler performed
|
|
24
|
+
* or suppressed the close, is not decidable from the source. `apply` asks for
|
|
25
|
+
* those by hand.
|
|
26
|
+
*/
|
|
27
|
+
const popoverOpenStatePropsTransform = (fileInfo, { j }) => {
|
|
28
|
+
const flowPackages = [
|
|
29
|
+
"@mittwald/flow-react-components",
|
|
30
|
+
"@mittwald/flow-remote-react-components",
|
|
31
|
+
];
|
|
32
|
+
const affectedComponents = new Set([
|
|
33
|
+
"Popover",
|
|
34
|
+
"ContextualHelp",
|
|
35
|
+
"ContextMenu",
|
|
36
|
+
]);
|
|
37
|
+
const isFlowImport = (source) => flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
|
|
38
|
+
const root = j(fileInfo.source, { parser: "tsx" });
|
|
39
|
+
// Local JSX identifier -> canonical component name (resolves `as` aliases).
|
|
40
|
+
const localToComponent = new Map();
|
|
41
|
+
// Local names of `import * as Flow` namespace imports from a Flow package.
|
|
42
|
+
const flowNamespaces = new Set();
|
|
43
|
+
root
|
|
44
|
+
.find(j.ImportDeclaration)
|
|
45
|
+
.filter((path) => isFlowImport(String(path.node.source.value)))
|
|
46
|
+
.forEach((path) => {
|
|
47
|
+
for (const specifier of path.node.specifiers ?? []) {
|
|
48
|
+
if (specifier.type === "ImportSpecifier" &&
|
|
49
|
+
specifier.imported.type === "Identifier" &&
|
|
50
|
+
affectedComponents.has(specifier.imported.name)) {
|
|
51
|
+
localToComponent.set(String(specifier.local?.name ?? specifier.imported.name), String(specifier.imported.name));
|
|
52
|
+
}
|
|
53
|
+
else if (specifier.type === "ImportNamespaceSpecifier" &&
|
|
54
|
+
specifier.local) {
|
|
55
|
+
flowNamespaces.add(String(specifier.local.name));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (localToComponent.size === 0 && flowNamespaces.size === 0) {
|
|
60
|
+
return fileInfo.source;
|
|
61
|
+
}
|
|
62
|
+
let changed = false;
|
|
63
|
+
root.find(j.JSXOpeningElement).forEach((path) => {
|
|
64
|
+
const name = path.node.name;
|
|
65
|
+
let isAffected = false;
|
|
66
|
+
if (name.type === "JSXIdentifier") {
|
|
67
|
+
isAffected = localToComponent.has(name.name);
|
|
68
|
+
}
|
|
69
|
+
else if (name.type === "JSXMemberExpression" &&
|
|
70
|
+
name.object.type === "JSXIdentifier" &&
|
|
71
|
+
name.property.type === "JSXIdentifier") {
|
|
72
|
+
isAffected =
|
|
73
|
+
flowNamespaces.has(name.object.name) &&
|
|
74
|
+
affectedComponents.has(name.property.name);
|
|
75
|
+
}
|
|
76
|
+
if (!isAffected) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const attributes = path.node.attributes ?? [];
|
|
80
|
+
const isNamed = (attribute, key) => attribute.type === "JSXAttribute" &&
|
|
81
|
+
attribute.name.type === "JSXIdentifier" &&
|
|
82
|
+
attribute.name.name === key;
|
|
83
|
+
if (!attributes.some((attribute) => isNamed(attribute, "defaultOpen"))) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
changed = true;
|
|
87
|
+
if (attributes.some((attribute) => isNamed(attribute, "isDefaultOpen"))) {
|
|
88
|
+
path.node.attributes = attributes.filter((attribute) => !isNamed(attribute, "defaultOpen"));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const attribute of attributes) {
|
|
92
|
+
if (isNamed(attribute, "defaultOpen") &&
|
|
93
|
+
attribute.type === "JSXAttribute" &&
|
|
94
|
+
attribute.name.type === "JSXIdentifier") {
|
|
95
|
+
attribute.name.name = "isDefaultOpen";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return changed ? root.toSource() : fileInfo.source;
|
|
100
|
+
};
|
|
101
|
+
exports.default = popoverOpenStatePropsTransform;
|
|
@@ -0,0 +1,73 @@
|
|
|
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 on the path, not a prefix: `./styles` was a single export with
|
|
16
|
+
* nothing underneath it, so there is no deeper path to rewrite and no risk of
|
|
17
|
+
* touching an unrelated `styles` of someone else's. A bundler query or fragment
|
|
18
|
+
* (`?url`, `?inline`) is split off before that comparison and carried over — it
|
|
19
|
+
* addresses the export, it does not name a different one.
|
|
20
|
+
*
|
|
21
|
+
* What this cannot reach is an `@import` in a `.css` or `.scss` file —
|
|
22
|
+
* jscodeshift only walks the JavaScript and TypeScript extensions. `apply` says
|
|
23
|
+
* so.
|
|
24
|
+
*/
|
|
25
|
+
const renamedCssExportTransform = (fileInfo, { j }) => {
|
|
26
|
+
const root = j(fileInfo.source, { parser: "tsx" });
|
|
27
|
+
let changed = false;
|
|
28
|
+
// A bundler addresses an asset through a query or fragment on the specifier,
|
|
29
|
+
// so `…/styles?url` is the old export requested as a URL. The rename applies
|
|
30
|
+
// to it just the same: match the path, carry the suffix over. Comparing the
|
|
31
|
+
// whole string leaves it pointing at an export the package no longer has.
|
|
32
|
+
const rewrite = (node) => {
|
|
33
|
+
if (typeof node.value !== "string") {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const suffixAt = node.value.search(/[?#]/);
|
|
37
|
+
const [path, suffix] = suffixAt === -1
|
|
38
|
+
? [node.value, ""]
|
|
39
|
+
: [node.value.slice(0, suffixAt), node.value.slice(suffixAt)];
|
|
40
|
+
if (path !== oldPath) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
node.value = `${newPath}${suffix}`;
|
|
44
|
+
changed = true;
|
|
45
|
+
};
|
|
46
|
+
// import … from "…", import "…"
|
|
47
|
+
root.find(j.ImportDeclaration).forEach((path) => rewrite(path.node.source));
|
|
48
|
+
// export … from "…", export * from "…"
|
|
49
|
+
root
|
|
50
|
+
.find(j.ExportNamedDeclaration)
|
|
51
|
+
.forEach((path) => path.node.source && rewrite(path.node.source));
|
|
52
|
+
root
|
|
53
|
+
.find(j.ExportAllDeclaration)
|
|
54
|
+
.forEach((path) => rewrite(path.node.source));
|
|
55
|
+
// import("…") and require("…")
|
|
56
|
+
root
|
|
57
|
+
.find(j.CallExpression)
|
|
58
|
+
.filter((path) => {
|
|
59
|
+
const callee = path.node.callee;
|
|
60
|
+
return (callee.type === "Import" ||
|
|
61
|
+
(callee.type === "Identifier" && callee.name === "require"));
|
|
62
|
+
})
|
|
63
|
+
.forEach((path) => {
|
|
64
|
+
const [argument] = path.node.arguments;
|
|
65
|
+
if (argument &&
|
|
66
|
+
(argument.type === "StringLiteral" || argument.type === "Literal")) {
|
|
67
|
+
rewrite(argument);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// Returning the source untouched keeps this file out of the "changed" count.
|
|
71
|
+
return changed ? root.toSource() : fileInfo.source;
|
|
72
|
+
};
|
|
73
|
+
exports.default = renamedCssExportTransform;
|