@mittwald/flow-codemods 1.3.0-next.9 → 1.3.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 +5 -57
- package/dist/cli/codemod.js +8 -3
- package/dist/cli/list.d.ts +2 -11
- package/dist/cli/list.js +58 -17
- package/dist/cli/upgrade.js +7 -28
- package/dist/resolve/range.d.ts +0 -11
- package/dist/resolve/range.js +1 -10
- package/dist/resolve/registry.d.ts +1 -26
- package/dist/resolve/registry.js +2 -15
- package/dist/run/jscodeshift.d.ts +8 -1
- package/dist/run/jscodeshift.js +65 -8
- package/package.json +2 -2
- package/dist/cli/peers.d.ts +0 -22
- package/dist/cli/peers.js +0 -73
- package/dist/cli/text.d.ts +0 -27
- package/dist/cli/text.js +0 -62
- package/dist/resolve/peers.d.ts +0 -64
- package/dist/resolve/peers.js +0 -72
package/README.md
CHANGED
|
@@ -60,56 +60,6 @@ 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
|
-
|
|
113
63
|
After installing, `upgrade` runs the codemod of every migration whose `since` is
|
|
114
64
|
at or below the target and prints the ones with no codemod, for you to apply by
|
|
115
65
|
hand.
|
|
@@ -155,11 +105,8 @@ involve.
|
|
|
155
105
|
Options:
|
|
156
106
|
|
|
157
107
|
- `--json` — machine-readable output: an object with `range`
|
|
158
|
-
(`current`/`target`, or `null` for the offline whole-catalogue form)
|
|
159
|
-
`migrations`, each entry carrying `catchUp
|
|
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
|
|
108
|
+
(`current`/`target`, or `null` for the offline whole-catalogue form) and
|
|
109
|
+
`migrations`, each entry carrying `catchUp`
|
|
163
110
|
|
|
164
111
|
### `<id> [path]`
|
|
165
112
|
|
|
@@ -184,5 +131,6 @@ npx @mittwald/flow-codemods@latest to-remote-package src
|
|
|
184
131
|
|
|
185
132
|
`0` on success, `1` on a refusal — dirty tree, unresolvable revision, failed
|
|
186
133
|
install, an unknown id, or an id whose migration has no codemod — and `1` when a
|
|
187
|
-
codemod reports an error,
|
|
188
|
-
|
|
134
|
+
codemod reports an error, found nothing under `path` to process, or declined
|
|
135
|
+
every file it looked at. "Every" means every: a decline among files the
|
|
136
|
+
transform did read is a per-file fact, not a failed run.
|
package/dist/cli/codemod.js
CHANGED
|
@@ -78,14 +78,19 @@ export const runSingleCodemod = async (parsed, { cwd, log, run = runCodemod }) =
|
|
|
78
78
|
}
|
|
79
79
|
// The same trap as `processedNothing`, one field over: a transform that
|
|
80
80
|
// declines a file by returning nothing counts as `skipped`, not `unmodified`.
|
|
81
|
-
// If every file was
|
|
81
|
+
// If every file was declined and none changed, "0 file(s) changed" would read
|
|
82
82
|
// as a clean no-op run when in fact the transform bailed on everything.
|
|
83
|
-
|
|
83
|
+
//
|
|
84
|
+
// "All" means all: `unmodified > 0` says the transform read those files and
|
|
85
|
+
// handed them back, so a decline among them is a per-file fact, not a failed
|
|
86
|
+
// run.
|
|
87
|
+
if (result.changed === 0 && result.unmodified === 0 && result.skipped > 0) {
|
|
84
88
|
log(`${id}: the transform declined all ${result.skipped} file(s) it looked at, and changed none.`);
|
|
85
89
|
return 1;
|
|
86
90
|
}
|
|
87
91
|
const skipped = result.skipped > 0 ? `, ${result.skipped} skipped` : "";
|
|
88
|
-
const
|
|
92
|
+
const empty = result.empty > 0 ? `, ${result.empty} empty` : "";
|
|
93
|
+
const summary = `${id}: ${result.changed} file(s) changed, ${result.unmodified} unchanged${skipped}${empty}.`;
|
|
89
94
|
// Only a catalogued id has a migration guide entry to point at — a transform
|
|
90
95
|
// like `to-remote-package` with no catalogue entry has no anchor in
|
|
91
96
|
// `MIGRATION.md` to link, so pointing there would be a dead link.
|
package/dist/cli/list.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type CatalogEntry } from "../catalog/entries.js";
|
|
2
|
-
import type { PeerSummary } from "../resolve/peers.js";
|
|
3
2
|
import { type RangeDeps } from "../resolve/range.js";
|
|
4
3
|
import type { ParsedCommand } from "./args.js";
|
|
5
4
|
export interface RenderListInput {
|
|
@@ -14,15 +13,6 @@ export interface RenderListInput {
|
|
|
14
13
|
to: string;
|
|
15
14
|
};
|
|
16
15
|
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;
|
|
26
16
|
/** Emit ANSI colour. Off by default so a test sees plain text. */
|
|
27
17
|
color?: boolean;
|
|
28
18
|
/** Terminal width to wrap prose to. */
|
|
@@ -57,6 +47,7 @@ export interface RenderListInput {
|
|
|
57
47
|
*/
|
|
58
48
|
frame?: boolean;
|
|
59
49
|
}
|
|
50
|
+
export declare const stripAnsi: (text: string) => string;
|
|
60
51
|
/**
|
|
61
52
|
* The invocation to print when nobody says otherwise.
|
|
62
53
|
*
|
|
@@ -76,7 +67,7 @@ export declare const defaultInvoke = "npx @mittwald/flow-codemods@latest";
|
|
|
76
67
|
* so the rendering is deterministic in a test — `painter` builds its palette
|
|
77
68
|
* from `color` alone, never from the terminal. `cli.ts` supplies them.
|
|
78
69
|
*/
|
|
79
|
-
export declare const renderList: ({ entries, range, json,
|
|
70
|
+
export declare const renderList: ({ entries, range, json, color, width, path, invoke, frame, }: RenderListInput) => string;
|
|
80
71
|
export interface ListDeps extends RangeDeps {
|
|
81
72
|
/**
|
|
82
73
|
* Raw output writer — matches `process.stdout.write`'s own contract: no
|
package/dist/cli/list.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import colors from "picocolors";
|
|
1
2
|
import { lte } from "semver";
|
|
2
3
|
import { allEntries } from "../catalog/entries.js";
|
|
3
4
|
import { selectEntries, sortBySince } from "../catalog/select.js";
|
|
4
5
|
import { defaultRangeDeps, resolveRange, } from "../resolve/range.js";
|
|
5
|
-
import { renderPeers } from "./peers.js";
|
|
6
|
-
import { painter, wrap } from "./text.js";
|
|
7
6
|
/**
|
|
8
7
|
* What the reader has to do, and the colour that says it at a glance.
|
|
9
8
|
*
|
|
@@ -22,6 +21,39 @@ const actions = {
|
|
|
22
21
|
tone: "blue",
|
|
23
22
|
},
|
|
24
23
|
};
|
|
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
|
+
};
|
|
25
57
|
/**
|
|
26
58
|
* Renders `code` spans as colour instead of literal backticks.
|
|
27
59
|
*
|
|
@@ -30,6 +62,27 @@ const actions = {
|
|
|
30
62
|
* scans for — a symbol name or a command.
|
|
31
63
|
*/
|
|
32
64
|
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
|
+
};
|
|
33
86
|
/**
|
|
34
87
|
* The invocation to print when nobody says otherwise.
|
|
35
88
|
*
|
|
@@ -157,7 +210,7 @@ const renderSummary = (selected, color) => {
|
|
|
157
210
|
* so the rendering is deterministic in a test — `painter` builds its palette
|
|
158
211
|
* from `color` alone, never from the terminal. `cli.ts` supplies them.
|
|
159
212
|
*/
|
|
160
|
-
export const renderList = ({ entries, range, json,
|
|
213
|
+
export const renderList = ({ entries, range, json, color = false, width = 80, path = "src", invoke = defaultInvoke, frame = true, }) => {
|
|
161
214
|
// The two paths differ in their bounds, deliberately: unbounded, this is a
|
|
162
215
|
// plain catalogue browse — every entry, sorted, regardless of whether it
|
|
163
216
|
// would apply to any given range. Bounded, it answers "what does this
|
|
@@ -182,21 +235,10 @@ export const renderList = ({ entries, range, json, peers, color = false, width =
|
|
|
182
235
|
...entry,
|
|
183
236
|
catchUp: isCatchUp(entry, range?.from),
|
|
184
237
|
})),
|
|
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,
|
|
189
238
|
}, null, 2)}\n`;
|
|
190
239
|
}
|
|
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 });
|
|
197
240
|
if (selected.length === 0) {
|
|
198
|
-
|
|
199
|
-
return peerSection === "" ? nothing : `${nothing}\n${peerSection}`;
|
|
241
|
+
return "Nothing to migrate in that range.\n";
|
|
200
242
|
}
|
|
201
243
|
const body = selected
|
|
202
244
|
.map((entry) => renderEntry(entry, width, color, isCatchUp(entry, range?.from), path, invoke))
|
|
@@ -211,7 +253,7 @@ export const renderList = ({ entries, range, json, peers, color = false, width =
|
|
|
211
253
|
// gets left dangling above the first entry.
|
|
212
254
|
const context = renderContext(selected, range, color, width);
|
|
213
255
|
const summary = renderSummary(selected, color);
|
|
214
|
-
return `${context === "" ? "" : `${context}\n\n`}${
|
|
256
|
+
return `${context === "" ? "" : `${context}\n\n`}${body}\n\n${summary}\n`;
|
|
215
257
|
};
|
|
216
258
|
export const defaultListDeps = (cwd) => ({
|
|
217
259
|
...defaultRangeDeps(cwd),
|
|
@@ -253,7 +295,6 @@ export const runList = async (parsed, deps) => {
|
|
|
253
295
|
entries: allEntries,
|
|
254
296
|
range: { from: resolved.current, to: resolved.target },
|
|
255
297
|
json: parsed.json,
|
|
256
|
-
peers: resolved.peers,
|
|
257
298
|
color: deps.color,
|
|
258
299
|
width: deps.width,
|
|
259
300
|
path: deps.path,
|
package/dist/cli/upgrade.js
CHANGED
|
@@ -12,7 +12,6 @@ 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";
|
|
16
15
|
export const defaultUpgradeDeps = (cwd) => ({
|
|
17
16
|
cwd,
|
|
18
17
|
fetchVersions,
|
|
@@ -87,7 +86,7 @@ export const runUpgrade = async (parsed, deps) => {
|
|
|
87
86
|
log(range.reason);
|
|
88
87
|
return 1;
|
|
89
88
|
}
|
|
90
|
-
const { manifestPath, manifestRaw, manifest, dependencies, current, target
|
|
89
|
+
const { manifestPath, manifestRaw, manifest, dependencies, current, target } = range;
|
|
91
90
|
// A stale dist-tag or an exact version at or below `current` resolves
|
|
92
91
|
// without complaint — `resolveRange` deliberately does not judge that
|
|
93
92
|
// (`list` treats the same fact as a legitimate answer). There is nothing to
|
|
@@ -106,36 +105,15 @@ export const runUpgrade = async (parsed, deps) => {
|
|
|
106
105
|
: ` ${dependency.name} ${dependency.range} — left as it is, ${dependency.field} states what your package supports, not what it installs`);
|
|
107
106
|
}
|
|
108
107
|
};
|
|
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
|
-
};
|
|
128
108
|
const bump = gt(target, current);
|
|
129
109
|
if (!bump) {
|
|
130
110
|
// Nothing to write or install — fall through to the codemod pass below.
|
|
131
111
|
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();
|
|
133
112
|
}
|
|
134
113
|
else if (dry) {
|
|
135
114
|
log(`Upgrading Flow from ${current} to ${target} (--dry)`);
|
|
136
115
|
log(`--dry: would write the following to ${manifestPath}:`);
|
|
137
116
|
reportDependencies();
|
|
138
|
-
reportPeers();
|
|
139
117
|
log("--dry: skipping the install.");
|
|
140
118
|
}
|
|
141
119
|
else {
|
|
@@ -143,7 +121,6 @@ export const runUpgrade = async (parsed, deps) => {
|
|
|
143
121
|
const indent = detectIndent(manifestRaw);
|
|
144
122
|
writeFileSync(manifestPath, `${JSON.stringify(applyTarget(manifest, target, flowPackages), null, indent)}\n`, "utf8");
|
|
145
123
|
reportDependencies();
|
|
146
|
-
reportPeers();
|
|
147
124
|
const manager = await detectPackageManagerIn(cwd);
|
|
148
125
|
try {
|
|
149
126
|
// Logged after the run, not before: `install` returns what it actually
|
|
@@ -191,9 +168,9 @@ export const runUpgrade = async (parsed, deps) => {
|
|
|
191
168
|
if (result.changed > 0) {
|
|
192
169
|
changedCount += 1;
|
|
193
170
|
}
|
|
194
|
-
// Same three-way distinction as the single-codemod command
|
|
195
|
-
//
|
|
196
|
-
//
|
|
171
|
+
// Same three-way distinction as the single-codemod command, and the same
|
|
172
|
+
// reading of "declined all": `unmodified > 0` proves the transform read
|
|
173
|
+
// files and handed them back, so it did not bail on the tree.
|
|
197
174
|
if (result.errors > 0) {
|
|
198
175
|
hadFailure = true;
|
|
199
176
|
log(` ${entry.id}: ${result.errors} file(s) failed to transform`);
|
|
@@ -202,7 +179,9 @@ export const runUpgrade = async (parsed, deps) => {
|
|
|
202
179
|
hadFailure = true;
|
|
203
180
|
log(` ${entry.id}: no files under ${path} were processed`);
|
|
204
181
|
}
|
|
205
|
-
else if (result.changed === 0 &&
|
|
182
|
+
else if (result.changed === 0 &&
|
|
183
|
+
result.unmodified === 0 &&
|
|
184
|
+
result.skipped > 0) {
|
|
206
185
|
hadFailure = true;
|
|
207
186
|
log(` ${entry.id}: declined all ${result.skipped} file(s) it looked at`);
|
|
208
187
|
}
|
package/dist/resolve/range.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type FlowDependency, type Manifest } from "../manifest.js";
|
|
2
|
-
import { type PeerSummary } from "./peers.js";
|
|
3
2
|
import { fetchVersions } from "./registry.js";
|
|
4
3
|
/**
|
|
5
4
|
* The installed version of `name`, read from `cwd`'s `node_modules`.
|
|
@@ -24,16 +23,6 @@ export interface ResolvedRange {
|
|
|
24
23
|
current: string;
|
|
25
24
|
target: string;
|
|
26
25
|
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;
|
|
37
26
|
}
|
|
38
27
|
export interface UnresolvedRange {
|
|
39
28
|
ok: false;
|
package/dist/resolve/range.js
CHANGED
|
@@ -3,7 +3,6 @@ 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";
|
|
7
6
|
import { fetchAllVersions, fetchVersions, intersectVersions, } from "./registry.js";
|
|
8
7
|
import { resolveTarget } from "./target.js";
|
|
9
8
|
/**
|
|
@@ -129,14 +128,7 @@ export const resolveRange = async (revision, deps) => {
|
|
|
129
128
|
// pick a version some of the others never published. Resolve instead from
|
|
130
129
|
// the intersection of what every declared dependency has actually
|
|
131
130
|
// published, so the version this returns is always installable.
|
|
132
|
-
|
|
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);
|
|
131
|
+
const fetched = await fetchAllVersions(dependencies.map(({ name }) => name), deps.fetchVersions);
|
|
140
132
|
const versions = intersectVersions(fetched);
|
|
141
133
|
if (versions.length === 0) {
|
|
142
134
|
return {
|
|
@@ -177,6 +169,5 @@ export const resolveRange = async (revision, deps) => {
|
|
|
177
169
|
current,
|
|
178
170
|
target,
|
|
179
171
|
versions,
|
|
180
|
-
peers: collectPeers(fetched, target, flowPackages),
|
|
181
172
|
};
|
|
182
173
|
};
|
|
@@ -1,37 +1,12 @@
|
|
|
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
|
-
}
|
|
15
1
|
export interface RegistryVersions {
|
|
16
2
|
versions: string[];
|
|
17
3
|
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>>;
|
|
26
4
|
}
|
|
27
5
|
/**
|
|
28
6
|
* Every published version of a package, plus its dist-tags.
|
|
29
7
|
*
|
|
30
8
|
* Uses the abbreviated packument media type: the full document for a package
|
|
31
|
-
* with hundreds of releases is megabytes, and none of it is needed here.
|
|
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).
|
|
9
|
+
* with hundreds of releases is megabytes, and none of it is needed here.
|
|
35
10
|
*/
|
|
36
11
|
export declare const fetchVersions: (packageName: string) => Promise<RegistryVersions>;
|
|
37
12
|
export interface PackageVersions extends RegistryVersions {
|
package/dist/resolve/registry.js
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
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
|
-
]));
|
|
10
2
|
/**
|
|
11
3
|
* The registry that actually serves `@mittwald`, from the consumer's npm
|
|
12
4
|
* config.
|
|
@@ -26,10 +18,7 @@ const registryFor = (packageName) => {
|
|
|
26
18
|
* Every published version of a package, plus its dist-tags.
|
|
27
19
|
*
|
|
28
20
|
* Uses the abbreviated packument media type: the full document for a package
|
|
29
|
-
* with hundreds of releases is megabytes, and none of it is needed here.
|
|
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).
|
|
21
|
+
* with hundreds of releases is megabytes, and none of it is needed here.
|
|
33
22
|
*/
|
|
34
23
|
export const fetchVersions = async (packageName) => {
|
|
35
24
|
// One wrapper for every failure, not just the HTTP one: `fetch` itself rejects
|
|
@@ -49,11 +38,9 @@ export const fetchVersions = async (packageName) => {
|
|
|
49
38
|
catch (error) {
|
|
50
39
|
throw new Error(`Could not read ${packageName} from the npm registry: ${error instanceof Error ? error.message : error}`, { cause: error });
|
|
51
40
|
}
|
|
52
|
-
const versions = Object.entries(packument.versions ?? {});
|
|
53
41
|
return {
|
|
54
|
-
versions:
|
|
42
|
+
versions: Object.keys(packument.versions ?? {}),
|
|
55
43
|
distTags: packument["dist-tags"] ?? {},
|
|
56
|
-
peerDependencies: Object.fromEntries(versions.map(([version, manifest]) => [version, declaredPeers(manifest)])),
|
|
57
44
|
};
|
|
58
45
|
};
|
|
59
46
|
/**
|
|
@@ -16,8 +16,15 @@ export interface CodemodOptions {
|
|
|
16
16
|
export interface CodemodResult {
|
|
17
17
|
changed: number;
|
|
18
18
|
unmodified: number;
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Files the transform declined by returning nothing — empty ones excluded.
|
|
21
|
+
*
|
|
22
|
+
* Both come back from jscodeshift as `skip`. A caller may report this field
|
|
23
|
+
* as "the transform bailed on these"; it must not report `empty` that way.
|
|
24
|
+
*/
|
|
20
25
|
skipped: number;
|
|
26
|
+
/** Files with no content, which no transform can do anything with. */
|
|
27
|
+
empty: number;
|
|
21
28
|
errors: number;
|
|
22
29
|
/** True when jscodeshift accounted for no file at all — see below. */
|
|
23
30
|
processedNothing: boolean;
|
package/dist/run/jscodeshift.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { extname, join } from "node:path";
|
|
2
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
4
|
import { unknownCodemodMessage } from "../catalog/entries.js";
|
|
4
5
|
// jscodeshift ships no types for its Runner — `allowJs` in this repo's shared
|
|
@@ -54,6 +55,61 @@ const transformPath = (id) => findTransform(id) ?? `${packageRoot}src/tools/${id
|
|
|
54
55
|
* module's own `existsSync` check below.
|
|
55
56
|
*/
|
|
56
57
|
export const transformExists = (id) => existsSync(transformPath(id));
|
|
58
|
+
/**
|
|
59
|
+
* The extensions the walk takes, and the directories it stays out of.
|
|
60
|
+
*
|
|
61
|
+
* Both are handed to jscodeshift below — without them its walk takes every file
|
|
62
|
+
* under the path, and since `upgrade` runs codemods after the install,
|
|
63
|
+
* `node_modules` is freshly populated underneath; non-JS files then fail to
|
|
64
|
+
* parse, which shows up as `errors > 0` and hides the real change count.
|
|
65
|
+
*
|
|
66
|
+
* `emptySourceFiles` reads the same two constants so that its walk and
|
|
67
|
+
* jscodeshift's agree by construction.
|
|
68
|
+
*/
|
|
69
|
+
const extensions = ["js", "jsx", "ts", "tsx", "cjs", "mjs", "cts", "mts"];
|
|
70
|
+
const ignoredDirectories = ["node_modules", "dist", ".git"];
|
|
71
|
+
const sourceExtensions = new Set(extensions.map((extension) => `.${extension}`));
|
|
72
|
+
const ignoredDirectorySet = new Set(ignoredDirectories);
|
|
73
|
+
/**
|
|
74
|
+
* How many source files under `path` hold nothing at all.
|
|
75
|
+
*
|
|
76
|
+
* Jscodeshift's worker tests a transform's result for truthiness
|
|
77
|
+
* (`updateStatus(out ? 'nochange' : 'skip', file)`), and on an empty file both
|
|
78
|
+
* `root.toSource()` and `fileInfo.source` are `""` — so an empty file lands in
|
|
79
|
+
* `skip`, indistinguishable there from a transform that declined it.
|
|
80
|
+
*
|
|
81
|
+
* Counted with a walk of our own rather than filtered out of jscodeshift's
|
|
82
|
+
* input: this number only ever explains a count. If the two walks disagree, the
|
|
83
|
+
* worst outcome is a slightly wrong explanation, never a file that should have
|
|
84
|
+
* been transformed and was not.
|
|
85
|
+
*/
|
|
86
|
+
const emptySourceFiles = (path) => {
|
|
87
|
+
let found = 0;
|
|
88
|
+
const visit = (candidate) => {
|
|
89
|
+
let stats;
|
|
90
|
+
try {
|
|
91
|
+
stats = statSync(candidate);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// A broken symlink or a file removed mid-walk. jscodeshift skips these
|
|
95
|
+
// too ("Skipping path … which does not exist").
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (stats.isDirectory()) {
|
|
99
|
+
for (const child of readdirSync(candidate)) {
|
|
100
|
+
if (!ignoredDirectorySet.has(child)) {
|
|
101
|
+
visit(join(candidate, child));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (stats.size === 0 && sourceExtensions.has(extname(candidate))) {
|
|
107
|
+
found += 1;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
visit(path);
|
|
111
|
+
return found;
|
|
112
|
+
};
|
|
57
113
|
/**
|
|
58
114
|
* Runs one codemod over a path.
|
|
59
115
|
*
|
|
@@ -105,21 +161,22 @@ export const runCodemod = async ({ id, path, dry = false, print = false, }) => {
|
|
|
105
161
|
silent: true,
|
|
106
162
|
dry,
|
|
107
163
|
print,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// underneath — and non-JS files fail to parse, which shows up as
|
|
111
|
-
// `errors > 0` and hides the real change count.
|
|
112
|
-
extensions: "js,jsx,ts,tsx,cjs,mjs,cts,mts",
|
|
113
|
-
ignorePattern: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
|
|
164
|
+
extensions: extensions.join(","),
|
|
165
|
+
ignorePattern: ignoredDirectories.map((name) => `**/${name}/**`),
|
|
114
166
|
}));
|
|
115
167
|
}
|
|
116
168
|
catch (error) {
|
|
117
169
|
throw new Error(`Running ${id} failed: ${error instanceof Error ? error.message : error}`, { cause: error });
|
|
118
170
|
}
|
|
171
|
+
// Clamped rather than subtracted outright: the two walks are independent, so
|
|
172
|
+
// a file that appeared or emptied between them must not push `skipped` below
|
|
173
|
+
// zero.
|
|
174
|
+
const empty = Math.min(emptySourceFiles(path), stats.skip);
|
|
119
175
|
return {
|
|
120
176
|
changed: stats.ok,
|
|
121
177
|
unmodified: stats.nochange,
|
|
122
|
-
skipped: stats.skip,
|
|
178
|
+
skipped: stats.skip - empty,
|
|
179
|
+
empty,
|
|
123
180
|
errors: stats.error,
|
|
124
181
|
processedNothing: stats.ok + stats.nochange + stats.skip + stats.error === 0,
|
|
125
182
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mittwald/flow-codemods",
|
|
3
|
-
"version": "1.3.0
|
|
3
|
+
"version": "1.3.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": "c10f5dfb91a2f7544de5224b80707d9bcb98669e"
|
|
42
42
|
}
|
package/dist/cli/peers.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
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;
|
package/dist/cli/peers.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
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
|
-
};
|
package/dist/cli/text.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
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/resolve/peers.d.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
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;
|
package/dist/resolve/peers.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
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;
|