@mittwald/flow-codemods 1.2.0-next.3 → 1.2.0-next.30
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.generated.d.ts.map +1 -1
- package/dist/migrations.generated.js +18 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
# @mittwald/flow-codemods
|
|
2
2
|
|
|
3
3
|
Codemods and an upgrade CLI for consumers of [Flow](https://flow.mittwald.de),
|
|
4
|
-
mittwald's design system. Run it
|
|
5
|
-
|
|
4
|
+
mittwald's design system. Run it one-off — there is no reason to install it as a
|
|
5
|
+
dependency.
|
|
6
6
|
|
|
7
7
|
```shell
|
|
8
8
|
npx @mittwald/flow-codemods@latest upgrade
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Use whatever your project's package manager calls that: `npx …` (npm, and Yarn
|
|
12
|
+
Classic, which has no `dlx`), `pnpm dlx …`, `yarn dlx …`, `bun x …`. Every
|
|
13
|
+
command the CLI prints for you to copy uses the form it detected for your
|
|
14
|
+
project, so the examples below stay on `npx` only because a README cannot know.
|
|
15
|
+
|
|
11
16
|
## Commands
|
|
12
17
|
|
|
13
18
|
### `upgrade [revision]`
|
|
14
19
|
|
|
15
|
-
Bumps every
|
|
16
|
-
|
|
20
|
+
Bumps every Flow-line dependency in `package.json` to a resolved target,
|
|
21
|
+
installs, then runs the codemod of every migration up to that target.
|
|
22
|
+
|
|
23
|
+
"Flow line" is wider than `@mittwald/flow-*`: every package published from the
|
|
24
|
+
Flow monorepo shares one version, so `@mittwald/ext-bridge`,
|
|
25
|
+
`@mittwald/mstudio-ext-react-components` and `@mittwald/react-tunnel` move too.
|
|
17
26
|
|
|
18
27
|
`revision` is one of:
|
|
19
28
|
|
|
@@ -33,6 +42,24 @@ versions your package supports, not one it installs, and narrowing `^1.0.0` to
|
|
|
33
42
|
`^1.0.14` would change what _your_ consumers are allowed to install. That is
|
|
34
43
|
your call, not the command's.
|
|
35
44
|
|
|
45
|
+
#### Which package manager installs
|
|
46
|
+
|
|
47
|
+
Detected by walking **up** from the directory you run in: lockfiles first, then
|
|
48
|
+
`packageManager`, then `devEngines.packageManager`, then the metadata a manager
|
|
49
|
+
leaves in `node_modules`. Walking up is what makes a workspace package work — it
|
|
50
|
+
has no lockfile of its own, and detecting in that directory alone used to fall
|
|
51
|
+
back to `npm install`, which on a `workspace:*` manifest fails outright. npm
|
|
52
|
+
stays the fallback when nothing says anything: it is always there next to Node.
|
|
53
|
+
|
|
54
|
+
A `packageManager` pin is honoured. If the binary on `PATH` already satisfies
|
|
55
|
+
it, that one runs; otherwise the install goes through corepack (with the
|
|
56
|
+
download prompt disabled, since this runs unattended). If neither works, the
|
|
57
|
+
command refuses **before** installing and names the pin, the version it found,
|
|
58
|
+
and where to get the right one — rather than installing with the wrong manager.
|
|
59
|
+
|
|
60
|
+
The log line names the agent, the pin and the command it actually ran, so a
|
|
61
|
+
wrong detection is visible instead of silent.
|
|
62
|
+
|
|
36
63
|
After installing, `upgrade` runs the codemod of every migration whose `since` is
|
|
37
64
|
at or below the target and prints the ones with no codemod, for you to apply by
|
|
38
65
|
hand.
|
|
@@ -65,8 +92,10 @@ Shows migrations — codemod and by-hand alike — without touching the project.
|
|
|
65
92
|
read-only planning entry point: run it before `upgrade` to see what a bump would
|
|
66
93
|
involve.
|
|
67
94
|
|
|
68
|
-
- `list` (no argument) — the whole catalogue.
|
|
69
|
-
|
|
95
|
+
- `list` (no argument) — the whole catalogue. Hits no network. It does read
|
|
96
|
+
lockfiles and `package.json` up the tree, but only to work out whether the
|
|
97
|
+
commands it prints should say `npx`, `pnpm dlx`, `yarn dlx` or `bun x` — a
|
|
98
|
+
command you can paste is worth more than never touching a manifest.
|
|
70
99
|
- `list [revision]` — the same manifest read, registry fetch, and revision
|
|
71
100
|
resolution `upgrade [revision]` does, showing exactly the range it would act
|
|
72
101
|
on, without writing anything. `revision` takes the same values as `upgrade`'s
|
package/dist/cli/codemod.d.ts
CHANGED
|
@@ -15,6 +15,22 @@ import { runCodemod } from "../run/jscodeshift.js";
|
|
|
15
15
|
* a different `cwd`.
|
|
16
16
|
*/
|
|
17
17
|
export declare const resolveSourcePath: (explicit: string | undefined, cwd: string, exists?: (path: string) => boolean) => string;
|
|
18
|
+
/**
|
|
19
|
+
* The same choice as `resolveSourcePath`, in the form a reader would type.
|
|
20
|
+
*
|
|
21
|
+
* `list` prints a runnable command per codemod entry, and it used to hardcode
|
|
22
|
+
* `src` there. On a project whose sources are anywhere else, pasting that line
|
|
23
|
+
* runs the codemod against a directory that does not exist — and the run then
|
|
24
|
+
* reports "no files under <path> were processed. Is the path right?", sending
|
|
25
|
+
* the reader after a path they were handed. So the printed command has to carry
|
|
26
|
+
* the path actually in use.
|
|
27
|
+
*
|
|
28
|
+
* Relative, not the absolute result of `resolveSourcePath`: a copy-pasteable
|
|
29
|
+
* command should stay short and keep working from the same directory the reader
|
|
30
|
+
* already is in. `"."` when the resolved path is `cwd` itself — a bare command
|
|
31
|
+
* with no argument would default back to `src` and pick the wrong tree.
|
|
32
|
+
*/
|
|
33
|
+
export declare const displaySourcePath: (explicit: string | undefined, cwd: string, exists?: (path: string) => boolean) => string;
|
|
18
34
|
export interface CodemodCommandDeps {
|
|
19
35
|
cwd: string;
|
|
20
36
|
log: (message: string) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codemod.d.ts","sourceRoot":"","sources":["../../src/cli/codemod.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAmB,MAAM,uBAAuB,CAAC;AAEpE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,aAClB,MAAM,GAAG,SAAS,OACvB,MAAM,WACH,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,KAChC,MAKF,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,OAAO,UAAU,CAAC;CACzB;AAED,eAAO,MAAM,gBAAgB,WACnB,aAAa,qBACW,kBAAkB,KACjD,OAAO,CAAC,MAAM,CA6DhB,CAAC"}
|
|
1
|
+
{"version":3,"file":"codemod.d.ts","sourceRoot":"","sources":["../../src/cli/codemod.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAmB,MAAM,uBAAuB,CAAC;AAEpE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,aAClB,MAAM,GAAG,SAAS,OACvB,MAAM,WACH,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,KAChC,MAKF,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,aAClB,MAAM,GAAG,SAAS,OACvB,MAAM,WACH,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,KAChC,MAKF,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,OAAO,UAAU,CAAC;CACzB;AAED,eAAO,MAAM,gBAAgB,WACnB,aAAa,qBACW,kBAAkB,KACjD,OAAO,CAAC,MAAM,CA6DhB,CAAC"}
|
package/dist/cli/codemod.js
CHANGED
|
@@ -22,6 +22,27 @@ export const resolveSourcePath = (explicit, cwd, exists = existsSync) => {
|
|
|
22
22
|
}
|
|
23
23
|
return exists(join(cwd, "src")) ? join(cwd, "src") : cwd;
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* The same choice as `resolveSourcePath`, in the form a reader would type.
|
|
27
|
+
*
|
|
28
|
+
* `list` prints a runnable command per codemod entry, and it used to hardcode
|
|
29
|
+
* `src` there. On a project whose sources are anywhere else, pasting that line
|
|
30
|
+
* runs the codemod against a directory that does not exist — and the run then
|
|
31
|
+
* reports "no files under <path> were processed. Is the path right?", sending
|
|
32
|
+
* the reader after a path they were handed. So the printed command has to carry
|
|
33
|
+
* the path actually in use.
|
|
34
|
+
*
|
|
35
|
+
* Relative, not the absolute result of `resolveSourcePath`: a copy-pasteable
|
|
36
|
+
* command should stay short and keep working from the same directory the reader
|
|
37
|
+
* already is in. `"."` when the resolved path is `cwd` itself — a bare command
|
|
38
|
+
* with no argument would default back to `src` and pick the wrong tree.
|
|
39
|
+
*/
|
|
40
|
+
export const displaySourcePath = (explicit, cwd, exists = existsSync) => {
|
|
41
|
+
if (explicit !== undefined) {
|
|
42
|
+
return explicit;
|
|
43
|
+
}
|
|
44
|
+
return exists(join(cwd, "src")) ? "src" : ".";
|
|
45
|
+
};
|
|
25
46
|
export const runSingleCodemod = async (parsed, { cwd, log, run = runCodemod }) => {
|
|
26
47
|
const id = parsed.id ?? "";
|
|
27
48
|
const entry = allEntries.find((candidate) => candidate.id === id);
|
package/dist/cli/list.d.ts
CHANGED
|
@@ -17,6 +17,24 @@ export interface RenderListInput {
|
|
|
17
17
|
color?: boolean;
|
|
18
18
|
/** Terminal width to wrap prose to. */
|
|
19
19
|
width?: number;
|
|
20
|
+
/**
|
|
21
|
+
* How to invoke this package in a printed command — e.g. `npx
|
|
22
|
+
* @mittwald/flow-codemods@latest` or `pnpm dlx …`.
|
|
23
|
+
*
|
|
24
|
+
* Defaults to the `npx` form, which is right for npm and for Yarn Classic
|
|
25
|
+
* (which has no `dlx`). A pnpm, Yarn Berry or Bun project gets a command it
|
|
26
|
+
* cannot paste otherwise.
|
|
27
|
+
*/
|
|
28
|
+
invoke?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The path argument to print in each codemod entry's runnable command.
|
|
31
|
+
*
|
|
32
|
+
* Defaults to `src` — the same default `resolveSourcePath` applies — but a
|
|
33
|
+
* caller that knows better must say so, or the printed command sends the
|
|
34
|
+
* reader at a directory their project does not have. See
|
|
35
|
+
* `displaySourcePath`.
|
|
36
|
+
*/
|
|
37
|
+
path?: string;
|
|
20
38
|
/**
|
|
21
39
|
* Render the frame around the entries: the context on top (the range, the
|
|
22
40
|
* catch-up legend) and the summary at the bottom (the counts). On by default.
|
|
@@ -30,6 +48,13 @@ export interface RenderListInput {
|
|
|
30
48
|
frame?: boolean;
|
|
31
49
|
}
|
|
32
50
|
export declare const stripAnsi: (text: string) => string;
|
|
51
|
+
/**
|
|
52
|
+
* The invocation to print when nobody says otherwise.
|
|
53
|
+
*
|
|
54
|
+
* `npx` is also correct for Yarn Classic, which has no `dlx` — the library
|
|
55
|
+
* resolves both to it.
|
|
56
|
+
*/
|
|
57
|
+
export declare const defaultInvoke = "npx @mittwald/flow-codemods@latest";
|
|
33
58
|
/**
|
|
34
59
|
* The migrations for a version range, as text or JSON.
|
|
35
60
|
*
|
|
@@ -42,7 +67,7 @@ export declare const stripAnsi: (text: string) => string;
|
|
|
42
67
|
* so the rendering is deterministic in a test — `painter` builds its palette
|
|
43
68
|
* from `color` alone, never from the terminal. `cli.ts` supplies them.
|
|
44
69
|
*/
|
|
45
|
-
export declare const renderList: ({ entries, range, json, color, width, frame, }: RenderListInput) => string;
|
|
70
|
+
export declare const renderList: ({ entries, range, json, color, width, path, invoke, frame, }: RenderListInput) => string;
|
|
46
71
|
export interface ListDeps extends RangeDeps {
|
|
47
72
|
/**
|
|
48
73
|
* Raw output writer — matches `process.stdout.write`'s own contract: no
|
|
@@ -51,6 +76,10 @@ export interface ListDeps extends RangeDeps {
|
|
|
51
76
|
write: (text: string) => void;
|
|
52
77
|
color?: boolean;
|
|
53
78
|
width?: number;
|
|
79
|
+
/** Printed in each codemod entry's command — see `RenderListInput.path`. */
|
|
80
|
+
path?: string;
|
|
81
|
+
/** Printed as the command's prefix — see `RenderListInput.invoke`. */
|
|
82
|
+
invoke?: string;
|
|
54
83
|
}
|
|
55
84
|
export declare const defaultListDeps: (cwd: string) => ListDeps;
|
|
56
85
|
/**
|
package/dist/cli/list.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/cli/list.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC,IAAI,EAAE,OAAO,CAAC;IACd,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAkCD,eAAO,MAAM,SAAS,SAAU,MAAM,KAAG,MAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/cli/list.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC,IAAI,EAAE,OAAO,CAAC;IACd,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAkCD,eAAO,MAAM,SAAS,SAAU,MAAM,KAAG,MAAgC,CAAC;AAkE1E;;;;;GAKG;AACH,eAAO,MAAM,aAAa,uCAAuC,CAAC;AAsKlE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,iEASpB,eAAe,KAAG,MAiEpB,CAAC;AAEF,MAAM,WAAW,QAAS,SAAQ,SAAS;IACzC;;;OAGG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,eAAe,QAAS,MAAM,KAAG,QAG5C,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,OAAO,WACV,aAAa,QACf,QAAQ,KACb,OAAO,CAAC,MAAM,CAmChB,CAAC"}
|
package/dist/cli/list.js
CHANGED
|
@@ -83,6 +83,13 @@ const painter = (color) => {
|
|
|
83
83
|
blue: palette.blue,
|
|
84
84
|
};
|
|
85
85
|
};
|
|
86
|
+
/**
|
|
87
|
+
* The invocation to print when nobody says otherwise.
|
|
88
|
+
*
|
|
89
|
+
* `npx` is also correct for Yarn Classic, which has no `dlx` — the library
|
|
90
|
+
* resolves both to it.
|
|
91
|
+
*/
|
|
92
|
+
export const defaultInvoke = "npx @mittwald/flow-codemods@latest";
|
|
86
93
|
const indent = " ";
|
|
87
94
|
const labelWidth = 8;
|
|
88
95
|
/** One `apply` row: dim label, wrapped body beside it. */
|
|
@@ -105,7 +112,7 @@ const field = (label, value, width, paint) => {
|
|
|
105
112
|
* see `selectEntries`.
|
|
106
113
|
*/
|
|
107
114
|
const isCatchUp = (entry, current) => current !== undefined && lte(entry.since, current);
|
|
108
|
-
const renderEntry = (entry, width, color, catchUp) => {
|
|
115
|
+
const renderEntry = (entry, width, color, catchUp, path, invoke) => {
|
|
109
116
|
const paint = painter(color);
|
|
110
117
|
const action = actions[entry.action];
|
|
111
118
|
// Hollow vs filled: catch-up shipped at or before `current`, so it may
|
|
@@ -127,7 +134,7 @@ const renderEntry = (entry, width, color, catchUp) => {
|
|
|
127
134
|
...field("apply", entry.apply, width, paint),
|
|
128
135
|
];
|
|
129
136
|
if (entry.action === "codemod") {
|
|
130
|
-
lines.push("", `${indent}${paint.dim("$")} ${paint.code(
|
|
137
|
+
lines.push("", `${indent}${paint.dim("$")} ${paint.code(`${invoke} ${entry.id} ${path}`)}`);
|
|
131
138
|
}
|
|
132
139
|
return lines.join("\n");
|
|
133
140
|
};
|
|
@@ -203,7 +210,7 @@ const renderSummary = (selected, color) => {
|
|
|
203
210
|
* so the rendering is deterministic in a test — `painter` builds its palette
|
|
204
211
|
* from `color` alone, never from the terminal. `cli.ts` supplies them.
|
|
205
212
|
*/
|
|
206
|
-
export const renderList = ({ entries, range, json, color = false, width = 80, frame = true, }) => {
|
|
213
|
+
export const renderList = ({ entries, range, json, color = false, width = 80, path = "src", invoke = defaultInvoke, frame = true, }) => {
|
|
207
214
|
// The two paths differ in their bounds, deliberately: unbounded, this is a
|
|
208
215
|
// plain catalogue browse — every entry, sorted, regardless of whether it
|
|
209
216
|
// would apply to any given range. Bounded, it answers "what does this
|
|
@@ -234,7 +241,7 @@ export const renderList = ({ entries, range, json, color = false, width = 80, fr
|
|
|
234
241
|
return "Nothing to migrate in that range.\n";
|
|
235
242
|
}
|
|
236
243
|
const body = selected
|
|
237
|
-
.map((entry) => renderEntry(entry, width, color, isCatchUp(entry, range?.from)))
|
|
244
|
+
.map((entry) => renderEntry(entry, width, color, isCatchUp(entry, range?.from), path, invoke))
|
|
238
245
|
.join("\n\n");
|
|
239
246
|
if (!frame) {
|
|
240
247
|
return `${body}\n`;
|
|
@@ -274,6 +281,8 @@ export const runList = async (parsed, deps) => {
|
|
|
274
281
|
json: parsed.json,
|
|
275
282
|
color: deps.color,
|
|
276
283
|
width: deps.width,
|
|
284
|
+
path: deps.path,
|
|
285
|
+
invoke: deps.invoke,
|
|
277
286
|
}));
|
|
278
287
|
return 0;
|
|
279
288
|
}
|
|
@@ -288,6 +297,8 @@ export const runList = async (parsed, deps) => {
|
|
|
288
297
|
json: parsed.json,
|
|
289
298
|
color: deps.color,
|
|
290
299
|
width: deps.width,
|
|
300
|
+
path: deps.path,
|
|
301
|
+
invoke: deps.invoke,
|
|
291
302
|
}));
|
|
292
303
|
return 0;
|
|
293
304
|
};
|
package/dist/cli/upgrade.d.ts
CHANGED
|
@@ -13,6 +13,13 @@ export interface UpgradeDeps {
|
|
|
13
13
|
isDirty: (cwd: string) => boolean;
|
|
14
14
|
readInstalledVersion: (cwd: string, name: string) => string | undefined;
|
|
15
15
|
log: (message: string) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Emit ANSI colour in the by-hand list. Off by default so a test sees plain
|
|
18
|
+
* text.
|
|
19
|
+
*/
|
|
20
|
+
color?: boolean;
|
|
21
|
+
/** Terminal width to wrap the by-hand list's prose to. */
|
|
22
|
+
width?: number;
|
|
16
23
|
}
|
|
17
24
|
export declare const defaultUpgradeDeps: (cwd: string) => UpgradeDeps;
|
|
18
25
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../src/cli/upgrade.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAItE,OAAO,
|
|
1
|
+
{"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../src/cli/upgrade.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAItE,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAsB,MAAM,uBAAuB,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAI/C,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,OAAO,EAAE,aAAa,CAAC;IACvB,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,2EAA2E;IAC3E,MAAM,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC7D,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAClC,oBAAoB,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACxE,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,kBAAkB,QAAS,MAAM,KAAG,WAS/C,CAAC;AA2BH;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,WACb,aAAa,QACf,WAAW,KAChB,OAAO,CAAC,MAAM,CAqNhB,CAAC"}
|
package/dist/cli/upgrade.js
CHANGED
|
@@ -5,12 +5,12 @@ import { allEntries } from "../catalog/entries.js";
|
|
|
5
5
|
import { selectEntries } from "../catalog/select.js";
|
|
6
6
|
import { flowPackages } from "../flowPackages.generated.js";
|
|
7
7
|
import { hasUncommittedChanges } from "../git.js";
|
|
8
|
-
import { detectPackageManagerIn, runInstall, } from "../install.js";
|
|
8
|
+
import { detectPackageManagerIn, resolveInvoke, runInstall, } from "../install.js";
|
|
9
9
|
import { applyTarget, isWritable } from "../manifest.js";
|
|
10
10
|
import { fetchVersions } from "../resolve/registry.js";
|
|
11
11
|
import { readInstalledVersion, resolveRange } from "../resolve/range.js";
|
|
12
12
|
import { runCodemod } from "../run/jscodeshift.js";
|
|
13
|
-
import { resolveSourcePath } from "./codemod.js";
|
|
13
|
+
import { displaySourcePath, resolveSourcePath } from "./codemod.js";
|
|
14
14
|
import { renderList } from "./list.js";
|
|
15
15
|
export const defaultUpgradeDeps = (cwd) => ({
|
|
16
16
|
cwd,
|
|
@@ -121,13 +121,16 @@ export const runUpgrade = async (parsed, deps) => {
|
|
|
121
121
|
const indent = detectIndent(manifestRaw);
|
|
122
122
|
writeFileSync(manifestPath, `${JSON.stringify(applyTarget(manifest, target, flowPackages), null, indent)}\n`, "utf8");
|
|
123
123
|
reportDependencies();
|
|
124
|
-
const manager = detectPackageManagerIn(cwd);
|
|
125
|
-
log(`Installing with ${manager}`);
|
|
124
|
+
const manager = await detectPackageManagerIn(cwd);
|
|
126
125
|
try {
|
|
127
|
-
|
|
126
|
+
// Logged after the run, not before: `install` returns what it actually
|
|
127
|
+
// ran — agent, pin and command line — so a wrong detection is visible in
|
|
128
|
+
// the output instead of hidden behind a bare manager name. On a throw the
|
|
129
|
+
// recovery message below carries the reason anyway.
|
|
130
|
+
log(`Installed with ${deps.install(manager, cwd)}`);
|
|
128
131
|
}
|
|
129
132
|
catch (error) {
|
|
130
|
-
log(`The dependency bump was written but the install failed, so package.json is on\n${target} while node_modules still holds ${current}.\n\
|
|
133
|
+
log(`The dependency bump was written but the install failed, so package.json is on\n${target} while node_modules still holds ${current}.\n\nDetected package manager: ${manager.agent}${manager.version === undefined ? "" : ` (pinned ${manager.version})`}. If that is wrong, finish the install yourself with the right one —\nthe bump in package.json is already correct.\n\nOtherwise re-run this command once the install works, or undo the bump with\n git checkout package.json\n\n${error instanceof Error ? error.message : error}`);
|
|
131
134
|
return 1;
|
|
132
135
|
}
|
|
133
136
|
}
|
|
@@ -184,32 +187,40 @@ export const runUpgrade = async (parsed, deps) => {
|
|
|
184
187
|
log(` ${entry.id}: ${result.changed} file(s) changed`);
|
|
185
188
|
}
|
|
186
189
|
}
|
|
187
|
-
// Selection has no lower bound (see `selectEntries`), so this loop can run
|
|
188
|
-
// every codemod in the catalogue on a project that never crossed a version
|
|
189
|
-
// at all — "N run, 0 changed" is the confirmation that there was nothing to
|
|
190
|
-
// catch up on, not a list of new work.
|
|
191
|
-
if (ranCount > 0) {
|
|
192
|
-
log(`\n${ranCount} codemod${ranCount === 1 ? "" : "s"} run, ${changedCount} changed something.`);
|
|
193
|
-
}
|
|
194
190
|
if (byHand.length > 0) {
|
|
195
191
|
log(`\n${byHand.length} migration(s) in this range have no codemod — apply them by hand:\n`);
|
|
196
|
-
// `frame: false` — this already printed the heading above and
|
|
197
|
-
//
|
|
198
|
-
// renderList's own frame
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
// the only thing left that says so now that hiding it is gone.
|
|
192
|
+
// `frame: false` — this already printed the heading above, and the
|
|
193
|
+
// closing summaries below cover the same aggregate ("N codemods run, N
|
|
194
|
+
// changed something") renderList's own frame would otherwise repeat.
|
|
195
|
+
// `range` is still passed so each entry gets its catch-up mark — a manual
|
|
196
|
+
// entry that shipped at or before `current` may already be done, and the
|
|
197
|
+
// mark is the only thing left that says so now that hiding it is gone.
|
|
203
198
|
log(renderList({
|
|
204
199
|
entries: byHand,
|
|
205
200
|
range: { from: current, to: target },
|
|
206
201
|
json: false,
|
|
202
|
+
color: deps.color,
|
|
203
|
+
width: deps.width,
|
|
204
|
+
// The same path this run used, so a command copied out of the by-hand
|
|
205
|
+
// block works instead of falling back to a hardcoded `src`.
|
|
206
|
+
path: displaySourcePath(parsed.path, cwd),
|
|
207
|
+
invoke: await resolveInvoke(cwd),
|
|
207
208
|
frame: false,
|
|
208
209
|
}));
|
|
209
210
|
}
|
|
210
211
|
else {
|
|
211
212
|
log("\nNo migration in this range required a change by hand.");
|
|
212
213
|
}
|
|
214
|
+
// Both closing summaries trail everything else, not just each other: a
|
|
215
|
+
// multi-screen by-hand list would otherwise scroll them out of sight before
|
|
216
|
+
// the reader reaches the end — exactly when they want the counts. Selection
|
|
217
|
+
// has no lower bound (see `selectEntries`), so this loop can run every
|
|
218
|
+
// codemod in the catalogue on a project that never crossed a version at
|
|
219
|
+
// all — "N run, 0 changed" is the confirmation that there was nothing to
|
|
220
|
+
// catch up on, not a list of new work.
|
|
221
|
+
if (ranCount > 0) {
|
|
222
|
+
log(`\n${ranCount} codemod${ranCount === 1 ? "" : "s"} run, ${changedCount} changed something.`);
|
|
223
|
+
}
|
|
213
224
|
if (incomplete.length > 0) {
|
|
214
225
|
log(`\n${incomplete.length} codemod(s) did not complete: ${incomplete.join(", ")}`);
|
|
215
226
|
}
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { parseArguments } from "./cli/args.js";
|
|
4
4
|
import { createChoose } from "./cli/choose.js";
|
|
5
|
-
import { runSingleCodemod } from "./cli/codemod.js";
|
|
5
|
+
import { displaySourcePath, runSingleCodemod } from "./cli/codemod.js";
|
|
6
|
+
import { resolveInvoke } from "./install.js";
|
|
6
7
|
import { defaultListDeps, runList } from "./cli/list.js";
|
|
7
8
|
import { defaultUpgradeDeps, runUpgrade } from "./cli/upgrade.js";
|
|
8
9
|
const usage = `flow-codemods — migrate a codebase across Flow versions
|
|
@@ -65,6 +66,16 @@ const main = async () => {
|
|
|
65
66
|
// (or none at all), and beyond ~100 columns long prose gets harder to
|
|
66
67
|
// read rather than easier.
|
|
67
68
|
width: Math.min(Math.max(process.stdout.columns ?? 80, 60), 100),
|
|
69
|
+
// The per-entry command `list` prints has to name the path this project
|
|
70
|
+
// actually uses; hardcoding `src` handed readers a command that fails on
|
|
71
|
+
// any other layout. `--path` wins, otherwise the same `src`-or-cwd
|
|
72
|
+
// choice a real run would make.
|
|
73
|
+
path: displaySourcePath(parsed.path, process.cwd()),
|
|
74
|
+
// `npx` is wrong for a pnpm, Yarn Berry or Bun project. Detecting the
|
|
75
|
+
// manager here costs the bare `list` its "reads no manifest" property —
|
|
76
|
+
// a deliberate trade: a command the reader can actually paste beats the
|
|
77
|
+
// purity of that claim. It still hits no network.
|
|
78
|
+
invoke: await resolveInvoke(process.cwd()),
|
|
68
79
|
});
|
|
69
80
|
case "codemod":
|
|
70
81
|
return await runSingleCodemod(parsed, {
|
|
@@ -96,6 +107,10 @@ const main = async () => {
|
|
|
96
107
|
return runUpgrade(parsed, {
|
|
97
108
|
...defaultUpgradeDeps(process.cwd()),
|
|
98
109
|
choose,
|
|
110
|
+
// Same rule as `list`: colour only for a person at a real terminal,
|
|
111
|
+
// off under `NO_COLOR` or a pipe so the output stays greppable.
|
|
112
|
+
color: process.stdout.isTTY === true && process.env.NO_COLOR === undefined,
|
|
113
|
+
width: Math.min(Math.max(process.stdout.columns ?? 80, 60), 100),
|
|
99
114
|
});
|
|
100
115
|
}
|
|
101
116
|
default:
|
package/dist/install.d.ts
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Agent, DetectResult } from "package-manager-detector";
|
|
2
2
|
/**
|
|
3
|
-
* The package manager
|
|
3
|
+
* The project's package manager, as `upgrade` needs to know it.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* `agent` is what resolves a command (`yarn` and `yarn@berry` want different
|
|
6
|
+
* ones); `version` is the pin from `packageManager`, when there is one, and is
|
|
7
|
+
* the string `"berry"` rather than a number for Yarn 2+ — see `pinnedRange`.
|
|
8
8
|
*/
|
|
9
|
-
export
|
|
10
|
-
|
|
9
|
+
export type PackageManager = Pick<DetectResult, "name" | "agent" | "version">;
|
|
10
|
+
/**
|
|
11
|
+
* The package manager for the project at `cwd`.
|
|
12
|
+
*
|
|
13
|
+
* Detection walks **up** from `cwd`, which is the whole point: the previous
|
|
14
|
+
* implementation looked for lockfiles in `cwd` only, and a workspace package
|
|
15
|
+
* has none of its own. In a Yarn or pnpm monorepo it therefore fell through to
|
|
16
|
+
* the npm default and ran `npm install`, which on a workspace manifest dies
|
|
17
|
+
* with `EUNSUPPORTEDPROTOCOL Unsupported URL Type "workspace:"` — after
|
|
18
|
+
* `upgrade` had already rewritten `package.json`. It also ignored
|
|
19
|
+
* `packageManager` entirely.
|
|
20
|
+
*
|
|
21
|
+
* Per directory the strategies are applied in order, so a lockfile in `cwd`
|
|
22
|
+
* still beats a `packageManager` pin in a parent — and when the lockfile
|
|
23
|
+
* strategy hits, the library reads that same directory's `package.json` and
|
|
24
|
+
* lets its pin win. That is the precedence we want, and it is the library's,
|
|
25
|
+
* not ours.
|
|
26
|
+
*
|
|
27
|
+
* Npm on `null`, preserving the old fallback: it is the one manager always
|
|
28
|
+
* present next to Node, so a project with no signal gets an install rather than
|
|
29
|
+
* an error.
|
|
30
|
+
*/
|
|
31
|
+
export declare const detectPackageManagerIn: (cwd: string) => Promise<PackageManager>;
|
|
11
32
|
export interface InstallCommand {
|
|
12
33
|
command: string;
|
|
13
34
|
args: string[];
|
|
@@ -15,7 +36,7 @@ export interface InstallCommand {
|
|
|
15
36
|
env: Record<string, string>;
|
|
16
37
|
}
|
|
17
38
|
/**
|
|
18
|
-
* How to install with
|
|
39
|
+
* How to install with `agent`, so that the install actually updates the
|
|
19
40
|
* lockfile.
|
|
20
41
|
*
|
|
21
42
|
* `upgrade` has just rewritten `package.json`, so the lockfile is deliberately
|
|
@@ -23,15 +44,93 @@ export interface InstallCommand {
|
|
|
23
44
|
* themselves when they detect CI, where an install then _fails_ instead of
|
|
24
45
|
* updating — and CI is exactly where this runs unattended. pnpm takes a flag;
|
|
25
46
|
* Yarn's flag differs between Classic and Berry, so it gets the environment
|
|
26
|
-
* variable instead, which Classic ignores harmlessly.
|
|
47
|
+
* variable instead, which Classic ignores harmlessly. Keeping the env var also
|
|
48
|
+
* keeps this one code path rather than branching on a Berry detection being
|
|
49
|
+
* right.
|
|
50
|
+
*
|
|
51
|
+
* The command itself comes from the library. Its `"install"` is the plain
|
|
52
|
+
* install for every agent — the frozen variants are a separate command there —
|
|
53
|
+
* so these quirks stay ours.
|
|
54
|
+
*/
|
|
55
|
+
export declare const installCommand: (agent: Agent) => InstallCommand;
|
|
56
|
+
/**
|
|
57
|
+
* The pin as a semver range, or `undefined` when there is nothing to check.
|
|
58
|
+
*
|
|
59
|
+
* Two values have to be filtered out. `"berry"` is what the library reports for
|
|
60
|
+
* Yarn 2+ — a specifier, not a version — and for an unparseable
|
|
61
|
+
* `packageManager` it hands back the raw string. Neither is a range, and
|
|
62
|
+
* `satisfies` would throw on both.
|
|
63
|
+
*
|
|
64
|
+
* A full pin like `8.15.0` is a valid range meaning exactly that version, so
|
|
65
|
+
* one comparison covers both `pnpm@8` and `pnpm@8.15.0`. That is also why this
|
|
66
|
+
* is `satisfies` and not a string compare: the library reports the
|
|
67
|
+
* `\d+(\.\d+){0,2}` match out of the field, so `packageManager: "pnpm@8"`
|
|
68
|
+
* yields the pin `"8"` while the binary reports `8.15.0` — a string compare
|
|
69
|
+
* would send a perfectly fine setup through corepack.
|
|
70
|
+
*/
|
|
71
|
+
export declare const pinnedRange: (version: string | undefined) => string | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Runs a command and returns its trimmed stdout, or `undefined` if it fails.
|
|
74
|
+
*
|
|
75
|
+
* Injected so the pin logic is testable without shelling out — the same reason
|
|
76
|
+
* `InstallRunner` exists. It is not a CLI flag: the seam belongs in the code.
|
|
77
|
+
*/
|
|
78
|
+
export type Probe = (command: string, args: string[]) => string | undefined;
|
|
79
|
+
export declare const runProbe: Probe;
|
|
80
|
+
export interface InstallPlan {
|
|
81
|
+
command: string;
|
|
82
|
+
args: string[];
|
|
83
|
+
env: Record<string, string>;
|
|
84
|
+
/** How this reads in the log — agent, pin, and the command actually run. */
|
|
85
|
+
description: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* What to run for `manager`, honouring a `packageManager` pin.
|
|
89
|
+
*
|
|
90
|
+
* The decision tree, and why it checks before reaching for corepack:
|
|
91
|
+
*
|
|
92
|
+
* | Situation | Behaviour |
|
|
93
|
+
* | ------------------------------------------------- | ------------------------- |
|
|
94
|
+
* | No pin, or the `"berry"` specifier | run directly |
|
|
95
|
+
* | Pin, binary present, version satisfies it | run directly |
|
|
96
|
+
* | Pin, mismatch or binary missing, corepack present | run under corepack |
|
|
97
|
+
* | Pin, mismatch or missing, no corepack | throw, before any install |
|
|
98
|
+
*
|
|
99
|
+
* Corepack ignores `PATH` and downloads into its own cache, so "always
|
|
100
|
+
* corepack" would send every project that has a `packageManager` field — most
|
|
101
|
+
* of them — through a download, offline CI included. pnpm 9.7+ additionally
|
|
102
|
+
* installs its own pinned version, so for pnpm the direct path is usually
|
|
103
|
+
* already right.
|
|
104
|
+
*
|
|
105
|
+
* `COREPACK_ENABLE_DOWNLOAD_PROMPT=0` is required rather than tidy: corepack
|
|
106
|
+
* prompts interactively before a first download, and `upgrade` runs unattended
|
|
107
|
+
* (`-y` is implied with no TTY). Only one `install` is spawned under corepack —
|
|
108
|
+
* no nested per-task spawns — so the corepack footgun in the repo's root
|
|
109
|
+
* `AGENTS.md` does not apply.
|
|
27
110
|
*/
|
|
28
|
-
export declare const
|
|
111
|
+
export declare const planInstall: (manager: PackageManager, probe?: Probe) => InstallPlan;
|
|
29
112
|
/**
|
|
30
113
|
* Runs the install.
|
|
31
114
|
*
|
|
32
115
|
* A named type rather than a bare call so tests can substitute it. There is no
|
|
33
116
|
* `--no-install` flag: the seam belongs in the code, not in the CLI surface.
|
|
117
|
+
*
|
|
118
|
+
* Returns the plan's description so the caller can log what actually ran — a
|
|
119
|
+
* wrong detection then shows up in the output instead of silently installing
|
|
120
|
+
* with the wrong manager.
|
|
34
121
|
*/
|
|
35
|
-
export type InstallRunner = (manager: PackageManager, cwd: string) =>
|
|
122
|
+
export type InstallRunner = (manager: PackageManager, cwd: string) => string;
|
|
36
123
|
export declare const runInstall: InstallRunner;
|
|
124
|
+
/**
|
|
125
|
+
* How to tell a reader to invoke this package, for the manager they use.
|
|
126
|
+
*
|
|
127
|
+
* `npx …` for npm and Yarn Classic (which has no `dlx`), `pnpm dlx …`, `yarn
|
|
128
|
+
* dlx …`, `bun x …`. The package name is passed as an argument rather than
|
|
129
|
+
* baked in, so the library decides the whole prefix.
|
|
130
|
+
*
|
|
131
|
+
* Falls back to the `npx` form when the agent has no `execute` command: a
|
|
132
|
+
* printed hint is worth more than a gap, and npx is the one form every reader
|
|
133
|
+
* can run.
|
|
134
|
+
*/
|
|
135
|
+
export declare const resolveInvoke: (cwd: string, pkg?: string) => Promise<string>;
|
|
37
136
|
//# sourceMappingURL=install.d.ts.map
|
package/dist/install.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAEpE;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;AAiB9E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,sBAAsB,QAC5B,MAAM,KACV,OAAO,CAAC,cAAc,CACyC,CAAC;AAEnE,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,cAAc,UAAW,KAAK,KAAG,cAiB7C,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,WAAW,YAAa,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SAGpD,CAAC;AAEhB;;;;;GAKG;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,MAAM,GAAG,SAAS,CAAC;AAE5E,eAAO,MAAM,QAAQ,EAAE,KAUtB,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,WAAW,YACb,cAAc,UAChB,KAAK,KACX,WAuCF,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;AAE7E,eAAO,MAAM,UAAU,EAAE,aAYxB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,QACnB,MAAM,mBAEV,OAAO,CAAC,MAAM,CAMhB,CAAC"}
|
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
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.generated.d.ts","sourceRoot":"","sources":["../src/migrations.generated.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,sEAAsE;AACtE,eAAO,MAAM,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"migrations.generated.d.ts","sourceRoot":"","sources":["../src/migrations.generated.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,sEAAsE;AACtE,eAAO,MAAM,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,EAwQpD,CAAC"}
|
|
@@ -3,6 +3,15 @@
|
|
|
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: "option-value-inferred-from-mixed-children",
|
|
8
|
+
since: "1.1.12",
|
|
9
|
+
title: "Option: value is inferred from mixed children",
|
|
10
|
+
kind: "migration",
|
|
11
|
+
action: "none",
|
|
12
|
+
remotePackage: true,
|
|
13
|
+
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.",
|
|
14
|
+
},
|
|
6
15
|
{
|
|
7
16
|
id: "use-design-tokens-build-metadata-removed",
|
|
8
17
|
since: "1.0.16",
|
|
@@ -75,6 +84,15 @@ export const migrations = [
|
|
|
75
84
|
remotePackage: false,
|
|
76
85
|
apply: "Replace the import path `@mittwald/flow-react-components/password-tools` with `@mittwald/flow-react-components/mittwald-password-tools-js`.",
|
|
77
86
|
},
|
|
87
|
+
{
|
|
88
|
+
id: "tabs-navigation-usage-to-tab-navigation",
|
|
89
|
+
since: "0.2.0-alpha.977",
|
|
90
|
+
title: "Tabs restyled; navigation usage moves to TabNavigation",
|
|
91
|
+
kind: "migration",
|
|
92
|
+
action: "manual",
|
|
93
|
+
remotePackage: true,
|
|
94
|
+
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.",
|
|
95
|
+
},
|
|
78
96
|
{
|
|
79
97
|
id: "table-column-width-props",
|
|
80
98
|
since: "0.2.0-alpha.956",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mittwald/flow-codemods",
|
|
3
|
-
"version": "1.2.0-next.
|
|
3
|
+
"version": "1.2.0-next.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Codemods and an upgrade CLI for consumers of Flow, mittwald's design system",
|
|
6
6
|
"homepage": "https://flow.mittwald.de",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@inquirer/prompts": "^7.9.0",
|
|
24
24
|
"jscodeshift": "^17.3.0",
|
|
25
|
+
"package-manager-detector": "^1.8.0",
|
|
25
26
|
"picocolors": "^1.1.1",
|
|
26
27
|
"registry-url": "^7.2.0",
|
|
27
28
|
"semver": "^7.8.5"
|
|
@@ -36,5 +37,5 @@
|
|
|
36
37
|
"vitest": "^4.1.11",
|
|
37
38
|
"yaml": "^2.8.1"
|
|
38
39
|
},
|
|
39
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "eaef842a55bfc5311e8ce05a2c0cbd60cb28c8e9"
|
|
40
41
|
}
|