@mittwald/flow-codemods 1.3.4 → 1.4.0-next.0
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 +55 -2
- package/dist/cli/list.d.ts +11 -2
- package/dist/cli/list.js +17 -58
- package/dist/cli/peers.d.ts +22 -0
- package/dist/cli/peers.js +73 -0
- package/dist/cli/text.d.ts +27 -0
- package/dist/cli/text.js +62 -0
- package/dist/cli/upgrade.js +24 -1
- package/dist/migrations.generated.js +10 -1
- package/dist/resolve/peers.d.ts +64 -0
- package/dist/resolve/peers.js +72 -0
- package/dist/resolve/range.d.ts +11 -0
- package/dist/resolve/range.js +10 -1
- package/dist/resolve/registry.d.ts +26 -1
- package/dist/resolve/registry.js +15 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -60,6 +60,56 @@ and where to get the right one — rather than installing with the wrong manager
|
|
|
60
60
|
The log line names the agent, the pin and the command it actually ran, so a
|
|
61
61
|
wrong detection is visible instead of silent.
|
|
62
62
|
|
|
63
|
+
#### Peer requirements
|
|
64
|
+
|
|
65
|
+
Before the install, `upgrade` prints what the Flow packages you declare peer on
|
|
66
|
+
at the target — react, react-dom, react-hook-form, `@internationalized/date`,
|
|
67
|
+
next, i18next — grouped by range and naming every package that asks for it:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Peer requirements at 1.2.4 — what the Flow packages you declare ask of your
|
|
71
|
+
project:
|
|
72
|
+
|
|
73
|
+
next ^16.2.3 — optional
|
|
74
|
+
@mittwald/flow-react-components
|
|
75
|
+
react ^19.2.0 — required
|
|
76
|
+
required by @mittwald/flow-react-components
|
|
77
|
+
optional for @mittwald/ext-bridge
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Each peer carries its status, because that is the question you have: **must I
|
|
81
|
+
install this at all?** One package requiring it settles that, however many
|
|
82
|
+
others call it optional — so `react` above reads `required`, and the two sides
|
|
83
|
+
are broken out underneath. Where every package agrees, the status alone carries
|
|
84
|
+
it.
|
|
85
|
+
|
|
86
|
+
**Optionality is per package**, and it comes from `peerDependenciesMeta`: the
|
|
87
|
+
same `react ^19.2.0` is a hard requirement of `flow-react-components` and an
|
|
88
|
+
optional peer of `ext-bridge`. An optional peer need not be installed at all —
|
|
89
|
+
it only has to match the range if it is. Most of what Flow peers on is optional,
|
|
90
|
+
so the ranges alone would tell a Vite project it needs Next 16.
|
|
91
|
+
|
|
92
|
+
Your package manager reports conflicts too, but only the ones it decides to name
|
|
93
|
+
— Yarn's "`@mittwald/ext-bridge` and other dependencies" never enumerates the
|
|
94
|
+
others. This list names every one of them, and it is printed **before** the
|
|
95
|
+
install, so a failed install still leaves it on screen. `list [revision]` prints
|
|
96
|
+
the same thing without touching anything.
|
|
97
|
+
|
|
98
|
+
The list covers the Flow packages your `package.json` **declares**. A project
|
|
99
|
+
depending only on `flow-react-components` never sees what `flow-remote-core`
|
|
100
|
+
peers on, because it never asks for it.
|
|
101
|
+
|
|
102
|
+
These are the ranges Flow declares, **not** a check of what you have installed.
|
|
103
|
+
Nothing here reads your `node_modules`; whether your project satisfies them is
|
|
104
|
+
the package manager's call during the install.
|
|
105
|
+
|
|
106
|
+
A Flow-internal peer the target does not satisfy gets its own warning. Flow
|
|
107
|
+
publishes those as exact pins and releases only the packages that changed, so
|
|
108
|
+
the pin usually names a neighbouring release rather than the version you are
|
|
109
|
+
installing — your package manager will report the conflict. Rarely the pinned
|
|
110
|
+
version was never published at all; see the publish-hole caveat in the
|
|
111
|
+
repository's release docs.
|
|
112
|
+
|
|
63
113
|
After installing, `upgrade` runs the codemod of every migration whose `since` is
|
|
64
114
|
at or below the target and prints the ones with no codemod, for you to apply by
|
|
65
115
|
hand.
|
|
@@ -105,8 +155,11 @@ involve.
|
|
|
105
155
|
Options:
|
|
106
156
|
|
|
107
157
|
- `--json` — machine-readable output: an object with `range`
|
|
108
|
-
(`current`/`target`, or `null` for the offline whole-catalogue form)
|
|
109
|
-
`migrations`, each entry carrying `catchUp`
|
|
158
|
+
(`current`/`target`, or `null` for the offline whole-catalogue form),
|
|
159
|
+
`migrations`, each entry carrying `catchUp`, and `peers` — `external` and
|
|
160
|
+
`flowPins`, each a list of `{ peer, range, requiredBy, optionalFor }`, or
|
|
161
|
+
`null` for the offline whole-catalogue form, which has no target to read them
|
|
162
|
+
at
|
|
110
163
|
|
|
111
164
|
### `<id> [path]`
|
|
112
165
|
|
package/dist/cli/list.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CatalogEntry } from "../catalog/entries.js";
|
|
2
|
+
import type { PeerSummary } from "../resolve/peers.js";
|
|
2
3
|
import { type RangeDeps } from "../resolve/range.js";
|
|
3
4
|
import type { ParsedCommand } from "./args.js";
|
|
4
5
|
export interface RenderListInput {
|
|
@@ -13,6 +14,15 @@ export interface RenderListInput {
|
|
|
13
14
|
to: string;
|
|
14
15
|
};
|
|
15
16
|
json: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The peer ranges the Flow packages declare at `range.to`, from
|
|
19
|
+
* `resolveRange`.
|
|
20
|
+
*
|
|
21
|
+
* Only meaningful together with `range`, and omitted by `upgrade`, which
|
|
22
|
+
* prints its own peer section before the install rather than inside this list
|
|
23
|
+
* — see `runUpgrade`.
|
|
24
|
+
*/
|
|
25
|
+
peers?: PeerSummary;
|
|
16
26
|
/** Emit ANSI colour. Off by default so a test sees plain text. */
|
|
17
27
|
color?: boolean;
|
|
18
28
|
/** Terminal width to wrap prose to. */
|
|
@@ -47,7 +57,6 @@ export interface RenderListInput {
|
|
|
47
57
|
*/
|
|
48
58
|
frame?: boolean;
|
|
49
59
|
}
|
|
50
|
-
export declare const stripAnsi: (text: string) => string;
|
|
51
60
|
/**
|
|
52
61
|
* The invocation to print when nobody says otherwise.
|
|
53
62
|
*
|
|
@@ -67,7 +76,7 @@ export declare const defaultInvoke = "npx @mittwald/flow-codemods@latest";
|
|
|
67
76
|
* so the rendering is deterministic in a test — `painter` builds its palette
|
|
68
77
|
* from `color` alone, never from the terminal. `cli.ts` supplies them.
|
|
69
78
|
*/
|
|
70
|
-
export declare const renderList: ({ entries, range, json, color, width, path, invoke, frame, }: RenderListInput) => string;
|
|
79
|
+
export declare const renderList: ({ entries, range, json, peers, color, width, path, invoke, frame, }: RenderListInput) => string;
|
|
71
80
|
export interface ListDeps extends RangeDeps {
|
|
72
81
|
/**
|
|
73
82
|
* Raw output writer — matches `process.stdout.write`'s own contract: no
|
package/dist/cli/list.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import colors from "picocolors";
|
|
2
1
|
import { lte } from "semver";
|
|
3
2
|
import { allEntries } from "../catalog/entries.js";
|
|
4
3
|
import { selectEntries, sortBySince } from "../catalog/select.js";
|
|
5
4
|
import { defaultRangeDeps, resolveRange, } from "../resolve/range.js";
|
|
5
|
+
import { renderPeers } from "./peers.js";
|
|
6
|
+
import { painter, wrap } from "./text.js";
|
|
6
7
|
/**
|
|
7
8
|
* What the reader has to do, and the colour that says it at a glance.
|
|
8
9
|
*
|
|
@@ -21,39 +22,6 @@ const actions = {
|
|
|
21
22
|
tone: "blue",
|
|
22
23
|
},
|
|
23
24
|
};
|
|
24
|
-
/**
|
|
25
|
-
* ANSI escapes have no width; measuring must ignore them.
|
|
26
|
-
*
|
|
27
|
-
* Built from a char code rather than written as a literal, so no control
|
|
28
|
-
* character sits in the source — which is also what `no-control-regex` wants.
|
|
29
|
-
*/
|
|
30
|
-
const ansi = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
31
|
-
export const stripAnsi = (text) => text.replace(ansi, "");
|
|
32
|
-
const visibleWidth = (text) => stripAnsi(text).length;
|
|
33
|
-
/**
|
|
34
|
-
* Wraps to `width`, measuring visible width so already-coloured text still
|
|
35
|
-
* breaks in the right place.
|
|
36
|
-
*/
|
|
37
|
-
const wrap = (text, width) => {
|
|
38
|
-
const lines = [];
|
|
39
|
-
let line = "";
|
|
40
|
-
for (const word of text.split(/\s+/).filter((part) => part !== "")) {
|
|
41
|
-
if (line === "") {
|
|
42
|
-
line = word;
|
|
43
|
-
}
|
|
44
|
-
else if (visibleWidth(line) + 1 + visibleWidth(word) <= width) {
|
|
45
|
-
line = `${line} ${word}`;
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
lines.push(line);
|
|
49
|
-
line = word;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (line !== "") {
|
|
53
|
-
lines.push(line);
|
|
54
|
-
}
|
|
55
|
-
return lines.length > 0 ? lines : [""];
|
|
56
|
-
};
|
|
57
25
|
/**
|
|
58
26
|
* Renders `code` spans as colour instead of literal backticks.
|
|
59
27
|
*
|
|
@@ -62,27 +30,6 @@ const wrap = (text, width) => {
|
|
|
62
30
|
* scans for — a symbol name or a command.
|
|
63
31
|
*/
|
|
64
32
|
const inlineCode = (text, paint) => text.replace(/`([^`]+)`/g, (_, code) => paint.code(code));
|
|
65
|
-
/**
|
|
66
|
-
* Builds a palette from `color`, and only from `color`.
|
|
67
|
-
*
|
|
68
|
-
* `colors` (the module-level `picocolors` default export) auto-detects a TTY
|
|
69
|
-
* and disables itself under one — reading `colors.green` etc. directly would
|
|
70
|
-
* make the _environment_, not this argument, the real decider, silently
|
|
71
|
-
* disagreeing with `cli.ts`'s own TTY/`NO_COLOR`/`--json` check. `createColors`
|
|
72
|
-
* returns a palette with colour forced on or off, so `color` is the only input
|
|
73
|
-
* to how this renders.
|
|
74
|
-
*/
|
|
75
|
-
const painter = (color) => {
|
|
76
|
-
const palette = colors.createColors(color);
|
|
77
|
-
return {
|
|
78
|
-
bold: palette.bold,
|
|
79
|
-
dim: palette.dim,
|
|
80
|
-
code: palette.cyan,
|
|
81
|
-
green: palette.green,
|
|
82
|
-
yellow: palette.yellow,
|
|
83
|
-
blue: palette.blue,
|
|
84
|
-
};
|
|
85
|
-
};
|
|
86
33
|
/**
|
|
87
34
|
* The invocation to print when nobody says otherwise.
|
|
88
35
|
*
|
|
@@ -210,7 +157,7 @@ const renderSummary = (selected, color) => {
|
|
|
210
157
|
* so the rendering is deterministic in a test — `painter` builds its palette
|
|
211
158
|
* from `color` alone, never from the terminal. `cli.ts` supplies them.
|
|
212
159
|
*/
|
|
213
|
-
export const renderList = ({ entries, range, json, color = false, width = 80, path = "src", invoke = defaultInvoke, frame = true, }) => {
|
|
160
|
+
export const renderList = ({ entries, range, json, peers, color = false, width = 80, path = "src", invoke = defaultInvoke, frame = true, }) => {
|
|
214
161
|
// The two paths differ in their bounds, deliberately: unbounded, this is a
|
|
215
162
|
// plain catalogue browse — every entry, sorted, regardless of whether it
|
|
216
163
|
// would apply to any given range. Bounded, it answers "what does this
|
|
@@ -235,10 +182,21 @@ export const renderList = ({ entries, range, json, color = false, width = 80, pa
|
|
|
235
182
|
...entry,
|
|
236
183
|
catchUp: isCatchUp(entry, range?.from),
|
|
237
184
|
})),
|
|
185
|
+
// `null` rather than omitted, so a consumer can tell "no peers at this
|
|
186
|
+
// target" from "this form does not carry peers" — a bare `list` has no
|
|
187
|
+
// range to read them at.
|
|
188
|
+
peers: peers ?? null,
|
|
238
189
|
}, null, 2)}\n`;
|
|
239
190
|
}
|
|
191
|
+
// A property of the range, not of the entries, so it prints even when no
|
|
192
|
+
// migration falls in it: a consumer already sitting on `target` can still be
|
|
193
|
+
// missing a peer, and that is exactly the range that selects nothing.
|
|
194
|
+
const peerSection = peers === undefined || range === undefined
|
|
195
|
+
? ""
|
|
196
|
+
: renderPeers({ summary: peers, target: range.to, color, width });
|
|
240
197
|
if (selected.length === 0) {
|
|
241
|
-
|
|
198
|
+
const nothing = "Nothing to migrate in that range.\n";
|
|
199
|
+
return peerSection === "" ? nothing : `${nothing}\n${peerSection}`;
|
|
242
200
|
}
|
|
243
201
|
const body = selected
|
|
244
202
|
.map((entry) => renderEntry(entry, width, color, isCatchUp(entry, range?.from), path, invoke))
|
|
@@ -253,7 +211,7 @@ export const renderList = ({ entries, range, json, color = false, width = 80, pa
|
|
|
253
211
|
// gets left dangling above the first entry.
|
|
254
212
|
const context = renderContext(selected, range, color, width);
|
|
255
213
|
const summary = renderSummary(selected, color);
|
|
256
|
-
return `${context === "" ? "" : `${context}\n\n`}${body}\n\n${summary}\n`;
|
|
214
|
+
return `${context === "" ? "" : `${context}\n\n`}${peerSection === "" ? "" : `${peerSection}\n`}${body}\n\n${summary}\n`;
|
|
257
215
|
};
|
|
258
216
|
export const defaultListDeps = (cwd) => ({
|
|
259
217
|
...defaultRangeDeps(cwd),
|
|
@@ -295,6 +253,7 @@ export const runList = async (parsed, deps) => {
|
|
|
295
253
|
entries: allEntries,
|
|
296
254
|
range: { from: resolved.current, to: resolved.target },
|
|
297
255
|
json: parsed.json,
|
|
256
|
+
peers: resolved.peers,
|
|
298
257
|
color: deps.color,
|
|
299
258
|
width: deps.width,
|
|
300
259
|
path: deps.path,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PeerSummary } from "../resolve/peers.js";
|
|
2
|
+
export interface RenderPeersInput {
|
|
3
|
+
summary: PeerSummary;
|
|
4
|
+
/** The version the ranges were read at. */
|
|
5
|
+
target: string;
|
|
6
|
+
/** Emit ANSI colour. Off by default so a test sees plain text. */
|
|
7
|
+
color?: boolean;
|
|
8
|
+
/** Terminal width to wrap prose to. */
|
|
9
|
+
width?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The peer ranges the Flow packages declare at `target`.
|
|
13
|
+
*
|
|
14
|
+
* Returns `""` when there is nothing to say, so a caller can print it
|
|
15
|
+
* unconditionally.
|
|
16
|
+
*
|
|
17
|
+
* The closing line is not a disclaimer for its own sake: without it the list
|
|
18
|
+
* reads as a check that passed. It is not one — nothing here looks at what the
|
|
19
|
+
* project has installed (see `collectPeers`), and a reader who assumes
|
|
20
|
+
* otherwise would take silence for a green light.
|
|
21
|
+
*/
|
|
22
|
+
export declare const renderPeers: ({ summary, target, color, width, }: RenderPeersInput) => string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { hasPeers } from "../resolve/peers.js";
|
|
2
|
+
import { painter, wrap } from "./text.js";
|
|
3
|
+
const indent = " ";
|
|
4
|
+
/**
|
|
5
|
+
* One requirement: the peer and its range on top, who asks for it below.
|
|
6
|
+
*
|
|
7
|
+
* The askers go on their own wrapped line rather than trailing the range — at
|
|
8
|
+
* eight Flow packages the list is longer than the range it belongs to, and
|
|
9
|
+
* inline it pushes the next range off the scannable left edge.
|
|
10
|
+
*
|
|
11
|
+
* Both lists are labelled, even when only one is present. An unlabelled list
|
|
12
|
+
* with a marker appended somewhere would leave the reader guessing how far the
|
|
13
|
+
* marker reaches — and "does this project actually need react?" is the whole
|
|
14
|
+
* question this output exists to answer.
|
|
15
|
+
*/
|
|
16
|
+
const renderRequirement = ({ peer, range, requiredBy, optionalFor }, width, paint) => {
|
|
17
|
+
const gutter = indent.repeat(2);
|
|
18
|
+
const required = requiredBy.length > 0;
|
|
19
|
+
// The status sits on the peer line, in colour, because it answers the
|
|
20
|
+
// question the reader actually has — "must I install this at all?" — and one
|
|
21
|
+
// package requiring it settles that no matter how many others call it
|
|
22
|
+
// optional. Underneath everything is dim, so each entry has exactly one
|
|
23
|
+
// bright thing to land on when scanning.
|
|
24
|
+
const status = required ? paint.yellow("required") : paint.dim("optional");
|
|
25
|
+
// Only a disagreement earns the two labelled lines. Where every package
|
|
26
|
+
// treats the peer the same way, the labels would repeat what the status
|
|
27
|
+
// already said, once per entry, for the whole list.
|
|
28
|
+
const rows = required && optionalFor.length > 0
|
|
29
|
+
? [
|
|
30
|
+
["required by ", requiredBy],
|
|
31
|
+
["optional for ", optionalFor],
|
|
32
|
+
]
|
|
33
|
+
: [["", required ? requiredBy : optionalFor]];
|
|
34
|
+
const askers = rows
|
|
35
|
+
.flatMap(([label, names]) => wrap(`${label}${names.join(", ")}`, Math.max(width - gutter.length, 20)).map((line) => `${gutter}${paint.dim(line)}`))
|
|
36
|
+
.join("\n");
|
|
37
|
+
return `${indent}${paint.bold(peer)} ${paint.code(range)} — ${status}\n${askers}`;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The peer ranges the Flow packages declare at `target`.
|
|
41
|
+
*
|
|
42
|
+
* Returns `""` when there is nothing to say, so a caller can print it
|
|
43
|
+
* unconditionally.
|
|
44
|
+
*
|
|
45
|
+
* The closing line is not a disclaimer for its own sake: without it the list
|
|
46
|
+
* reads as a check that passed. It is not one — nothing here looks at what the
|
|
47
|
+
* project has installed (see `collectPeers`), and a reader who assumes
|
|
48
|
+
* otherwise would take silence for a green light.
|
|
49
|
+
*/
|
|
50
|
+
export const renderPeers = ({ summary, target, color = false, width = 80, }) => {
|
|
51
|
+
if (!hasPeers(summary)) {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
const paint = painter(color);
|
|
55
|
+
const sections = [];
|
|
56
|
+
if (summary.external.length > 0) {
|
|
57
|
+
sections.push([
|
|
58
|
+
...wrap(`Peer requirements at ${paint.bold(target)} — what the Flow packages you declare ask of your project:`, width),
|
|
59
|
+
"",
|
|
60
|
+
...summary.external.map((requirement) => renderRequirement(requirement, width, paint)),
|
|
61
|
+
"",
|
|
62
|
+
...wrap(paint.dim("Ranges as Flow declares them, not a check of what you have installed — your package manager decides that during the install. An optional peer need not be installed at all; it only has to match the range if it is."), width),
|
|
63
|
+
].join("\n"));
|
|
64
|
+
}
|
|
65
|
+
if (summary.flowPins.length > 0) {
|
|
66
|
+
sections.push([
|
|
67
|
+
...wrap(paint.yellow(`${summary.flowPins.length} Flow-internal peer pin(s) do not accept ${target}. Flow publishes these as exact pins and releases only the packages that changed, so a pin usually names a neighbouring release rather than the version you are installing — your package manager will report the conflict. Rarely the pinned version was never published at all (#2887).`), width),
|
|
68
|
+
"",
|
|
69
|
+
...summary.flowPins.map((requirement) => renderRequirement(requirement, width, paint)),
|
|
70
|
+
].join("\n"));
|
|
71
|
+
}
|
|
72
|
+
return `${sections.join("\n\n")}\n`;
|
|
73
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A palette key from `painter()` — see the `tone` field on `actions` in
|
|
3
|
+
* `list.ts`.
|
|
4
|
+
*/
|
|
5
|
+
export type Tone = "green" | "yellow" | "blue";
|
|
6
|
+
export declare const stripAnsi: (text: string) => string;
|
|
7
|
+
/**
|
|
8
|
+
* Wraps to `width`, measuring visible width so already-coloured text still
|
|
9
|
+
* breaks in the right place.
|
|
10
|
+
*/
|
|
11
|
+
export declare const wrap: (text: string, width: number) => string[];
|
|
12
|
+
export interface Painter extends Record<Tone, (text: string) => string> {
|
|
13
|
+
bold: (text: string) => string;
|
|
14
|
+
dim: (text: string) => string;
|
|
15
|
+
code: (text: string) => string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Builds a palette from `color`, and only from `color`.
|
|
19
|
+
*
|
|
20
|
+
* `colors` (the module-level `picocolors` default export) auto-detects a TTY
|
|
21
|
+
* and disables itself under one — reading `colors.green` etc. directly would
|
|
22
|
+
* make the _environment_, not this argument, the real decider, silently
|
|
23
|
+
* disagreeing with `cli.ts`'s own TTY/`NO_COLOR`/`--json` check. `createColors`
|
|
24
|
+
* returns a palette with colour forced on or off, so `color` is the only input
|
|
25
|
+
* to how this renders.
|
|
26
|
+
*/
|
|
27
|
+
export declare const painter: (color: boolean) => Painter;
|
package/dist/cli/text.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal text helpers shared by the renderers in this directory.
|
|
3
|
+
*
|
|
4
|
+
* They live here rather than in `list.ts` because `peers.ts` needs them too,
|
|
5
|
+
* and `list.ts` imports `peers.ts` to render its own peer section — keeping
|
|
6
|
+
* them there would make that pair circular.
|
|
7
|
+
*/
|
|
8
|
+
import colors from "picocolors";
|
|
9
|
+
/**
|
|
10
|
+
* ANSI escapes have no width; measuring must ignore them.
|
|
11
|
+
*
|
|
12
|
+
* Built from a char code rather than written as a literal, so no control
|
|
13
|
+
* character sits in the source — which is also what `no-control-regex` wants.
|
|
14
|
+
*/
|
|
15
|
+
const ansi = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
16
|
+
export const stripAnsi = (text) => text.replace(ansi, "");
|
|
17
|
+
const visibleWidth = (text) => stripAnsi(text).length;
|
|
18
|
+
/**
|
|
19
|
+
* Wraps to `width`, measuring visible width so already-coloured text still
|
|
20
|
+
* breaks in the right place.
|
|
21
|
+
*/
|
|
22
|
+
export const wrap = (text, width) => {
|
|
23
|
+
const lines = [];
|
|
24
|
+
let line = "";
|
|
25
|
+
for (const word of text.split(/\s+/).filter((part) => part !== "")) {
|
|
26
|
+
if (line === "") {
|
|
27
|
+
line = word;
|
|
28
|
+
}
|
|
29
|
+
else if (visibleWidth(line) + 1 + visibleWidth(word) <= width) {
|
|
30
|
+
line = `${line} ${word}`;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
lines.push(line);
|
|
34
|
+
line = word;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (line !== "") {
|
|
38
|
+
lines.push(line);
|
|
39
|
+
}
|
|
40
|
+
return lines.length > 0 ? lines : [""];
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Builds a palette from `color`, and only from `color`.
|
|
44
|
+
*
|
|
45
|
+
* `colors` (the module-level `picocolors` default export) auto-detects a TTY
|
|
46
|
+
* and disables itself under one — reading `colors.green` etc. directly would
|
|
47
|
+
* make the _environment_, not this argument, the real decider, silently
|
|
48
|
+
* disagreeing with `cli.ts`'s own TTY/`NO_COLOR`/`--json` check. `createColors`
|
|
49
|
+
* returns a palette with colour forced on or off, so `color` is the only input
|
|
50
|
+
* to how this renders.
|
|
51
|
+
*/
|
|
52
|
+
export const painter = (color) => {
|
|
53
|
+
const palette = colors.createColors(color);
|
|
54
|
+
return {
|
|
55
|
+
bold: palette.bold,
|
|
56
|
+
dim: palette.dim,
|
|
57
|
+
code: palette.cyan,
|
|
58
|
+
green: palette.green,
|
|
59
|
+
yellow: palette.yellow,
|
|
60
|
+
blue: palette.blue,
|
|
61
|
+
};
|
|
62
|
+
};
|
package/dist/cli/upgrade.js
CHANGED
|
@@ -12,6 +12,7 @@ import { readInstalledVersion, resolveRange } from "../resolve/range.js";
|
|
|
12
12
|
import { runCodemod } from "../run/jscodeshift.js";
|
|
13
13
|
import { displaySourcePath, resolveSourcePath } from "./codemod.js";
|
|
14
14
|
import { renderList } from "./list.js";
|
|
15
|
+
import { renderPeers } from "./peers.js";
|
|
15
16
|
export const defaultUpgradeDeps = (cwd) => ({
|
|
16
17
|
cwd,
|
|
17
18
|
fetchVersions,
|
|
@@ -86,7 +87,7 @@ export const runUpgrade = async (parsed, deps) => {
|
|
|
86
87
|
log(range.reason);
|
|
87
88
|
return 1;
|
|
88
89
|
}
|
|
89
|
-
const { manifestPath, manifestRaw, manifest, dependencies, current, target } = range;
|
|
90
|
+
const { manifestPath, manifestRaw, manifest, dependencies, current, target, peers, } = range;
|
|
90
91
|
// A stale dist-tag or an exact version at or below `current` resolves
|
|
91
92
|
// without complaint — `resolveRange` deliberately does not judge that
|
|
92
93
|
// (`list` treats the same fact as a legitimate answer). There is nothing to
|
|
@@ -105,15 +106,36 @@ export const runUpgrade = async (parsed, deps) => {
|
|
|
105
106
|
: ` ${dependency.name} ${dependency.range} — left as it is, ${dependency.field} states what your package supports, not what it installs`);
|
|
106
107
|
}
|
|
107
108
|
};
|
|
109
|
+
// Printed before the install in every branch, never after it: the install
|
|
110
|
+
// can fail (and returns early below), yet a peer range the new version needs
|
|
111
|
+
// is exactly what the reader wants in hand when it does. It is also the one
|
|
112
|
+
// part of this output that stays true when nothing is bumped at all — a
|
|
113
|
+
// project sitting on `target` can still be missing a peer.
|
|
114
|
+
const reportPeers = () => {
|
|
115
|
+
const rendered = renderPeers({
|
|
116
|
+
summary: peers,
|
|
117
|
+
target,
|
|
118
|
+
color: deps.color,
|
|
119
|
+
width: deps.width,
|
|
120
|
+
});
|
|
121
|
+
if (rendered !== "") {
|
|
122
|
+
// Blank line on both sides: this sits between the dependency list and
|
|
123
|
+
// whatever the install prints next, and without the trailing one the
|
|
124
|
+
// package manager's first line reads as part of the summary.
|
|
125
|
+
log(`\n${rendered.trimEnd()}\n`);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
108
128
|
const bump = gt(target, current);
|
|
109
129
|
if (!bump) {
|
|
110
130
|
// Nothing to write or install — fall through to the codemod pass below.
|
|
111
131
|
log(`Already on ${current}; "${revision}" resolves to ${target}. No dependency bump needed — checking for codemods to catch up on, since nothing records whether this project already ran them.`);
|
|
132
|
+
reportPeers();
|
|
112
133
|
}
|
|
113
134
|
else if (dry) {
|
|
114
135
|
log(`Upgrading Flow from ${current} to ${target} (--dry)`);
|
|
115
136
|
log(`--dry: would write the following to ${manifestPath}:`);
|
|
116
137
|
reportDependencies();
|
|
138
|
+
reportPeers();
|
|
117
139
|
log("--dry: skipping the install.");
|
|
118
140
|
}
|
|
119
141
|
else {
|
|
@@ -121,6 +143,7 @@ export const runUpgrade = async (parsed, deps) => {
|
|
|
121
143
|
const indent = detectIndent(manifestRaw);
|
|
122
144
|
writeFileSync(manifestPath, `${JSON.stringify(applyTarget(manifest, target, flowPackages), null, indent)}\n`, "utf8");
|
|
123
145
|
reportDependencies();
|
|
146
|
+
reportPeers();
|
|
124
147
|
const manager = await detectPackageManagerIn(cwd);
|
|
125
148
|
try {
|
|
126
149
|
// Logged after the run, not before: `install` returns what it actually
|
|
@@ -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: "image-rounded-corners-default",
|
|
8
|
+
since: "1.3.0",
|
|
9
|
+
title: "Image: rounded corners by default, withBorder draws only the border",
|
|
10
|
+
kind: "migration",
|
|
11
|
+
action: "none",
|
|
12
|
+
remotePackage: true,
|
|
13
|
+
apply: "No code change required. To keep square corners on an `Image` without `withBorder`, pass `withRoundedCorners={false}`.",
|
|
14
|
+
},
|
|
6
15
|
{
|
|
7
16
|
id: "popover-open-state-props",
|
|
8
17
|
since: "1.1.47",
|
|
@@ -207,7 +216,7 @@ export const migrations = [
|
|
|
207
216
|
title: "Form: resets itself after the surrounding modal closes",
|
|
208
217
|
kind: "migration",
|
|
209
218
|
action: "none",
|
|
210
|
-
remotePackage:
|
|
219
|
+
remotePackage: false,
|
|
211
220
|
apply: "No code change required. To keep the previous behaviour (the form keeping what the user entered), pass `autoReset={false}` (or `autoReset={{ onAfterModalClose: false }}`) to `Form`.",
|
|
212
221
|
},
|
|
213
222
|
{
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { PackageVersions } from "./registry.js";
|
|
2
|
+
/** One peer range, and how each Flow package that states it treats it. */
|
|
3
|
+
export interface PeerRequirement {
|
|
4
|
+
/** The peer package, e.g. `react`. */
|
|
5
|
+
peer: string;
|
|
6
|
+
/** The range, exactly as the Flow packages authored it. */
|
|
7
|
+
range: string;
|
|
8
|
+
/** The declared Flow dependencies that require it, sorted. */
|
|
9
|
+
requiredBy: string[];
|
|
10
|
+
/**
|
|
11
|
+
* The declared Flow dependencies that mark it `optional`, sorted.
|
|
12
|
+
*
|
|
13
|
+
* Separate from `requiredBy` rather than a flag on the requirement, because
|
|
14
|
+
* optionality is **per package**: at 1.2.2 `react ^19.2.0` is required by
|
|
15
|
+
* `flow-react-components` and optional for `ext-bridge`. One line carrying
|
|
16
|
+
* both lists says that; a single boolean would have to pick a side, and
|
|
17
|
+
* either choice is wrong for half the readers.
|
|
18
|
+
*/
|
|
19
|
+
optionalFor: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface PeerSummary {
|
|
22
|
+
/**
|
|
23
|
+
* Everything the Flow packages peer on that is not itself a Flow package —
|
|
24
|
+
* react, react-hook-form, next, i18next and the rest.
|
|
25
|
+
*/
|
|
26
|
+
external: PeerRequirement[];
|
|
27
|
+
/**
|
|
28
|
+
* Flow-internal peers the target does **not** satisfy.
|
|
29
|
+
*
|
|
30
|
+
* Flow's internal peers publish as exact pins (`@mittwald/flow-remote-core`
|
|
31
|
+
* peers on `@mittwald/flow-react-components: "1.1.52"`), so the expected case
|
|
32
|
+
* — the pin naming the version being installed — is noise. What is not noise
|
|
33
|
+
* is a pin pointing somewhere else: upgrading every declared package to one
|
|
34
|
+
* target then leaves it unsatisfied, and nothing else in the output says so.
|
|
35
|
+
*
|
|
36
|
+
* This is not hypothetical. Across the published history of the five remote
|
|
37
|
+
* packages, 2253 of 6210 Flow-internal peer entries pin a version other than
|
|
38
|
+
* the one they ship with — e.g. `flow-remote-core@0.2.0-alpha.700` peers on
|
|
39
|
+
* `ext-bridge@0.2.0-alpha.699`.
|
|
40
|
+
*
|
|
41
|
+
* What it usually is **not** is a publish hole. Lerna releases only the
|
|
42
|
+
* packages that changed, so the pin names a neighbouring release that exists:
|
|
43
|
+
* 2207 of those 2253 point at a published version, leaving 46 that are
|
|
44
|
+
* genuinely missing (#2887). The rendered warning leads with the common cause
|
|
45
|
+
* for that reason (#3204 review).
|
|
46
|
+
*/
|
|
47
|
+
flowPins: PeerRequirement[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* What the declared Flow dependencies require of their peers at `target`.
|
|
51
|
+
*
|
|
52
|
+
* Grouped by peer **and** range, not by peer alone: two Flow packages asking
|
|
53
|
+
* for different ranges of the same peer is precisely what a consumer needs to
|
|
54
|
+
* see, and collapsing them would hide it. Optionality does **not** split a
|
|
55
|
+
* group — it is recorded per package inside it (see `optionalFor`).
|
|
56
|
+
*
|
|
57
|
+
* Ranges are reported exactly as authored — this never decides whether the
|
|
58
|
+
* project satisfies them. That is the package manager's job, it already does it
|
|
59
|
+
* during the install, and a second opinion here could only drift from the one
|
|
60
|
+
* that actually gates.
|
|
61
|
+
*/
|
|
62
|
+
export declare const collectPeers: (packages: PackageVersions[], target: string, flowPackages: string[]) => PeerSummary;
|
|
63
|
+
/** Whether a summary has anything worth printing. */
|
|
64
|
+
export declare const hasPeers: (summary: PeerSummary) => boolean;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { satisfies, validRange } from "semver";
|
|
2
|
+
/**
|
|
3
|
+
* Whether a Flow-internal peer pin is the unremarkable one.
|
|
4
|
+
*
|
|
5
|
+
* `satisfies` rather than a string compare, so a genuine range (`^1.1.0`) is as
|
|
6
|
+
* expected as the exact pin the publish pipeline usually writes. An unparseable
|
|
7
|
+
* range — `workspace:*`, from a package resolved straight out of a checkout
|
|
8
|
+
* rather than the registry — is not a claim about `target` at all, so it is
|
|
9
|
+
* reported rather than silently judged.
|
|
10
|
+
*
|
|
11
|
+
* `includePrerelease` is load-bearing, not tidiness: without it a prerelease
|
|
12
|
+
* satisfies **no** non-prerelease range, `*` included. The historical
|
|
13
|
+
* Flow-internal pin _is_ `*` (2328 of 6210 published entries), and `next` and
|
|
14
|
+
* `experimental` are prerelease dist-tags — so every prerelease target reported
|
|
15
|
+
* a publish hole for a pin that accepts everything.
|
|
16
|
+
*/
|
|
17
|
+
const expectedFlowPin = (range, target) => validRange(range) !== null &&
|
|
18
|
+
satisfies(target, range, { includePrerelease: true });
|
|
19
|
+
/**
|
|
20
|
+
* Ties break on the string, not on the locale.
|
|
21
|
+
*
|
|
22
|
+
* `localeCompare` depends on the runtime's ICU data, which differs between Node
|
|
23
|
+
* builds and would reorder hyphenated names like `react-dom` from one machine
|
|
24
|
+
* to the next — including between a developer's run and CI's.
|
|
25
|
+
*/
|
|
26
|
+
const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
27
|
+
/**
|
|
28
|
+
* What the declared Flow dependencies require of their peers at `target`.
|
|
29
|
+
*
|
|
30
|
+
* Grouped by peer **and** range, not by peer alone: two Flow packages asking
|
|
31
|
+
* for different ranges of the same peer is precisely what a consumer needs to
|
|
32
|
+
* see, and collapsing them would hide it. Optionality does **not** split a
|
|
33
|
+
* group — it is recorded per package inside it (see `optionalFor`).
|
|
34
|
+
*
|
|
35
|
+
* Ranges are reported exactly as authored — this never decides whether the
|
|
36
|
+
* project satisfies them. That is the package manager's job, it already does it
|
|
37
|
+
* during the install, and a second opinion here could only drift from the one
|
|
38
|
+
* that actually gates.
|
|
39
|
+
*/
|
|
40
|
+
export const collectPeers = (packages, target, flowPackages) => {
|
|
41
|
+
const grouped = new Map();
|
|
42
|
+
for (const pkg of packages) {
|
|
43
|
+
const peers = pkg.peerDependencies[target] ?? {};
|
|
44
|
+
for (const [peer, { range, optional }] of Object.entries(peers)) {
|
|
45
|
+
// Keyed on both halves, so the two sides of a disagreement survive as
|
|
46
|
+
// two entries. JSON rather than a joined string: no separator to pick
|
|
47
|
+
// that a package name or a range could contain.
|
|
48
|
+
const key = JSON.stringify([peer, range]);
|
|
49
|
+
const existing = grouped.get(key) ?? {
|
|
50
|
+
peer,
|
|
51
|
+
range,
|
|
52
|
+
requiredBy: [],
|
|
53
|
+
optionalFor: [],
|
|
54
|
+
};
|
|
55
|
+
(optional ? existing.optionalFor : existing.requiredBy).push(pkg.name);
|
|
56
|
+
grouped.set(key, existing);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const sorted = [...grouped.values()]
|
|
60
|
+
.map((requirement) => ({
|
|
61
|
+
...requirement,
|
|
62
|
+
requiredBy: [...requirement.requiredBy].sort(byString),
|
|
63
|
+
optionalFor: [...requirement.optionalFor].sort(byString),
|
|
64
|
+
}))
|
|
65
|
+
.sort((a, b) => byString(a.peer, b.peer) || byString(a.range, b.range));
|
|
66
|
+
return {
|
|
67
|
+
external: sorted.filter(({ peer }) => !flowPackages.includes(peer)),
|
|
68
|
+
flowPins: sorted.filter(({ peer, range }) => flowPackages.includes(peer) && !expectedFlowPin(range, target)),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
/** Whether a summary has anything worth printing. */
|
|
72
|
+
export const hasPeers = (summary) => summary.external.length > 0 || summary.flowPins.length > 0;
|
package/dist/resolve/range.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type FlowDependency, type Manifest } from "../manifest.js";
|
|
2
|
+
import { type PeerSummary } from "./peers.js";
|
|
2
3
|
import { fetchVersions } from "./registry.js";
|
|
3
4
|
/**
|
|
4
5
|
* The installed version of `name`, read from `cwd`'s `node_modules`.
|
|
@@ -23,6 +24,16 @@ export interface ResolvedRange {
|
|
|
23
24
|
current: string;
|
|
24
25
|
target: string;
|
|
25
26
|
versions: string[];
|
|
27
|
+
/**
|
|
28
|
+
* What the declared Flow dependencies peer on at `target` — the answer to
|
|
29
|
+
* "which Flow package needs which peer range", which otherwise takes reading
|
|
30
|
+
* every Flow `package.json` by hand (#3059).
|
|
31
|
+
*
|
|
32
|
+
* Resolved here rather than in either command because both need it and the
|
|
33
|
+
* packuments are already fetched above: `list <revision>` can then answer it
|
|
34
|
+
* before anything is written, which is where the answer is worth most.
|
|
35
|
+
*/
|
|
36
|
+
peers: PeerSummary;
|
|
26
37
|
}
|
|
27
38
|
export interface UnresolvedRange {
|
|
28
39
|
ok: false;
|
package/dist/resolve/range.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { rsort } from "semver";
|
|
4
4
|
import { flowPackages } from "../flowPackages.generated.js";
|
|
5
5
|
import { detectCurrentVersion, findFlowDependencies, } from "../manifest.js";
|
|
6
|
+
import { collectPeers } from "./peers.js";
|
|
6
7
|
import { fetchAllVersions, fetchVersions, intersectVersions, } from "./registry.js";
|
|
7
8
|
import { resolveTarget } from "./target.js";
|
|
8
9
|
/**
|
|
@@ -128,7 +129,14 @@ export const resolveRange = async (revision, deps) => {
|
|
|
128
129
|
// pick a version some of the others never published. Resolve instead from
|
|
129
130
|
// the intersection of what every declared dependency has actually
|
|
130
131
|
// published, so the version this returns is always installable.
|
|
131
|
-
|
|
132
|
+
// Deduplicated, because `findFlowDependencies` returns one entry per
|
|
133
|
+
// manifest *field*: a library that wraps Flow declares it in both
|
|
134
|
+
// `peerDependencies` and `devDependencies`, and that package would otherwise
|
|
135
|
+
// be fetched twice and counted twice — once per field — in every peer line it
|
|
136
|
+
// appears in (#3204 review). `dependencies` itself keeps both entries, on
|
|
137
|
+
// purpose: the two fields are reported separately, one rewritten and one left
|
|
138
|
+
// alone.
|
|
139
|
+
const fetched = await fetchAllVersions([...new Set(dependencies.map(({ name }) => name))], deps.fetchVersions);
|
|
132
140
|
const versions = intersectVersions(fetched);
|
|
133
141
|
if (versions.length === 0) {
|
|
134
142
|
return {
|
|
@@ -169,5 +177,6 @@ export const resolveRange = async (revision, deps) => {
|
|
|
169
177
|
current,
|
|
170
178
|
target,
|
|
171
179
|
versions,
|
|
180
|
+
peers: collectPeers(fetched, target, flowPackages),
|
|
172
181
|
};
|
|
173
182
|
};
|
|
@@ -1,12 +1,37 @@
|
|
|
1
|
+
/** A peer range as a Flow package declares it. */
|
|
2
|
+
export interface PeerDeclaration {
|
|
3
|
+
range: string;
|
|
4
|
+
/**
|
|
5
|
+
* Whether the declaring package marks this peer `optional`.
|
|
6
|
+
*
|
|
7
|
+
* Npm splits one declaration across two fields — the range in
|
|
8
|
+
* `peerDependencies`, the flag in `peerDependenciesMeta` — and a reader that
|
|
9
|
+
* takes only the first turns every optional peer into a hard requirement. The
|
|
10
|
+
* two are joined here, at the one place that knows the packument's encoding,
|
|
11
|
+
* so nothing downstream can read a range without its flag (#3059).
|
|
12
|
+
*/
|
|
13
|
+
optional: boolean;
|
|
14
|
+
}
|
|
1
15
|
export interface RegistryVersions {
|
|
2
16
|
versions: string[];
|
|
3
17
|
distTags: Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* What each published version declares as peers, keyed by version.
|
|
20
|
+
*
|
|
21
|
+
* Kept rather than discarded because the abbreviated packument below carries
|
|
22
|
+
* it for free — see `fetchVersions`. A version with no peers maps to an empty
|
|
23
|
+
* object, so a lookup never has to distinguish "none" from "not fetched".
|
|
24
|
+
*/
|
|
25
|
+
peerDependencies: Record<string, Record<string, PeerDeclaration>>;
|
|
4
26
|
}
|
|
5
27
|
/**
|
|
6
28
|
* Every published version of a package, plus its dist-tags.
|
|
7
29
|
*
|
|
8
30
|
* Uses the abbreviated packument media type: the full document for a package
|
|
9
|
-
* with hundreds of releases is megabytes, and none of it is needed here.
|
|
31
|
+
* with hundreds of releases is megabytes, and none of it is needed here. The
|
|
32
|
+
* abbreviated form still carries `peerDependencies` and `peerDependenciesMeta`
|
|
33
|
+
* per version, which is why the peer summary costs no request of its own
|
|
34
|
+
* (#3059).
|
|
10
35
|
*/
|
|
11
36
|
export declare const fetchVersions: (packageName: string) => Promise<RegistryVersions>;
|
|
12
37
|
export interface PackageVersions extends RegistryVersions {
|
package/dist/resolve/registry.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import registryUrl from "registry-url";
|
|
2
|
+
/** One version's peers, with the optional flag folded into each range. */
|
|
3
|
+
const declaredPeers = (manifest) => Object.fromEntries(Object.entries(manifest?.peerDependencies ?? {}).map(([peer, range]) => [
|
|
4
|
+
peer,
|
|
5
|
+
{
|
|
6
|
+
range,
|
|
7
|
+
optional: manifest?.peerDependenciesMeta?.[peer]?.optional === true,
|
|
8
|
+
},
|
|
9
|
+
]));
|
|
2
10
|
/**
|
|
3
11
|
* The registry that actually serves `@mittwald`, from the consumer's npm
|
|
4
12
|
* config.
|
|
@@ -18,7 +26,10 @@ const registryFor = (packageName) => {
|
|
|
18
26
|
* Every published version of a package, plus its dist-tags.
|
|
19
27
|
*
|
|
20
28
|
* Uses the abbreviated packument media type: the full document for a package
|
|
21
|
-
* with hundreds of releases is megabytes, and none of it is needed here.
|
|
29
|
+
* with hundreds of releases is megabytes, and none of it is needed here. The
|
|
30
|
+
* abbreviated form still carries `peerDependencies` and `peerDependenciesMeta`
|
|
31
|
+
* per version, which is why the peer summary costs no request of its own
|
|
32
|
+
* (#3059).
|
|
22
33
|
*/
|
|
23
34
|
export const fetchVersions = async (packageName) => {
|
|
24
35
|
// One wrapper for every failure, not just the HTTP one: `fetch` itself rejects
|
|
@@ -38,9 +49,11 @@ export const fetchVersions = async (packageName) => {
|
|
|
38
49
|
catch (error) {
|
|
39
50
|
throw new Error(`Could not read ${packageName} from the npm registry: ${error instanceof Error ? error.message : error}`, { cause: error });
|
|
40
51
|
}
|
|
52
|
+
const versions = Object.entries(packument.versions ?? {});
|
|
41
53
|
return {
|
|
42
|
-
versions:
|
|
54
|
+
versions: versions.map(([version]) => version),
|
|
43
55
|
distTags: packument["dist-tags"] ?? {},
|
|
56
|
+
peerDependencies: Object.fromEntries(versions.map(([version, manifest]) => [version, declaredPeers(manifest)])),
|
|
44
57
|
};
|
|
45
58
|
};
|
|
46
59
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mittwald/flow-codemods",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0-next.0",
|
|
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",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"vitest": "^4.1.11",
|
|
39
39
|
"yaml": "^2.8.1"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "d3cf8325cc06635accd2c170750429f9c834c87c"
|
|
42
42
|
}
|