rolldown-pnpm-config 0.2.2 → 0.4.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 +2 -2
- package/bin/rolldown-pnpm-config.js +3 -6
- package/cli/commands/export.js +4 -4
- package/cli/commands/preview.js +2 -2
- package/cli/commands/upgrade.js +168 -98
- package/cli/drift.js +8 -2
- package/cli/edits.js +28 -18
- package/cli/interop.js +10 -10
- package/cli/peer-range.js +30 -4
- package/cli/plan.js +10 -8
- package/cli/resolve.js +31 -11
- package/cli/summary.js +94 -67
- package/cli/ui/Walk.js +53 -27
- package/cli/ui/run-preview.js +1 -1
- package/cli/ui/run-walk.js +7 -5
- package/cli/validate.js +88 -0
- package/cli/walk-plan.js +11 -3
- package/cli/walk-reducer.js +90 -35
- package/cli/workspace-file.js +7 -7
- package/descriptors/build.js +6 -1
- package/descriptors/hoisting.js +17 -3
- package/descriptors/misc.js +10 -2
- package/descriptors/resolution.js +2 -5
- package/descriptors/runtime-cfg.js +12 -2
- package/descriptors/schemas.js +4 -16
- package/descriptors/workspace.js +13 -4
- package/package.json +7 -17
- package/plugin/freeze.js +2 -2
- package/virtual.d.ts +12 -2
package/cli/interop.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import { Range, SemVer } from "semver
|
|
2
|
+
import { Range, SemVer } from "@effected/semver";
|
|
3
3
|
|
|
4
4
|
//#region src/cli/interop.ts
|
|
5
5
|
/** Maximum number of concurrent peerDependencies fetches inside runInterop. @internal */
|
|
@@ -34,7 +34,7 @@ function deriveFloors(resolved, fetchPeer) {
|
|
|
34
34
|
const valid = (yield* Effect.forEach(declared, (f) => SemVer.parse(f).pipe(Effect.map((sv) => ({
|
|
35
35
|
f,
|
|
36
36
|
sv
|
|
37
|
-
})), Effect.
|
|
37
|
+
})), Effect.catch(() => Effect.succeed(null))))).filter((x) => x !== null);
|
|
38
38
|
valid.sort((a, b) => a.sv.compare(b.sv));
|
|
39
39
|
out.set(pkg, `^${valid[0]?.f ?? version}`);
|
|
40
40
|
} else out.set(pkg, `^${version}`);
|
|
@@ -45,9 +45,9 @@ function deriveFloors(resolved, fetchPeer) {
|
|
|
45
45
|
/** Does `version` satisfy `range`? Unparseable input is treated as not-satisfied. */
|
|
46
46
|
function satisfies(version, range) {
|
|
47
47
|
return Effect.gen(function* () {
|
|
48
|
-
const r = yield* Range.parse(range).pipe(Effect.
|
|
48
|
+
const r = yield* Range.parse(range).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
49
49
|
if (!r) return false;
|
|
50
|
-
const v = yield* SemVer.parse(version).pipe(Effect.
|
|
50
|
+
const v = yield* SemVer.parse(version).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
51
51
|
return v ? r.test(v) : false;
|
|
52
52
|
});
|
|
53
53
|
}
|
|
@@ -81,8 +81,8 @@ function resolveGroup(members, fetchPeer) {
|
|
|
81
81
|
const resolved = new Map(members.map((m) => [m.pkg, m.ceiling]));
|
|
82
82
|
const ceilingOf = new Map(members.map((m) => [m.pkg, m.ceiling]));
|
|
83
83
|
const leq = (a, b) => Effect.gen(function* () {
|
|
84
|
-
const av = yield* SemVer.parse(a).pipe(Effect.
|
|
85
|
-
const bv = yield* SemVer.parse(b).pipe(Effect.
|
|
84
|
+
const av = yield* SemVer.parse(a).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
85
|
+
const bv = yield* SemVer.parse(b).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
86
86
|
return av && bv ? av.compare(bv) <= 0 : false;
|
|
87
87
|
});
|
|
88
88
|
const maxIter = members.reduce((n, m) => n + m.candidates.length, 0) + members.length + 1;
|
|
@@ -130,7 +130,7 @@ function sortDesc(versions) {
|
|
|
130
130
|
const parsed = yield* Effect.forEach(versions, (v) => SemVer.parse(v).pipe(Effect.map((sv) => ({
|
|
131
131
|
v,
|
|
132
132
|
sv
|
|
133
|
-
})), Effect.
|
|
133
|
+
})), Effect.catch(() => Effect.succeed({
|
|
134
134
|
v,
|
|
135
135
|
sv: null
|
|
136
136
|
}))));
|
|
@@ -165,7 +165,7 @@ function runInterop(members, resolver, cache = /* @__PURE__ */ new Map()) {
|
|
|
165
165
|
const k = key(pkg, v);
|
|
166
166
|
const cached = cache.get(k);
|
|
167
167
|
if (cached !== void 0) return Effect.succeed(cached);
|
|
168
|
-
return resolver.peerDependencies(pkg, v).pipe(Effect.
|
|
168
|
+
return resolver.peerDependencies(pkg, v).pipe(Effect.catch(() => Effect.succeed({}))).pipe(Effect.map((deps) => {
|
|
169
169
|
cache.set(k, deps);
|
|
170
170
|
return deps;
|
|
171
171
|
}));
|
|
@@ -270,11 +270,11 @@ function buildInteropEdits(entries, result) {
|
|
|
270
270
|
*/
|
|
271
271
|
function capVersions(list, max) {
|
|
272
272
|
return Effect.gen(function* () {
|
|
273
|
-
const mv = yield* SemVer.parse(max).pipe(Effect.
|
|
273
|
+
const mv = yield* SemVer.parse(max).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
274
274
|
if (!mv) return [...list];
|
|
275
275
|
const out = [];
|
|
276
276
|
for (const v of list) {
|
|
277
|
-
const sv = yield* SemVer.parse(v).pipe(Effect.
|
|
277
|
+
const sv = yield* SemVer.parse(v).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
278
278
|
if (sv && sv.compare(mv) <= 0) out.push(v);
|
|
279
279
|
}
|
|
280
280
|
return out;
|
package/cli/peer-range.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Data, Effect } from "effect";
|
|
2
|
-
import { SemVer } from "semver
|
|
2
|
+
import { SemVer } from "@effected/semver";
|
|
3
3
|
|
|
4
4
|
//#region src/cli/peer-range.ts
|
|
5
5
|
/**
|
|
@@ -15,8 +15,20 @@ const PREFIX_RE = /^(\^|~|)(\d.*)$/;
|
|
|
15
15
|
* "lock" pins to the exact version; "lock-minor" floors the patch to .0.
|
|
16
16
|
* The operator (^/~/exact) is preserved.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* "lock" reuses the version text verbatim rather than rebuilding it from
|
|
19
|
+
* major.minor.patch, so prerelease and build identifiers survive intact
|
|
20
|
+
* (rebuilding would silently drop them and derive an unpublished range).
|
|
21
|
+
*
|
|
22
|
+
* "lock-minor" floors a stable version's patch to .0, which intentionally
|
|
23
|
+
* drops any build metadata: `^6.5.1+build.7` derives to `^6.5.0`, not
|
|
24
|
+
* `^6.5.0+build.7`. Build metadata identifies a specific build of 6.5.1, not
|
|
25
|
+
* of the floored 6.5.0, and semver ignores build metadata when matching
|
|
26
|
+
* ranges anyway, so carrying it forward would be misleading.
|
|
27
|
+
*
|
|
28
|
+
* "lock-minor" is not meaningful on a prerelease — flooring `3.0.0-next.8` to
|
|
29
|
+
* `^3.0.0` yields a range that does not match `3.0.0-next.8` at all, excluding
|
|
30
|
+
* the very version being catalogued. It therefore degrades to "lock" behavior
|
|
31
|
+
* and reports a warning rather than emitting an unsatisfiable range.
|
|
20
32
|
*
|
|
21
33
|
* @internal
|
|
22
34
|
*/
|
|
@@ -26,7 +38,21 @@ function derivePeerRange(range, strategy) {
|
|
|
26
38
|
if (!match) return yield* Effect.fail(new PeerRangeError({ message: `Cannot derive peer range from "${range}"` }));
|
|
27
39
|
const [, prefix, version] = match;
|
|
28
40
|
const parsed = yield* SemVer.parse(version).pipe(Effect.mapError(() => new PeerRangeError({ message: `Invalid version in range "${range}"` })));
|
|
29
|
-
|
|
41
|
+
if (strategy === "lock") return {
|
|
42
|
+
range: `${prefix}${version}`,
|
|
43
|
+
warning: null
|
|
44
|
+
};
|
|
45
|
+
if (parsed.prerelease.length > 0) return {
|
|
46
|
+
range: `${prefix}${version}`,
|
|
47
|
+
warning: {
|
|
48
|
+
kind: "lock-minor-prerelease",
|
|
49
|
+
message: `lock-minor cannot floor the prerelease "${version}" — pinned to the exact version instead`
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
range: `${prefix}${parsed.major}.${parsed.minor}.0`,
|
|
54
|
+
warning: null
|
|
55
|
+
};
|
|
30
56
|
});
|
|
31
57
|
}
|
|
32
58
|
|
package/cli/plan.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { derivePeerRange } from "./peer-range.js";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
|
-
import { Range, SemVer } from "semver
|
|
3
|
+
import { Range, SemVer } from "@effected/semver";
|
|
4
4
|
|
|
5
5
|
//#region src/cli/plan.ts
|
|
6
6
|
/** Parse a version, returning null instead of failing (filters junk tags). */
|
|
7
|
-
const parseOrNull = (v) => SemVer.parse(v).pipe(Effect.
|
|
7
|
+
const parseOrNull = (v) => SemVer.parse(v).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
8
8
|
/**
|
|
9
9
|
* Compute the candidate versions for one catalog entry against the list of
|
|
10
10
|
* published versions. Order: latest in-range (when newer than current), latest
|
|
@@ -16,20 +16,22 @@ const parseOrNull = (v) => SemVer.parse(v).pipe(Effect.catchAll(() => Effect.suc
|
|
|
16
16
|
*/
|
|
17
17
|
function planEntry(entry, versions) {
|
|
18
18
|
return Effect.gen(function* () {
|
|
19
|
-
const range = yield* Range.parse(entry.currentRange).pipe(Effect.
|
|
19
|
+
const range = yield* Range.parse(entry.currentRange).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
20
|
+
const currentStripped = entry.currentRange.replace(/^[\^~]/, "");
|
|
21
|
+
const current = yield* parseOrNull(currentStripped);
|
|
22
|
+
const currentMajor = current?.major ?? 0;
|
|
23
|
+
const track = current && current.prerelease.length > 0 ? String(current.prerelease[0]) : null;
|
|
24
|
+
const onTrack = (v) => track !== null && v.prerelease.length > 0 && String(v.prerelease[0]) === track;
|
|
20
25
|
const parsed = [];
|
|
21
26
|
for (const v of versions) {
|
|
22
27
|
const sv = yield* parseOrNull(v);
|
|
23
|
-
if (sv
|
|
28
|
+
if (sv && (sv.isStable || onTrack(sv))) parsed.push(sv);
|
|
24
29
|
}
|
|
25
30
|
parsed.sort((a, b) => a.compare(b));
|
|
26
31
|
const maxOf = (list) => list.length ? list[list.length - 1] : null;
|
|
27
|
-
const currentStripped = entry.currentRange.replace(/^[\^~]/, "");
|
|
28
|
-
const current = yield* parseOrNull(currentStripped);
|
|
29
|
-
const currentMajor = current?.major ?? 0;
|
|
30
32
|
const inRangeMax = range ? maxOf(parsed.filter((v) => range.test(v))) : null;
|
|
31
33
|
const overallMax = maxOf(parsed);
|
|
32
|
-
const withPeer = (version) => entry.strategy && entry.strategy !== "interop" ? derivePeerRange(`${entry.operator}${version}`, entry.strategy) : Effect.succeed(void 0);
|
|
34
|
+
const withPeer = (version) => entry.strategy && entry.strategy !== "interop" ? derivePeerRange(`${entry.operator}${version}`, entry.strategy).pipe(Effect.map((d) => d.range)) : Effect.succeed(void 0);
|
|
33
35
|
const candidates = [];
|
|
34
36
|
if (inRangeMax && (current === null || inRangeMax.gt(current))) {
|
|
35
37
|
const version = inRangeMax.toString();
|
package/cli/resolve.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Context, Data, Effect, Layer } from "effect";
|
|
2
|
-
import {
|
|
2
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
3
3
|
|
|
4
4
|
//#region src/cli/resolve.ts
|
|
5
|
+
const Spawner = ChildProcessSpawner.ChildProcessSpawner;
|
|
5
6
|
/**
|
|
6
7
|
* Typed failure raised when a package's versions cannot be resolved.
|
|
7
8
|
*
|
|
@@ -15,7 +16,7 @@ var ResolveError = class extends Data.TaggedError("ResolveError") {};
|
|
|
15
16
|
*
|
|
16
17
|
* @internal
|
|
17
18
|
*/
|
|
18
|
-
var RegistryResolver = class extends Context.
|
|
19
|
+
var RegistryResolver = class extends Context.Service()("RegistryResolver") {};
|
|
19
20
|
/** Parse `pnpm view ... versions --json` stdout: a JSON array, or a single JSON string. */
|
|
20
21
|
function parseVersions(pkg, stdout) {
|
|
21
22
|
return Effect.try({
|
|
@@ -75,32 +76,51 @@ function parsePeerDeps(pkg, stdout) {
|
|
|
75
76
|
* @internal
|
|
76
77
|
*/
|
|
77
78
|
const RegistryResolverLive = Layer.effect(RegistryResolver, Effect.gen(function* () {
|
|
78
|
-
const
|
|
79
|
+
const spawner = yield* Spawner;
|
|
79
80
|
return {
|
|
80
81
|
versions: (pkg) => Effect.gen(function* () {
|
|
81
|
-
const cmd =
|
|
82
|
-
|
|
82
|
+
const cmd = ChildProcess.make("pnpm", [
|
|
83
|
+
"view",
|
|
84
|
+
pkg,
|
|
85
|
+
"versions",
|
|
86
|
+
"--json"
|
|
87
|
+
]);
|
|
88
|
+
return yield* parseVersions(pkg, yield* spawner.string(cmd).pipe(Effect.mapError((e) => new ResolveError({
|
|
83
89
|
pkg,
|
|
84
90
|
message: String(e)
|
|
85
91
|
}))));
|
|
86
92
|
}),
|
|
87
93
|
times: (pkg) => Effect.gen(function* () {
|
|
88
|
-
const cmd =
|
|
89
|
-
|
|
94
|
+
const cmd = ChildProcess.make("pnpm", [
|
|
95
|
+
"view",
|
|
96
|
+
pkg,
|
|
97
|
+
"time",
|
|
98
|
+
"--json"
|
|
99
|
+
]);
|
|
100
|
+
return yield* parseTimes(pkg, yield* spawner.string(cmd).pipe(Effect.mapError((e) => new ResolveError({
|
|
90
101
|
pkg,
|
|
91
102
|
message: String(e)
|
|
92
103
|
}))));
|
|
93
104
|
}),
|
|
94
105
|
peerDependencies: (pkg, version) => Effect.gen(function* () {
|
|
95
|
-
const cmd =
|
|
96
|
-
|
|
106
|
+
const cmd = ChildProcess.make("pnpm", [
|
|
107
|
+
"view",
|
|
108
|
+
`${pkg}@${version}`,
|
|
109
|
+
"peerDependencies",
|
|
110
|
+
"--json"
|
|
111
|
+
]);
|
|
112
|
+
return yield* parsePeerDeps(pkg, yield* spawner.string(cmd).pipe(Effect.mapError((e) => new ResolveError({
|
|
97
113
|
pkg,
|
|
98
114
|
message: String(e)
|
|
99
115
|
}))));
|
|
100
116
|
}),
|
|
101
117
|
pnpmConfig: (key) => Effect.gen(function* () {
|
|
102
|
-
const cmd =
|
|
103
|
-
|
|
118
|
+
const cmd = ChildProcess.make("pnpm", [
|
|
119
|
+
"config",
|
|
120
|
+
"get",
|
|
121
|
+
key
|
|
122
|
+
]);
|
|
123
|
+
return yield* spawner.string(cmd).pipe(Effect.map((s) => s.trim()), Effect.catch(() => Effect.succeed(null)));
|
|
104
124
|
})
|
|
105
125
|
};
|
|
106
126
|
}));
|
package/cli/summary.js
CHANGED
|
@@ -1,89 +1,90 @@
|
|
|
1
1
|
import { toAnsi } from "./ui/ansi.js";
|
|
2
|
+
import { displayCandidates, peerFor } from "./walk-reducer.js";
|
|
2
3
|
|
|
3
4
|
//#region src/cli/summary.ts
|
|
5
|
+
/** Trailing annotation appended to a major candidate's cell, e.g. " ⚠ major". */
|
|
6
|
+
const MAJOR_SUFFIX = " ⚠ major";
|
|
7
|
+
/** Filled / hollow radio glyphs. MUST match `ui/Walk.ts` — the summary mirrors the table. */
|
|
8
|
+
const SELECTED = "●";
|
|
9
|
+
const UNSELECTED = "○";
|
|
10
|
+
/** "● " / "○ " glyph-plus-space prefix width, common to every cell. */
|
|
11
|
+
const BUBBLE_WIDTH = 2;
|
|
4
12
|
/**
|
|
5
|
-
* Build the pending-decisions summary as styled lines: one
|
|
6
|
-
*
|
|
7
|
-
*
|
|
13
|
+
* Build the pending-decisions summary as styled lines: one table row per
|
|
14
|
+
* decision — mirroring the interactive selection table, catalog headers,
|
|
15
|
+
* chosen bubble filled — then a dim tally, interop adjustments, conflicts,
|
|
16
|
+
* and any rejected edits. Pure; color is applied by `renderSummary`/`toAnsi`.
|
|
8
17
|
*
|
|
9
18
|
* @internal
|
|
10
19
|
*/
|
|
11
|
-
function summaryLines(decisions, interop) {
|
|
20
|
+
function summaryLines(decisions, interop, rejected) {
|
|
12
21
|
const lines = [];
|
|
13
22
|
let toUpdate = 0;
|
|
14
23
|
let major = 0;
|
|
15
24
|
let resync = 0;
|
|
16
25
|
let materialize = 0;
|
|
17
26
|
let upToDate = 0;
|
|
27
|
+
const pkgWidth = decisions.length ? Math.max(...decisions.map((d) => d.item.entry.pkg.length)) : 0;
|
|
28
|
+
const cellWidth = decisions.length ? Math.max(...decisions.flatMap((d) => displayCandidates(d.item).map((c) => c.range.length + (c.isMajor ? 8 : 0)))) : 0;
|
|
29
|
+
const maxCells = decisions.length ? Math.max(...decisions.map((d) => displayCandidates(d.item).length)) : 0;
|
|
30
|
+
const blankCell = `${" ".repeat(BUBBLE_WIDTH + cellWidth)} `;
|
|
31
|
+
let lastCatalog = null;
|
|
18
32
|
for (const { item, chosen } of decisions) {
|
|
19
33
|
const { entry } = item;
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
if (chosen.isMajor) major++;
|
|
34
|
+
if (entry.catalog !== lastCatalog) {
|
|
35
|
+
lastCatalog = entry.catalog;
|
|
23
36
|
lines.push({
|
|
24
37
|
indent: 0,
|
|
25
|
-
gutter: "
|
|
38
|
+
gutter: " ",
|
|
26
39
|
segments: [{
|
|
27
|
-
text:
|
|
28
|
-
style: "
|
|
40
|
+
text: `── catalog: ${entry.catalog} ──`,
|
|
41
|
+
style: "unchanged"
|
|
29
42
|
}]
|
|
30
43
|
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
style: "added"
|
|
46
|
-
}]
|
|
47
|
-
});
|
|
48
|
-
materialize++;
|
|
49
|
-
}
|
|
50
|
-
} else if (entry.peer && item.driftPeer) {
|
|
51
|
-
resync++;
|
|
52
|
-
lines.push({
|
|
53
|
-
indent: 0,
|
|
54
|
-
gutter: "~",
|
|
55
|
-
segments: [{
|
|
56
|
-
text: `${entry.catalog} › ${entry.pkg} (resync peer)`,
|
|
57
|
-
style: "changed"
|
|
58
|
-
}]
|
|
59
|
-
});
|
|
60
|
-
lines.push({
|
|
61
|
-
indent: 1,
|
|
62
|
-
gutter: "~",
|
|
63
|
-
segments: [{
|
|
64
|
-
text: `↳ peer ${entry.peer.value} → ${item.driftPeer}`,
|
|
65
|
-
style: "changed"
|
|
66
|
-
}]
|
|
67
|
-
});
|
|
68
|
-
} else if (!entry.peer && item.materializePeer) {
|
|
69
|
-
materialize++;
|
|
70
|
-
lines.push({
|
|
71
|
-
indent: 0,
|
|
72
|
-
gutter: "+",
|
|
73
|
-
segments: [{
|
|
74
|
-
text: `${entry.catalog} › ${entry.pkg} (materialize peer)`,
|
|
75
|
-
style: "added"
|
|
76
|
-
}]
|
|
77
|
-
});
|
|
78
|
-
lines.push({
|
|
79
|
-
indent: 1,
|
|
80
|
-
gutter: "+",
|
|
81
|
-
segments: [{
|
|
82
|
-
text: `↳ peer (new) → ${item.materializePeer}`,
|
|
83
|
-
style: "added"
|
|
84
|
-
}]
|
|
44
|
+
}
|
|
45
|
+
const cells = displayCandidates(item);
|
|
46
|
+
const segments = [{
|
|
47
|
+
text: entry.pkg.padEnd(pkgWidth + 2),
|
|
48
|
+
style: "plain"
|
|
49
|
+
}];
|
|
50
|
+
for (const c of cells) {
|
|
51
|
+
const selected = c.kind === chosen.kind;
|
|
52
|
+
const bubble = selected ? SELECTED : UNSELECTED;
|
|
53
|
+
const style = !selected ? "unchanged" : c.kind === "keep" ? "unchanged" : c.isMajor ? "changed" : "added";
|
|
54
|
+
const content = `${c.range}${c.isMajor ? MAJOR_SUFFIX : ""}`.padEnd(cellWidth);
|
|
55
|
+
segments.push({
|
|
56
|
+
text: `${bubble} ${content} `,
|
|
57
|
+
style
|
|
85
58
|
});
|
|
86
|
-
}
|
|
59
|
+
}
|
|
60
|
+
for (let ci = cells.length; ci < maxCells; ci++) segments.push({
|
|
61
|
+
text: blankCell,
|
|
62
|
+
style: "plain"
|
|
63
|
+
});
|
|
64
|
+
segments.push({
|
|
65
|
+
text: `│ ${peerFor(item, chosen)}`,
|
|
66
|
+
style: "unchanged"
|
|
67
|
+
});
|
|
68
|
+
lines.push({
|
|
69
|
+
indent: 0,
|
|
70
|
+
gutter: chosen.kind === "keep" ? " " : "~",
|
|
71
|
+
segments
|
|
72
|
+
});
|
|
73
|
+
if (item.peerWarning) lines.push({
|
|
74
|
+
indent: 1,
|
|
75
|
+
gutter: "⚠",
|
|
76
|
+
segments: [{
|
|
77
|
+
text: item.peerWarning.message,
|
|
78
|
+
style: "warn"
|
|
79
|
+
}]
|
|
80
|
+
});
|
|
81
|
+
if (chosen.kind !== "keep") {
|
|
82
|
+
toUpdate++;
|
|
83
|
+
if (chosen.isMajor) major++;
|
|
84
|
+
if (!entry.peer && entry.strategy && chosen.peerRange) materialize++;
|
|
85
|
+
} else if (entry.peer && item.driftPeer) resync++;
|
|
86
|
+
else if (!entry.peer && item.materializePeer) materialize++;
|
|
87
|
+
else upToDate++;
|
|
87
88
|
}
|
|
88
89
|
lines.push({
|
|
89
90
|
indent: 0,
|
|
@@ -121,6 +122,32 @@ function summaryLines(decisions, interop) {
|
|
|
121
122
|
}]
|
|
122
123
|
});
|
|
123
124
|
}
|
|
125
|
+
if (rejected && rejected.length > 0) {
|
|
126
|
+
lines.push({
|
|
127
|
+
indent: 0,
|
|
128
|
+
gutter: " ",
|
|
129
|
+
segments: [{
|
|
130
|
+
text: "",
|
|
131
|
+
style: "plain"
|
|
132
|
+
}]
|
|
133
|
+
});
|
|
134
|
+
lines.push({
|
|
135
|
+
indent: 0,
|
|
136
|
+
gutter: "⚠",
|
|
137
|
+
segments: [{
|
|
138
|
+
text: "Rejected (no published version satisfies these):",
|
|
139
|
+
style: "warn"
|
|
140
|
+
}]
|
|
141
|
+
});
|
|
142
|
+
for (const r of rejected) lines.push({
|
|
143
|
+
indent: 1,
|
|
144
|
+
gutter: "⚠",
|
|
145
|
+
segments: [{
|
|
146
|
+
text: `${r.pkg} ${r.kind} ${r.value} — ${r.reason}`,
|
|
147
|
+
style: "warn"
|
|
148
|
+
}]
|
|
149
|
+
});
|
|
150
|
+
}
|
|
124
151
|
return lines;
|
|
125
152
|
}
|
|
126
153
|
/**
|
|
@@ -129,8 +156,8 @@ function summaryLines(decisions, interop) {
|
|
|
129
156
|
*
|
|
130
157
|
* @internal
|
|
131
158
|
*/
|
|
132
|
-
function renderSummary(decisions, interop, opts) {
|
|
133
|
-
return toAnsi(summaryLines(decisions, interop), { color: opts?.color ?? false });
|
|
159
|
+
function renderSummary(decisions, interop, opts, rejected) {
|
|
160
|
+
return toAnsi(summaryLines(decisions, interop, rejected), { color: opts?.color ?? false });
|
|
134
161
|
}
|
|
135
162
|
|
|
136
163
|
//#endregion
|
package/cli/ui/Walk.js
CHANGED
|
@@ -1,54 +1,80 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cellColor, displayCandidates, initTable, peerFor, tableDecisions, tableStep } from "../walk-reducer.js";
|
|
2
2
|
import { Box, Text, useApp, useInput } from "ink";
|
|
3
3
|
import { createElement, useEffect, useState } from "react";
|
|
4
4
|
|
|
5
5
|
//#region src/cli/ui/Walk.ts
|
|
6
|
+
/** Rows visible at once before the viewport scrolls. */
|
|
7
|
+
const VIEWPORT = 20;
|
|
6
8
|
/**
|
|
7
|
-
* Interactive
|
|
9
|
+
* Interactive upgrade table rendered with Ink. Every package is one row; each
|
|
10
|
+
* row is a radio group over its candidates with keep (index 0) preselected, so
|
|
11
|
+
* the default state of the table applies nothing.
|
|
8
12
|
*
|
|
9
13
|
* Written with React.createElement (no JSX) so the file can be plain .ts
|
|
10
14
|
* without requiring TSX transform configuration.
|
|
11
15
|
*
|
|
12
16
|
* @internal
|
|
13
17
|
*/
|
|
14
|
-
function Walk({ items, onDone }) {
|
|
18
|
+
function Walk({ items, onDone, dryRun = false, unresolved = [] }) {
|
|
15
19
|
const app = useApp();
|
|
16
|
-
const [state, setState] = useState(() =>
|
|
20
|
+
const [state, setState] = useState(() => initTable(items));
|
|
17
21
|
useEffect(() => {
|
|
18
|
-
if (
|
|
19
|
-
onDone(
|
|
22
|
+
if (items.length === 0) {
|
|
23
|
+
onDone([]);
|
|
20
24
|
app.exit();
|
|
21
25
|
}
|
|
22
26
|
}, []);
|
|
23
27
|
useInput((_input, key) => {
|
|
24
28
|
if (state.done) return;
|
|
25
|
-
const which = key.upArrow ? "up" : key.downArrow ? "down" : key.return ? "
|
|
29
|
+
const which = key.upArrow ? "up" : key.downArrow ? "down" : key.leftArrow ? "left" : key.rightArrow ? "right" : key.return ? "submit" : key.escape ? "cancel" : null;
|
|
26
30
|
if (!which) return;
|
|
27
|
-
const next =
|
|
31
|
+
const next = tableStep(state, items, which);
|
|
28
32
|
setState(next);
|
|
29
33
|
if (next.done) {
|
|
30
|
-
onDone(next
|
|
34
|
+
onDone(tableDecisions(next, items));
|
|
31
35
|
app.exit();
|
|
32
36
|
}
|
|
33
37
|
});
|
|
34
|
-
if (state.done ||
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
38
|
+
if (state.done || items.length === 0) return createElement(Text, null, "Done.");
|
|
39
|
+
const pkgWidth = Math.max(...items.map((i) => i.entry.pkg.length));
|
|
40
|
+
const MAJOR_SUFFIX = " ⚠ major";
|
|
41
|
+
const SELECTED = "●";
|
|
42
|
+
const UNSELECTED = "○";
|
|
43
|
+
const cellWidth = Math.max(...items.flatMap((i) => displayCandidates(i).map((c) => c.range.length + (c.isMajor ? 8 : 0))));
|
|
44
|
+
const maxCells = Math.max(...items.map((i) => displayCandidates(i).length));
|
|
45
|
+
const blankCell = `${" ".repeat(2 + cellWidth)} `;
|
|
46
|
+
const start = Math.max(0, Math.min(state.cursor - Math.floor(VIEWPORT / 2), items.length - VIEWPORT));
|
|
47
|
+
const visible = items.slice(Math.max(0, start), Math.max(0, start) + VIEWPORT);
|
|
48
|
+
const rows = [];
|
|
49
|
+
let lastCatalog = null;
|
|
50
|
+
visible.forEach((item, offset) => {
|
|
51
|
+
const i = Math.max(0, start) + offset;
|
|
52
|
+
if (item.entry.catalog !== lastCatalog) {
|
|
53
|
+
lastCatalog = item.entry.catalog;
|
|
54
|
+
rows.push(createElement(Text, {
|
|
55
|
+
key: `cat-${item.entry.catalog}`,
|
|
56
|
+
dimColor: true
|
|
57
|
+
}, ` ── catalog: ${lastCatalog} ──`));
|
|
58
|
+
}
|
|
59
|
+
const onCursor = i === state.cursor;
|
|
60
|
+
const pick = state.picks[i] ?? 0;
|
|
61
|
+
const candidates = displayCandidates(item);
|
|
62
|
+
const cells = candidates.map((c, ci) => {
|
|
63
|
+
const selected = ci === pick;
|
|
64
|
+
const bubble = selected ? SELECTED : UNSELECTED;
|
|
65
|
+
const major = c.isMajor ? MAJOR_SUFFIX : "";
|
|
66
|
+
const content = `${c.range}${major}`.padEnd(cellWidth);
|
|
67
|
+
const color = cellColor(c, selected);
|
|
68
|
+
return createElement(Text, {
|
|
69
|
+
key: c.kind,
|
|
70
|
+
...color ? { color } : {}
|
|
71
|
+
}, `${bubble} ${content} `);
|
|
72
|
+
});
|
|
73
|
+
for (let ci = candidates.length; ci < maxCells; ci++) cells.push(createElement(Text, { key: `blank-${ci}` }, blankCell));
|
|
74
|
+
const chosen = candidates[pick];
|
|
75
|
+
rows.push(createElement(Box, { key: `${item.entry.catalog}/${item.entry.pkg}` }, createElement(Text, { ...onCursor ? { color: "cyan" } : {} }, onCursor ? "❯ " : " "), createElement(Text, { bold: onCursor }, item.entry.pkg.padEnd(pkgWidth + 2)), ...cells, createElement(Text, { dimColor: true }, `│ ${chosen === void 0 ? "—" : peerFor(item, chosen)}`), item.peerWarning ? createElement(Text, { color: "red" }, ` ⚠ ${item.peerWarning.message}`) : null));
|
|
76
|
+
});
|
|
77
|
+
return createElement(Box, { flexDirection: "column" }, createElement(Text, { bold: true }, dryRun ? "Enter to preview • Esc to cancel" : "Enter to update • Esc to cancel"), dryRun ? createElement(Text, { color: "yellow" }, "DRY RUN — nothing will be written to the config") : null, unresolved.length > 0 ? createElement(Text, { color: "red" }, `⚠ Could not resolve from the registry — check for a typo: ${unresolved.join(", ")}`) : null, createElement(Box, { height: 1 }), ...rows);
|
|
52
78
|
}
|
|
53
79
|
|
|
54
80
|
//#endregion
|
package/cli/ui/run-preview.js
CHANGED
package/cli/ui/run-walk.js
CHANGED
|
@@ -5,17 +5,19 @@ import { createElement } from "react";
|
|
|
5
5
|
|
|
6
6
|
//#region src/cli/ui/run-walk.ts
|
|
7
7
|
/**
|
|
8
|
-
* Render the interactive
|
|
9
|
-
* decisions once the user
|
|
10
|
-
* after Ink has fully exited.
|
|
8
|
+
* Render the interactive table inside an Effect, resolving with the collected
|
|
9
|
+
* decisions once the user submits (or with an empty list when they cancel with
|
|
10
|
+
* Esc, or when no rows are actionable), after Ink has fully exited.
|
|
11
11
|
*
|
|
12
12
|
* @internal
|
|
13
13
|
*/
|
|
14
|
-
function runWalk(items) {
|
|
15
|
-
return Effect.
|
|
14
|
+
function runWalk(items, dryRun = false, unresolved = []) {
|
|
15
|
+
return Effect.callback((resume) => {
|
|
16
16
|
let collected = [];
|
|
17
17
|
render(createElement(Walk, {
|
|
18
18
|
items,
|
|
19
|
+
dryRun,
|
|
20
|
+
unresolved,
|
|
19
21
|
onDone: (d) => {
|
|
20
22
|
collected = d;
|
|
21
23
|
}
|