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

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 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 with `npx` — there is no reason to install it
5
- as a dependency.
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 `@mittwald/flow-*` dependency in `package.json` to a resolved
16
- target, installs, then runs the codemod of every migration up to that target.
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. Offline: reads no manifest, hits
69
- no network.
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
@@ -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"}
@@ -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);
@@ -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
  /**
@@ -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;AAoO1E;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,mDAOpB,eAAe,KAAG,MA0DpB,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;CAChB;AAED,eAAO,MAAM,eAAe,QAAS,MAAM,KAAG,QAG5C,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,OAAO,WACV,aAAa,QACf,QAAQ,KACb,OAAO,CAAC,MAAM,CA+BhB,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(`npx @mittwald/flow-codemods@latest ${entry.id} src`)}`);
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
  };
@@ -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,EAGL,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;CAChC;AAED,eAAO,MAAM,kBAAkB,QAAS,MAAM,KAAG,WAS/C,CAAC;AA2BH;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,WACb,aAAa,QACf,WAAW,KAChB,OAAO,CAAC,MAAM,CAwMhB,CAAC"}
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;CAChC;AAED,eAAO,MAAM,kBAAkB,QAAS,MAAM,KAAG,WAS/C,CAAC;AA2BH;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,WACb,aAAa,QACf,WAAW,KAChB,OAAO,CAAC,MAAM,CAiNhB,CAAC"}
@@ -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
- deps.install(manager, cwd);
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\nEither 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}`);
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
  }
@@ -204,6 +207,10 @@ export const runUpgrade = async (parsed, deps) => {
204
207
  entries: byHand,
205
208
  range: { from: current, to: target },
206
209
  json: false,
210
+ // The same path this run used, so a command copied out of the by-hand
211
+ // block works instead of falling back to a hardcoded `src`.
212
+ path: displaySourcePath(parsed.path, cwd),
213
+ invoke: await resolveInvoke(cwd),
207
214
  frame: false,
208
215
  }));
209
216
  }
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, {
package/dist/install.d.ts CHANGED
@@ -1,13 +1,34 @@
1
- export type PackageManager = "pnpm" | "npm" | "yarn" | "bun";
1
+ import type { Agent, DetectResult } from "package-manager-detector";
2
2
  /**
3
- * The package manager a project uses, from the lockfiles present.
3
+ * The project's package manager, as `upgrade` needs to know it.
4
4
  *
5
- * Npm is the fallback: it is the one manager that is always available next to
6
- * Node, so a project with no lockfile still gets an install rather than an
7
- * error.
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 declare const detectPackageManager: (present: string[]) => PackageManager;
10
- export declare const detectPackageManagerIn: (cwd: string) => PackageManager;
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 a given manager, so that the install actually updates the
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 installCommand: (manager: PackageManager) => InstallCommand;
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) => void;
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
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC;AAe7D;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,YAAa,MAAM,EAAE,KAAG,cAOxD,CAAC;AAEF,eAAO,MAAM,sBAAsB,QAAS,MAAM,KAAG,cAKlD,CAAC;AAEJ,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;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,YAAa,cAAc,KAAG,cAkBxD,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;AAE3E,eAAO,MAAM,UAAU,EAAE,aAOxB,CAAC"}
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 { existsSync } from "node:fs";
3
- import { join } from "node:path";
4
- /** Lockfile to manager, in precedence order. */
5
- const lockfiles = [
6
- ["pnpm-lock.yaml", "pnpm"],
7
- // Both Bun lockfile names: `bun.lock` (text) is the default only from Bun
8
- // 1.2; before that Bun writes the binary `bun.lockb`, which plenty of
9
- // projects still have. Missing it would fall through to the npm default and
10
- // run `npm install` on a Bun project, leaving a stray package-lock.json.
11
- ["bun.lock", "bun"],
12
- ["bun.lockb", "bun"],
13
- ["yarn.lock", "yarn"],
14
- ["package-lock.json", "npm"],
2
+ import { satisfies, validRange } from "semver";
3
+ import { resolveCommand } from "package-manager-detector/commands";
4
+ import { INSTALL_PAGE } from "package-manager-detector/constants";
5
+ import { detect } from "package-manager-detector/detect";
6
+ /** Every strategy the library offers, in the order it applies them. */
7
+ const strategies = [
8
+ "lockfile",
9
+ "packageManager-field",
10
+ "devEngines-field",
11
+ // Not in the library's default set. `upgrade` always runs on an installed
12
+ // project — it reads `node_modules` to learn the current Flow version — so the
13
+ // marker a manager leaves in there is a real signal, and the last one before
14
+ // the npm fallback.
15
+ "install-metadata",
15
16
  ];
17
+ /** What a project with no signal at all gets. */
18
+ const fallback = { name: "npm", agent: "npm" };
16
19
  /**
17
- * The package manager a project uses, from the lockfiles present.
20
+ * The package manager for the project at `cwd`.
18
21
  *
19
- * Npm is the fallback: it is the one manager that is always available next to
20
- * Node, so a project with no lockfile still gets an install rather than an
21
- * error.
22
+ * Detection walks **up** from `cwd`, which is the whole point: the previous
23
+ * implementation looked for lockfiles in `cwd` only, and a workspace package
24
+ * has none of its own. In a Yarn or pnpm monorepo it therefore fell through to
25
+ * the npm default and ran `npm install`, which on a workspace manifest dies
26
+ * with `EUNSUPPORTEDPROTOCOL Unsupported URL Type "workspace:"` — after
27
+ * `upgrade` had already rewritten `package.json`. It also ignored
28
+ * `packageManager` entirely.
29
+ *
30
+ * Per directory the strategies are applied in order, so a lockfile in `cwd`
31
+ * still beats a `packageManager` pin in a parent — and when the lockfile
32
+ * strategy hits, the library reads that same directory's `package.json` and
33
+ * lets its pin win. That is the precedence we want, and it is the library's,
34
+ * not ours.
35
+ *
36
+ * Npm on `null`, preserving the old fallback: it is the one manager always
37
+ * present next to Node, so a project with no signal gets an install rather than
38
+ * an error.
22
39
  */
23
- export const detectPackageManager = (present) => {
24
- for (const [lockfile, manager] of lockfiles) {
25
- if (present.includes(lockfile)) {
26
- return manager;
27
- }
28
- }
29
- return "npm";
30
- };
31
- export const detectPackageManagerIn = (cwd) => detectPackageManager(lockfiles
32
- .map(([lockfile]) => lockfile)
33
- .filter((lockfile) => existsSync(join(cwd, lockfile))));
40
+ export const detectPackageManagerIn = async (cwd) => (await detect({ cwd, strategies: [...strategies] })) ?? fallback;
34
41
  /**
35
- * How to install with a given manager, so that the install actually updates the
42
+ * How to install with `agent`, so that the install actually updates the
36
43
  * lockfile.
37
44
  *
38
45
  * `upgrade` has just rewritten `package.json`, so the lockfile is deliberately
@@ -40,32 +47,138 @@ export const detectPackageManagerIn = (cwd) => detectPackageManager(lockfiles
40
47
  * themselves when they detect CI, where an install then _fails_ instead of
41
48
  * updating — and CI is exactly where this runs unattended. pnpm takes a flag;
42
49
  * Yarn's flag differs between Classic and Berry, so it gets the environment
43
- * variable instead, which Classic ignores harmlessly.
50
+ * variable instead, which Classic ignores harmlessly. Keeping the env var also
51
+ * keeps this one code path rather than branching on a Berry detection being
52
+ * right.
53
+ *
54
+ * The command itself comes from the library. Its `"install"` is the plain
55
+ * install for every agent — the frozen variants are a separate command there —
56
+ * so these quirks stay ours.
57
+ */
58
+ export const installCommand = (agent) => {
59
+ const frozenOptOut = agent.startsWith("pnpm") ? ["--no-frozen-lockfile"] : [];
60
+ const resolved = resolveCommand(agent, "install", frozenOptOut);
61
+ if (resolved === null) {
62
+ throw new Error(`${agent} has no install command. Install the dependencies yourself, then re-run this command.`);
63
+ }
64
+ return {
65
+ command: resolved.command,
66
+ args: resolved.args,
67
+ env: agent.startsWith("yarn")
68
+ ? { YARN_ENABLE_IMMUTABLE_INSTALLS: "false" }
69
+ : {},
70
+ };
71
+ };
72
+ /**
73
+ * The pin as a semver range, or `undefined` when there is nothing to check.
74
+ *
75
+ * Two values have to be filtered out. `"berry"` is what the library reports for
76
+ * Yarn 2+ — a specifier, not a version — and for an unparseable
77
+ * `packageManager` it hands back the raw string. Neither is a range, and
78
+ * `satisfies` would throw on both.
79
+ *
80
+ * A full pin like `8.15.0` is a valid range meaning exactly that version, so
81
+ * one comparison covers both `pnpm@8` and `pnpm@8.15.0`. That is also why this
82
+ * is `satisfies` and not a string compare: the library reports the
83
+ * `\d+(\.\d+){0,2}` match out of the field, so `packageManager: "pnpm@8"`
84
+ * yields the pin `"8"` while the binary reports `8.15.0` — a string compare
85
+ * would send a perfectly fine setup through corepack.
44
86
  */
45
- export const installCommand = (manager) => {
46
- switch (manager) {
47
- case "pnpm":
48
- return {
49
- command: "pnpm",
50
- args: ["install", "--no-frozen-lockfile"],
51
- env: {},
52
- };
53
- case "yarn":
54
- return {
55
- command: "yarn",
56
- args: ["install"],
57
- env: { YARN_ENABLE_IMMUTABLE_INSTALLS: "false" },
58
- };
59
- case "npm":
60
- case "bun":
61
- return { command: manager, args: ["install"], env: {} };
87
+ export const pinnedRange = (version) => version !== undefined && version !== "berry" && validRange(version) !== null
88
+ ? version
89
+ : undefined;
90
+ export const runProbe = (command, args) => {
91
+ try {
92
+ return execFileSync(command, args, {
93
+ encoding: "utf8",
94
+ stdio: ["ignore", "pipe", "ignore"],
95
+ shell: process.platform === "win32",
96
+ }).trim();
97
+ }
98
+ catch {
99
+ return undefined;
62
100
  }
63
101
  };
102
+ /**
103
+ * What to run for `manager`, honouring a `packageManager` pin.
104
+ *
105
+ * The decision tree, and why it checks before reaching for corepack:
106
+ *
107
+ * | Situation | Behaviour |
108
+ * | ------------------------------------------------- | ------------------------- |
109
+ * | No pin, or the `"berry"` specifier | run directly |
110
+ * | Pin, binary present, version satisfies it | run directly |
111
+ * | Pin, mismatch or binary missing, corepack present | run under corepack |
112
+ * | Pin, mismatch or missing, no corepack | throw, before any install |
113
+ *
114
+ * Corepack ignores `PATH` and downloads into its own cache, so "always
115
+ * corepack" would send every project that has a `packageManager` field — most
116
+ * of them — through a download, offline CI included. pnpm 9.7+ additionally
117
+ * installs its own pinned version, so for pnpm the direct path is usually
118
+ * already right.
119
+ *
120
+ * `COREPACK_ENABLE_DOWNLOAD_PROMPT=0` is required rather than tidy: corepack
121
+ * prompts interactively before a first download, and `upgrade` runs unattended
122
+ * (`-y` is implied with no TTY). Only one `install` is spawned under corepack —
123
+ * no nested per-task spawns — so the corepack footgun in the repo's root
124
+ * `AGENTS.md` does not apply.
125
+ */
126
+ export const planInstall = (manager, probe = runProbe) => {
127
+ const { command, args, env } = installCommand(manager.agent);
128
+ const line = [command, ...args].join(" ");
129
+ const pin = pinnedRange(manager.version);
130
+ if (pin === undefined) {
131
+ return { command, args, env, description: `${manager.agent} — ${line}` };
132
+ }
133
+ const found = probe(command, ["--version"]);
134
+ if (found !== undefined && satisfies(found, pin)) {
135
+ return {
136
+ command,
137
+ args,
138
+ env,
139
+ description: `${manager.agent} ${found} — ${line}`,
140
+ };
141
+ }
142
+ if (probe("corepack", ["--version"]) === undefined) {
143
+ throw new Error(`This project pins ${manager.name}@${pin} in "packageManager", but ${found === undefined
144
+ ? `${command} is not on PATH`
145
+ : `the ${command} on PATH is ${found}`}, and corepack is not available to bridge the gap.\n\nInstall ${manager.name}@${pin} (${INSTALL_PAGE[manager.agent]}) or enable corepack, then re-run this command. Nothing has been installed.`);
146
+ }
147
+ return {
148
+ command: "corepack",
149
+ args: [command, ...args],
150
+ env: { ...env, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" },
151
+ description: `${manager.agent} ${pin} via corepack — corepack ${line}`,
152
+ };
153
+ };
64
154
  export const runInstall = (manager, cwd) => {
65
- const { command, args, env } = installCommand(manager);
66
- execFileSync(command, args, {
155
+ const plan = planInstall(manager);
156
+ execFileSync(plan.command, plan.args, {
67
157
  cwd,
68
158
  stdio: "inherit",
69
- env: { ...process.env, ...env },
159
+ env: { ...process.env, ...plan.env },
160
+ // Node has refused to spawn `.cmd`/`.bat` without a shell since 2024, which
161
+ // is why every non-npm manager died with ENOENT on Windows. Every argument
162
+ // here is our own literal or the library's, so there is no quoting hazard.
163
+ shell: process.platform === "win32",
70
164
  });
165
+ return plan.description;
166
+ };
167
+ /**
168
+ * How to tell a reader to invoke this package, for the manager they use.
169
+ *
170
+ * `npx …` for npm and Yarn Classic (which has no `dlx`), `pnpm dlx …`, `yarn
171
+ * dlx …`, `bun x …`. The package name is passed as an argument rather than
172
+ * baked in, so the library decides the whole prefix.
173
+ *
174
+ * Falls back to the `npx` form when the agent has no `execute` command: a
175
+ * printed hint is worth more than a gap, and npx is the one form every reader
176
+ * can run.
177
+ */
178
+ export const resolveInvoke = async (cwd, pkg = "@mittwald/flow-codemods@latest") => {
179
+ const { agent } = await detectPackageManagerIn(cwd);
180
+ const resolved = resolveCommand(agent, "execute", [pkg]);
181
+ return resolved === null
182
+ ? `npx ${pkg}`
183
+ : [resolved.command, ...resolved.args].join(" ");
71
184
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/flow-codemods",
3
- "version": "1.2.0-next.5",
3
+ "version": "1.2.0-next.7",
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": "98920a921dbc3a75c53ba37985ecb9108bf1cff4"
40
+ "gitHead": "1b3973f89e6bdaf1aeb76efc442adb6344c96d4f"
40
41
  }