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/README.md
CHANGED
|
@@ -77,11 +77,11 @@ The bundled `rolldown-pnpm-config upgrade` command rewrites the version ranges i
|
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
79
|
npx rolldown-pnpm-config upgrade
|
|
80
|
-
#
|
|
80
|
+
# opens a radio-group table, one row per package, then on <Enter>:
|
|
81
81
|
# Applied <n> change(s).
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
The
|
|
84
|
+
The command shows an interactive table by default — every catalog package at once, one row per package, `●`/`○` bubbles, modeled on `pnpm up -i`. `--yes` takes the latest in-range version for every package without prompting and fails hard on any warning or unresolvable package name; `--dry-run` runs the identical table and skips only the write; `--catalog <name>` restricts the table to one catalog. Pass `--preview` for a non-interactive projection of what an upgrade would do, with `--full` to show up-to-date entries too. The output is colorized in a supporting terminal. For packages that declare a `strategy`, the command also resyncs their materialized peer range, preserving any prerelease identifier rather than rebuilding it from parsed version parts. See [upgrading catalogs](https://github.com/spencerbeggs/rolldown-pnpm-config/blob/main/docs/05-upgrading-catalogs.md) for the full surface.
|
|
85
85
|
|
|
86
86
|
## Exporting to pnpm-workspace.yaml
|
|
87
87
|
|
|
@@ -3,8 +3,8 @@ import { exportCommand } from "../cli/commands/export.js";
|
|
|
3
3
|
import { previewCommand } from "../cli/commands/preview.js";
|
|
4
4
|
import { upgradeCommand } from "../cli/commands/upgrade.js";
|
|
5
5
|
import { Effect } from "effect";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
7
|
+
import { Command } from "effect/unstable/cli";
|
|
8
8
|
|
|
9
9
|
//#region src/cli/bin.ts
|
|
10
10
|
const root = Command.make("rolldown-pnpm-config").pipe(Command.withSubcommands([
|
|
@@ -12,10 +12,7 @@ const root = Command.make("rolldown-pnpm-config").pipe(Command.withSubcommands([
|
|
|
12
12
|
exportCommand,
|
|
13
13
|
previewCommand
|
|
14
14
|
]));
|
|
15
|
-
Command.run(root, {
|
|
16
|
-
name: "rolldown-pnpm-config",
|
|
17
|
-
version: "0.2.2"
|
|
18
|
-
})(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);
|
|
15
|
+
Command.run(root, { version: "0.4.0" }).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
|
|
19
16
|
|
|
20
17
|
//#endregion
|
|
21
18
|
export { };
|
package/cli/commands/export.js
CHANGED
|
@@ -16,7 +16,7 @@ import { overlayWorkspace } from "../workspace-overlay.js";
|
|
|
16
16
|
import { Data, Effect, Option } from "effect";
|
|
17
17
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { dirname, join, relative } from "node:path";
|
|
19
|
-
import {
|
|
19
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
20
20
|
|
|
21
21
|
//#region src/cli/commands/export.ts
|
|
22
22
|
/**
|
|
@@ -110,9 +110,9 @@ function runExport(opts) {
|
|
|
110
110
|
};
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
|
-
const pathArg =
|
|
114
|
-
const dryRunFlag =
|
|
115
|
-
const fullFlag =
|
|
113
|
+
const pathArg = Argument.file("path").pipe(Argument.optional);
|
|
114
|
+
const dryRunFlag = Flag.boolean("dry-run").pipe(Flag.withDefault(false));
|
|
115
|
+
const fullFlag = Flag.boolean("full").pipe(Flag.withDefault(false));
|
|
116
116
|
/**
|
|
117
117
|
* The "export" command. Materializes the plugin config into pnpm-workspace.yaml.
|
|
118
118
|
* An optional path argument overrides the auto-detected workspace file. --dry-run
|
package/cli/commands/preview.js
CHANGED
|
@@ -11,7 +11,7 @@ import { runPreview } from "../ui/run-preview.js";
|
|
|
11
11
|
import { Data, Effect, Option } from "effect";
|
|
12
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
|
-
import {
|
|
14
|
+
import { Argument, Command } from "effect/unstable/cli";
|
|
15
15
|
|
|
16
16
|
//#region src/cli/commands/preview.ts
|
|
17
17
|
/** Typed failure for the preview run. @internal */
|
|
@@ -48,7 +48,7 @@ function runPreviewViews(opts) {
|
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
|
-
const pathArg =
|
|
51
|
+
const pathArg = Argument.file("path").pipe(Argument.optional);
|
|
52
52
|
/**
|
|
53
53
|
* The "preview" command: interactive ink-tab explorer of the export diff
|
|
54
54
|
* (Changes / Full / Simulated). Falls back to printing the Changes view when
|
package/cli/commands/upgrade.js
CHANGED
|
@@ -12,11 +12,12 @@ import { RegistryResolver, RegistryResolverLive } from "../resolve.js";
|
|
|
12
12
|
import { applyEdits } from "../rewrite.js";
|
|
13
13
|
import { renderSummary } from "../summary.js";
|
|
14
14
|
import { runWalk } from "../ui/run-walk.js";
|
|
15
|
+
import { validateEdits } from "../validate.js";
|
|
15
16
|
import { buildWalkItems } from "../walk-plan.js";
|
|
16
|
-
import { Data, Effect, Option } from "effect";
|
|
17
|
+
import { Data, Effect, Option, Result } from "effect";
|
|
17
18
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
19
|
+
import { NodeServices } from "@effect/platform-node";
|
|
20
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
20
21
|
|
|
21
22
|
//#region src/cli/commands/upgrade.ts
|
|
22
23
|
/**
|
|
@@ -28,9 +29,9 @@ var UpgradeError = class extends Data.TaggedError("UpgradeError") {};
|
|
|
28
29
|
/** Combine the config-declared and pnpm-resolved release-age gates (strictest of both). @internal */
|
|
29
30
|
function computeGate(source, file, resolver) {
|
|
30
31
|
return Effect.gen(function* () {
|
|
31
|
-
const { config } = yield* Effect.try(() => evaluatePluginConfig(source, file)).pipe(Effect.
|
|
32
|
+
const { config } = yield* Effect.try(() => evaluatePluginConfig(source, file)).pipe(Effect.catch(() => Effect.succeed({ config: null })));
|
|
32
33
|
const cfg = readConfigReleaseAge(config);
|
|
33
|
-
const [age, exc] = yield* Effect.all([resolver.pnpmConfig("minimumReleaseAge").pipe(Effect.
|
|
34
|
+
const [age, exc] = yield* Effect.all([resolver.pnpmConfig("minimumReleaseAge").pipe(Effect.catch(() => Effect.succeed(null))), resolver.pnpmConfig("minimumReleaseAgeExclude").pipe(Effect.catch(() => Effect.succeed(null)))], { concurrency: "unbounded" });
|
|
34
35
|
return combineReleaseAge(cfg, parsePnpmGate(age, exc));
|
|
35
36
|
});
|
|
36
37
|
}
|
|
@@ -51,15 +52,27 @@ function resolveGatedVersions(entries, resolver, gate, now, onProgress) {
|
|
|
51
52
|
let resolved = 0;
|
|
52
53
|
onProgress?.(0, total);
|
|
53
54
|
return Effect.forEach(uniquePkgs, (pkg) => Effect.gen(function* () {
|
|
54
|
-
const vr = yield* resolver.versions(pkg).pipe(Effect.
|
|
55
|
-
if (vr
|
|
55
|
+
const vr = yield* resolver.versions(pkg).pipe(Effect.result);
|
|
56
|
+
if (Result.isFailure(vr)) {
|
|
56
57
|
onProgress?.(++resolved, total);
|
|
57
|
-
return [
|
|
58
|
+
return [
|
|
59
|
+
pkg,
|
|
60
|
+
[],
|
|
61
|
+
[]
|
|
62
|
+
];
|
|
58
63
|
}
|
|
59
|
-
const times = gate.ageMinutes > 0 ? yield* resolver.times(pkg).pipe(Effect.
|
|
64
|
+
const times = gate.ageMinutes > 0 ? yield* resolver.times(pkg).pipe(Effect.catch(() => Effect.succeed({}))) : {};
|
|
60
65
|
onProgress?.(++resolved, total);
|
|
61
|
-
return [
|
|
62
|
-
|
|
66
|
+
return [
|
|
67
|
+
pkg,
|
|
68
|
+
filterByReleaseAge(vr.success, times, gate, pkg, now),
|
|
69
|
+
vr.success
|
|
70
|
+
];
|
|
71
|
+
}), { concurrency: 12 }).pipe(Effect.map((triples) => ({
|
|
72
|
+
gated: new Map(triples.map(([pkg, gated]) => [pkg, gated])),
|
|
73
|
+
raw: new Map(triples.map(([pkg, , raw]) => [pkg, raw])),
|
|
74
|
+
unresolved: triples.filter(([, , raw]) => raw.length === 0).map(([pkg]) => pkg)
|
|
75
|
+
})));
|
|
63
76
|
}
|
|
64
77
|
/**
|
|
65
78
|
* Write a resolve-progress line to stderr. Overwrites the previous line with
|
|
@@ -85,6 +98,12 @@ function writeResolveProgress(resolved, total) {
|
|
|
85
98
|
* is treated as a skip, except that a strategy entry can still resync or
|
|
86
99
|
* materialize its managed peer offline from the current range.
|
|
87
100
|
*
|
|
101
|
+
* This path runs UNATTENDED (`--yes`, i.e. CI), so it fails hard rather than
|
|
102
|
+
* degrading: any peer-strategy warning, or any planned edit no published
|
|
103
|
+
* version satisfies, aborts the run and writes NOTHING. A warning that scrolls
|
|
104
|
+
* past unread in a CI log is a bad range in a published artifact. The
|
|
105
|
+
* interactive path is deliberately more forgiving (see `upgradeCommand`).
|
|
106
|
+
*
|
|
88
107
|
* @internal
|
|
89
108
|
*/
|
|
90
109
|
function runUpgrade(opts) {
|
|
@@ -99,69 +118,69 @@ function runUpgrade(opts) {
|
|
|
99
118
|
});
|
|
100
119
|
const gate = yield* computeGate(source, opts.file, opts.resolver);
|
|
101
120
|
const versionsByPkg = yield* resolveGatedVersions(entries, opts.resolver, gate, Date.now(), opts.onProgress);
|
|
121
|
+
if (versionsByPkg.unresolved.length > 0) return yield* Effect.fail(new UpgradeError({ message: unresolvedMessage(versionsByPkg.unresolved) }));
|
|
102
122
|
const edits = [];
|
|
123
|
+
const interopEdits = [];
|
|
124
|
+
const warnings = [];
|
|
103
125
|
const changedSpans = /* @__PURE__ */ new Set();
|
|
104
126
|
for (const entry of entries) {
|
|
105
127
|
if (entry.strategy === "interop") continue;
|
|
106
|
-
const versions = versionsByPkg.get(entry.pkg) ?? [];
|
|
128
|
+
const versions = versionsByPkg.gated.get(entry.pkg) ?? [];
|
|
129
|
+
const pkg = entry.pkg;
|
|
130
|
+
const rangeEdit = (span, value) => ({
|
|
131
|
+
span,
|
|
132
|
+
text: JSON.stringify(value),
|
|
133
|
+
pkg,
|
|
134
|
+
kind: "range",
|
|
135
|
+
value
|
|
136
|
+
});
|
|
137
|
+
const peerEdit = (span, value) => ({
|
|
138
|
+
span,
|
|
139
|
+
text: JSON.stringify(value),
|
|
140
|
+
pkg,
|
|
141
|
+
kind: "peer",
|
|
142
|
+
value
|
|
143
|
+
});
|
|
144
|
+
const peerInsert = (at, value) => ({
|
|
145
|
+
span: [at, at],
|
|
146
|
+
text: `, peer: ${JSON.stringify(value)}`,
|
|
147
|
+
pkg,
|
|
148
|
+
kind: "peer",
|
|
149
|
+
value
|
|
150
|
+
});
|
|
151
|
+
const derived = entry.strategy ? yield* derivePeerRange(entry.currentRange, entry.strategy).pipe(Effect.catch(() => Effect.succeed(null))) : null;
|
|
152
|
+
if (derived?.warning) warnings.push(`${entry.pkg}: ${derived.warning.message}`);
|
|
107
153
|
if (versions.length === 0) {
|
|
108
154
|
const at = entry.rangeSpan[1];
|
|
109
155
|
if (entry.peer && entry.strategy) {
|
|
110
|
-
const expected = yield* detectPeerDrift(entry).pipe(Effect.
|
|
156
|
+
const expected = yield* detectPeerDrift(entry).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
111
157
|
if (expected !== null) {
|
|
112
|
-
edits.push(
|
|
113
|
-
span: entry.peer.span,
|
|
114
|
-
text: JSON.stringify(expected)
|
|
115
|
-
});
|
|
116
|
-
changedSpans.add(entry.rangeSpan[0]);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
} else if (!entry.peer && entry.strategy) {
|
|
120
|
-
const peerRange = yield* derivePeerRange(entry.currentRange, entry.strategy).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
121
|
-
if (peerRange !== null) {
|
|
122
|
-
edits.push({
|
|
123
|
-
span: [at, at],
|
|
124
|
-
text: `, peer: ${JSON.stringify(peerRange)}`
|
|
125
|
-
});
|
|
158
|
+
edits.push(peerEdit(entry.peer.span, expected));
|
|
126
159
|
changedSpans.add(entry.rangeSpan[0]);
|
|
127
160
|
continue;
|
|
128
161
|
}
|
|
162
|
+
} else if (!entry.peer && entry.strategy && derived !== null) {
|
|
163
|
+
edits.push(peerInsert(at, derived.range));
|
|
164
|
+
changedSpans.add(entry.rangeSpan[0]);
|
|
165
|
+
continue;
|
|
129
166
|
}
|
|
130
167
|
skipped.push(`${entry.catalog}.${entry.pkg}`);
|
|
131
168
|
continue;
|
|
132
169
|
}
|
|
133
|
-
const inRange = (yield* planEntry(entry, versions).pipe(Effect.
|
|
170
|
+
const inRange = (yield* planEntry(entry, versions).pipe(Effect.catch(() => Effect.succeed([])))).find((c) => c.kind === "in-range");
|
|
134
171
|
const at = entry.rangeSpan[1];
|
|
135
172
|
if (inRange) {
|
|
136
|
-
edits.push(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
173
|
+
edits.push(rangeEdit(entry.rangeSpan, inRange.range));
|
|
174
|
+
changedSpans.add(entry.rangeSpan[0]);
|
|
175
|
+
if (entry.peer && inRange.peerRange) edits.push(peerEdit(entry.peer.span, inRange.peerRange));
|
|
176
|
+
else if (!entry.peer && entry.strategy && inRange.peerRange) edits.push(peerInsert(at, inRange.peerRange));
|
|
177
|
+
} else if (!entry.peer && entry.strategy && derived !== null) {
|
|
178
|
+
edits.push(peerInsert(at, derived.range));
|
|
140
179
|
changedSpans.add(entry.rangeSpan[0]);
|
|
141
|
-
if (entry.peer && inRange.peerRange) edits.push({
|
|
142
|
-
span: entry.peer.span,
|
|
143
|
-
text: JSON.stringify(inRange.peerRange)
|
|
144
|
-
});
|
|
145
|
-
else if (!entry.peer && entry.strategy && inRange.peerRange) edits.push({
|
|
146
|
-
span: [at, at],
|
|
147
|
-
text: `, peer: ${JSON.stringify(inRange.peerRange)}`
|
|
148
|
-
});
|
|
149
|
-
} else if (!entry.peer && entry.strategy) {
|
|
150
|
-
const peerRange = yield* derivePeerRange(entry.currentRange, entry.strategy).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
151
|
-
if (peerRange !== null) {
|
|
152
|
-
edits.push({
|
|
153
|
-
span: [at, at],
|
|
154
|
-
text: `, peer: ${JSON.stringify(peerRange)}`
|
|
155
|
-
});
|
|
156
|
-
changedSpans.add(entry.rangeSpan[0]);
|
|
157
|
-
}
|
|
158
180
|
} else if (entry.peer && entry.strategy) {
|
|
159
|
-
const expected = yield* detectPeerDrift(entry).pipe(Effect.
|
|
181
|
+
const expected = yield* detectPeerDrift(entry).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
160
182
|
if (expected !== null) {
|
|
161
|
-
edits.push(
|
|
162
|
-
span: entry.peer.span,
|
|
163
|
-
text: JSON.stringify(expected)
|
|
164
|
-
});
|
|
183
|
+
edits.push(peerEdit(entry.peer.span, expected));
|
|
165
184
|
changedSpans.add(entry.rangeSpan[0]);
|
|
166
185
|
}
|
|
167
186
|
}
|
|
@@ -177,8 +196,8 @@ function runUpgrade(opts) {
|
|
|
177
196
|
for (const [, group] of byCatalog) {
|
|
178
197
|
const members = [];
|
|
179
198
|
for (const e of group) {
|
|
180
|
-
const versions = versionsByPkg.get(e.pkg) ?? [];
|
|
181
|
-
const inRange = (yield* planEntry(e, versions).pipe(Effect.
|
|
199
|
+
const versions = versionsByPkg.gated.get(e.pkg) ?? [];
|
|
200
|
+
const inRange = (yield* planEntry(e, versions).pipe(Effect.catch(() => Effect.succeed([])))).find((c) => c.kind === "in-range");
|
|
182
201
|
const ceiling = inRange ? inRange.version : e.currentRange.replace(/^[\^~]/, "");
|
|
183
202
|
members.push({
|
|
184
203
|
pkg: e.pkg,
|
|
@@ -187,12 +206,16 @@ function runUpgrade(opts) {
|
|
|
187
206
|
});
|
|
188
207
|
}
|
|
189
208
|
const result = yield* runInterop(members, opts.resolver);
|
|
190
|
-
|
|
209
|
+
interopEdits.push(...buildInteropEdits(group, result));
|
|
191
210
|
for (const e of group) if (interopEntryChanged(e, result)) changedSpans.add(e.rangeSpan[0]);
|
|
192
211
|
conflicts.push(...result.conflicts);
|
|
193
212
|
}
|
|
194
|
-
if (
|
|
195
|
-
|
|
213
|
+
if (warnings.length > 0) return yield* Effect.fail(new UpgradeError({ message: `Refusing to apply with an incompatible peer strategy:\n${warnings.map((w) => ` ${w}`).join("\n")}` }));
|
|
214
|
+
const { accepted, rejected } = yield* validateEdits(edits, versionsByPkg.raw);
|
|
215
|
+
if (rejected.length > 0) return yield* Effect.fail(new UpgradeError({ message: `Refusing to write unsatisfiable range(s):\n${rejected.map((r) => ` ${r.reason}`).join("\n")}` }));
|
|
216
|
+
const allEdits = [...accepted, ...interopEdits];
|
|
217
|
+
if (allEdits.length > 0 && !opts.dryRun) {
|
|
218
|
+
const next = applyEdits(source, allEdits);
|
|
196
219
|
yield* Effect.try({
|
|
197
220
|
try: () => writeFileSync(opts.file, next, "utf8"),
|
|
198
221
|
catch: () => new UpgradeError({ message: `Cannot write ${opts.file}` })
|
|
@@ -201,22 +224,29 @@ function runUpgrade(opts) {
|
|
|
201
224
|
return {
|
|
202
225
|
updated: changedSpans.size,
|
|
203
226
|
skipped,
|
|
204
|
-
conflicts
|
|
227
|
+
conflicts,
|
|
228
|
+
rejected
|
|
205
229
|
};
|
|
206
230
|
});
|
|
207
231
|
}
|
|
232
|
+
/** Count the decisions that actually change the file (a bump, a peer resync, or a materialize). @internal */
|
|
233
|
+
function countChangedDecisions(decisions) {
|
|
234
|
+
return decisions.filter((d) => d.chosen.kind !== "keep" || d.item.entry.peer !== void 0 && d.item.driftPeer !== null || d.item.entry.peer === void 0 && d.item.materializePeer !== null).length;
|
|
235
|
+
}
|
|
208
236
|
/**
|
|
209
|
-
* Apply the interactive result
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
237
|
+
* Apply the interactive result: the (already validated) non-interop edits plus
|
|
238
|
+
* the interop members' separately-computed span edits. Interop members are
|
|
239
|
+
* EXCLUDED from `buildEdits` upstream so the two never emit a range edit over
|
|
240
|
+
* the same span (which `applyEdits` would reject as overlapping).
|
|
241
|
+
*
|
|
242
|
+
* Edits arrive pre-validated so the caller can report what was dropped rather
|
|
243
|
+
* than failing the whole run.
|
|
214
244
|
*
|
|
215
245
|
* @internal
|
|
216
246
|
*/
|
|
217
|
-
function applyInteropAndDecisions(file, source,
|
|
247
|
+
function applyInteropAndDecisions(file, source, nonInteropEdits, interopEdits) {
|
|
218
248
|
return Effect.gen(function* () {
|
|
219
|
-
const edits = [...
|
|
249
|
+
const edits = [...nonInteropEdits, ...interopEdits];
|
|
220
250
|
if (edits.length === 0) return;
|
|
221
251
|
const next = applyEdits(source, edits);
|
|
222
252
|
yield* Effect.try({
|
|
@@ -225,6 +255,40 @@ function applyInteropAndDecisions(file, source, nonInteropDecisions, interopEdit
|
|
|
225
255
|
});
|
|
226
256
|
});
|
|
227
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Filter walk items down to the ones the interactive table should show: a row
|
|
260
|
+
* is actionable when it is anything but up-to-date (a range bump, a peer
|
|
261
|
+
* drift resync, or a peer materialization), unless `--full` asks for every
|
|
262
|
+
* row including inert up-to-date ones. Parity with the old walk's
|
|
263
|
+
* `nextActionable` auto-skip, now applied as an upfront filter instead of a
|
|
264
|
+
* per-step cursor advance.
|
|
265
|
+
*
|
|
266
|
+
* @internal
|
|
267
|
+
*/
|
|
268
|
+
function actionableWalkItems(items, full) {
|
|
269
|
+
return full ? [...items] : items.filter((i) => !i.upToDate);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The message printed instead of entering the interactive table when nothing
|
|
273
|
+
* is actionable: either no catalog packages were discovered at all, or every
|
|
274
|
+
* discovered package is already up to date.
|
|
275
|
+
*
|
|
276
|
+
* @internal
|
|
277
|
+
*/
|
|
278
|
+
function nothingToUpgradeMessage(totalItems) {
|
|
279
|
+
return totalItems === 0 ? "Nothing to upgrade — no catalog packages found.\n" : `Nothing to upgrade — ${totalItems} package(s) already up to date.\n`;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The message for packages the registry could not resolve. Almost always a
|
|
283
|
+
* misspelt name in the config; occasionally a private package the current
|
|
284
|
+
* .npmrc cannot authenticate against.
|
|
285
|
+
*
|
|
286
|
+
* @internal
|
|
287
|
+
*/
|
|
288
|
+
function unresolvedMessage(unresolved) {
|
|
289
|
+
const list = unresolved.map((p) => ` ${p}`).join("\n");
|
|
290
|
+
return `Could not resolve ${unresolved.length} package(s) from the registry — check the name(s) for typos, or your registry auth:\n${list}`;
|
|
291
|
+
}
|
|
228
292
|
/** Project walk items to the non-interactive default decisions (latest-in-range, plus peer-only keeps). @internal */
|
|
229
293
|
function projectDecisions(items, full) {
|
|
230
294
|
const out = [];
|
|
@@ -270,7 +334,8 @@ function runUpgradePreview(opts) {
|
|
|
270
334
|
});
|
|
271
335
|
const gate = yield* computeGate(source, opts.file, opts.resolver);
|
|
272
336
|
const versions = yield* resolveGatedVersions(discovered.entries, opts.resolver, gate, Date.now());
|
|
273
|
-
|
|
337
|
+
const text = renderSummary(projectDecisions(yield* buildWalkItems(discovered.entries, versions.gated).pipe(Effect.catch((e) => Effect.fail(new UpgradeError({ message: e.message })))), opts.full), void 0, { color: opts.color ?? false });
|
|
338
|
+
return versions.unresolved.length > 0 ? `${text}\n⚠ ${unresolvedMessage(versions.unresolved)}` : text;
|
|
274
339
|
});
|
|
275
340
|
}
|
|
276
341
|
/**
|
|
@@ -287,19 +352,17 @@ function resolveTargetFile(fileOpt) {
|
|
|
287
352
|
return picked.file;
|
|
288
353
|
});
|
|
289
354
|
}
|
|
290
|
-
const fileArg =
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
const catalogOption = Options.text("catalog").pipe(Options.optional);
|
|
297
|
-
const previewFlag = Options.boolean("preview").pipe(Options.withDefault(false));
|
|
298
|
-
const fullFlag = Options.boolean("full").pipe(Options.withDefault(false));
|
|
355
|
+
const fileArg = Argument.file("file", { mustExist: true }).pipe(Argument.optional);
|
|
356
|
+
const yesFlag = Flag.boolean("yes").pipe(Flag.withAlias("y"), Flag.withDefault(false));
|
|
357
|
+
const dryRunFlag = Flag.boolean("dry-run").pipe(Flag.withDefault(false));
|
|
358
|
+
const catalogOption = Flag.string("catalog").pipe(Flag.optional);
|
|
359
|
+
const previewFlag = Flag.boolean("preview").pipe(Flag.withDefault(false));
|
|
360
|
+
const fullFlag = Flag.boolean("full").pipe(Flag.withDefault(false));
|
|
299
361
|
/**
|
|
300
|
-
* The "upgrade" command. The default path runs the interactive
|
|
301
|
-
* --yes applies latest-in-range non-interactively; --dry-run
|
|
302
|
-
*
|
|
362
|
+
* The "upgrade" command. The default path runs the interactive table;
|
|
363
|
+
* --yes applies latest-in-range non-interactively; --dry-run runs the identical
|
|
364
|
+
* interactive flow and reports what it would have written, but writes nothing;
|
|
365
|
+
* --catalog restricts to a single catalog by name.
|
|
303
366
|
*
|
|
304
367
|
* @internal
|
|
305
368
|
*/
|
|
@@ -328,9 +391,10 @@ const upgradeCommand = Command.make("upgrade", {
|
|
|
328
391
|
const result = yield* runUpgrade({
|
|
329
392
|
file,
|
|
330
393
|
resolver,
|
|
394
|
+
dryRun,
|
|
331
395
|
...caps.interactive ? { onProgress: writeResolveProgress } : {}
|
|
332
396
|
});
|
|
333
|
-
yield* Effect.sync(() => process.stdout.write(`Updated ${result.updated} package(s); skipped ${result.skipped.length}.\n`));
|
|
397
|
+
yield* Effect.sync(() => process.stdout.write(dryRun ? `Dry run — no changes written. ${result.updated} package(s) would be updated; skipped ${result.skipped.length}.\n` : `Updated ${result.updated} package(s); skipped ${result.skipped.length}.\n`));
|
|
334
398
|
if (result.conflicts.length > 0) {
|
|
335
399
|
const lines = result.conflicts.map((c) => ` ${c.pkg} (kept ${c.ceiling}) blocked by ${c.blockedBy}`).join("\n");
|
|
336
400
|
yield* Effect.sync(() => process.stdout.write(`Interop conflicts (left at your pick):\n${lines}\n`));
|
|
@@ -348,18 +412,21 @@ const upgradeCommand = Command.make("upgrade", {
|
|
|
348
412
|
const catalogName = Option.getOrUndefined(catalog);
|
|
349
413
|
const entries = filterEntriesByCatalog(discovered.entries, catalogName);
|
|
350
414
|
const versions = yield* resolveGatedVersions(entries, resolver, yield* computeGate(source, file, resolver), Date.now(), caps.interactive ? writeResolveProgress : void 0);
|
|
351
|
-
const items = yield* buildWalkItems(entries, versions).pipe(Effect.
|
|
352
|
-
if (dryRun) {
|
|
353
|
-
const decisions = projectDecisions(items, false);
|
|
354
|
-
yield* Effect.sync(() => process.stdout.write(`${renderSummary(decisions, void 0, { color: caps.color })}\n`));
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
415
|
+
const items = yield* buildWalkItems(entries, versions.gated).pipe(Effect.catch((e) => Effect.fail(new UpgradeError({ message: e.message }))));
|
|
357
416
|
if (!caps.interactive) {
|
|
358
417
|
const text = renderSummary(projectDecisions(items, full), void 0, { color: caps.color });
|
|
359
|
-
|
|
418
|
+
const note = dryRun ? "(dry run — nothing written)" : "(non-interactive terminal — run with --yes to apply, or in a TTY to choose)";
|
|
419
|
+
const warn = versions.unresolved.length > 0 ? `\n⚠ ${unresolvedMessage(versions.unresolved)}\n` : "";
|
|
420
|
+
yield* Effect.sync(() => process.stdout.write(`${text}${warn}\n\n${note}\n`));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const actionable = actionableWalkItems(items, full);
|
|
424
|
+
if (actionable.length === 0) {
|
|
425
|
+
const warn = versions.unresolved.length > 0 ? `⚠ ${unresolvedMessage(versions.unresolved)}\n\n` : "";
|
|
426
|
+
yield* Effect.sync(() => process.stdout.write(`${warn}${nothingToUpgradeMessage(items.length)}`));
|
|
360
427
|
return;
|
|
361
428
|
}
|
|
362
|
-
const decisions = yield* runWalk(
|
|
429
|
+
const decisions = yield* runWalk(actionable, dryRun, versions.unresolved);
|
|
363
430
|
const interopByCatalog = /* @__PURE__ */ new Map();
|
|
364
431
|
for (const e of entries) {
|
|
365
432
|
if (e.strategy !== "interop") continue;
|
|
@@ -382,7 +449,7 @@ const upgradeCommand = Command.make("upgrade", {
|
|
|
382
449
|
let members = group.map((e) => ({
|
|
383
450
|
pkg: e.pkg,
|
|
384
451
|
ceiling: pickOf(e.pkg),
|
|
385
|
-
candidates: versions.get(e.pkg) ?? []
|
|
452
|
+
candidates: versions.gated.get(e.pkg) ?? []
|
|
386
453
|
}));
|
|
387
454
|
const originalPick = new Map(members.map((m) => [m.pkg, m.ceiling]));
|
|
388
455
|
const peerCache = /* @__PURE__ */ new Map();
|
|
@@ -393,10 +460,10 @@ const upgradeCommand = Command.make("upgrade", {
|
|
|
393
460
|
const capEntries = group.filter((e) => reentry.some((rc) => rc.pkg === e.pkg));
|
|
394
461
|
const cappedVersions = /* @__PURE__ */ new Map();
|
|
395
462
|
for (const rc of reentry) {
|
|
396
|
-
const all = versions.get(rc.pkg) ?? [];
|
|
463
|
+
const all = versions.gated.get(rc.pkg) ?? [];
|
|
397
464
|
cappedVersions.set(rc.pkg, rc.cap === null ? all : yield* capVersions(all, rc.cap));
|
|
398
465
|
}
|
|
399
|
-
const reDecisions = yield* runWalk(yield* buildWalkItems(capEntries, cappedVersions).pipe(Effect.
|
|
466
|
+
const reDecisions = yield* runWalk(yield* buildWalkItems(capEntries, cappedVersions).pipe(Effect.catch((err) => Effect.fail(new UpgradeError({ message: err.message })))));
|
|
400
467
|
const before = new Map(members.map((m) => [m.pkg, m.ceiling]));
|
|
401
468
|
members = members.map((m) => {
|
|
402
469
|
const rd = reDecisions.find((d) => d.item.entry.pkg === m.pkg);
|
|
@@ -424,14 +491,17 @@ const upgradeCommand = Command.make("upgrade", {
|
|
|
424
491
|
});
|
|
425
492
|
}
|
|
426
493
|
}
|
|
494
|
+
const { accepted, rejected } = yield* validateEdits(buildEdits(nonInteropDecisions), versions.raw);
|
|
495
|
+
const acceptedPkgs = new Set(accepted.map((e) => e.pkg));
|
|
427
496
|
yield* Effect.sync(() => process.stdout.write(`${renderSummary(decisions, {
|
|
428
497
|
adjustments,
|
|
429
498
|
conflicts: allConflicts
|
|
430
|
-
}, { color: caps.color })}\n`));
|
|
431
|
-
yield* applyInteropAndDecisions(file, source,
|
|
432
|
-
const
|
|
433
|
-
yield* Effect.sync(() => process.stdout.write(`
|
|
434
|
-
|
|
499
|
+
}, { color: caps.color }, rejected)}\n`));
|
|
500
|
+
if (!dryRun) yield* applyInteropAndDecisions(file, source, accepted, interopEdits);
|
|
501
|
+
const changed = countChangedDecisions(nonInteropDecisions.filter((d) => acceptedPkgs.has(d.item.entry.pkg))) + interopChanged;
|
|
502
|
+
yield* Effect.sync(() => process.stdout.write(dryRun ? `Dry run — no changes written. ${changed} change(s) would be applied.\n` : `Applied ${changed} change(s).\n`));
|
|
503
|
+
if (versions.unresolved.length > 0) yield* Effect.sync(() => process.stdout.write(`\n⚠ ${unresolvedMessage(versions.unresolved)}\n`));
|
|
504
|
+
}).pipe(Effect.provide(RegistryResolverLive), Effect.provide(NodeServices.layer))).pipe(Command.withDescription("Upgrade catalog versions in a config file"));
|
|
435
505
|
|
|
436
506
|
//#endregion
|
|
437
|
-
export { UpgradeError, applyInteropAndDecisions, computeGate, projectDecisions, resolveGatedVersions, resolveTargetFile, runUpgrade, runUpgradePreview, upgradeCommand, writeResolveProgress };
|
|
507
|
+
export { UpgradeError, actionableWalkItems, applyInteropAndDecisions, computeGate, countChangedDecisions, nothingToUpgradeMessage, projectDecisions, resolveGatedVersions, resolveTargetFile, runUpgrade, runUpgradePreview, unresolvedMessage, upgradeCommand, writeResolveProgress };
|
package/cli/drift.js
CHANGED
|
@@ -8,12 +8,18 @@ import { Effect } from "effect";
|
|
|
8
8
|
* up-to-date peer range) on drift, or null when in sync or not applicable
|
|
9
9
|
* (missing peer or strategy).
|
|
10
10
|
*
|
|
11
|
+
* "interop" is NOT a per-package derivation: an interop peer is computed
|
|
12
|
+
* GROUP-WISE by `interop.ts` from the resolved peerDependencies of the whole
|
|
13
|
+
* catalog group. Deriving one per package here would fall through to the
|
|
14
|
+
* lock-minor branch and report a bogus resync target, so interop entries are
|
|
15
|
+
* excluded — parity with `materializePeer` in walk-plan.ts.
|
|
16
|
+
*
|
|
11
17
|
* @internal
|
|
12
18
|
*/
|
|
13
19
|
function detectPeerDrift(entry) {
|
|
14
20
|
return Effect.gen(function* () {
|
|
15
|
-
if (!entry.peer || !entry.strategy) return null;
|
|
16
|
-
const expected = yield* derivePeerRange(entry.currentRange, entry.strategy);
|
|
21
|
+
if (!entry.peer || !entry.strategy || entry.strategy === "interop") return null;
|
|
22
|
+
const { range: expected } = yield* derivePeerRange(entry.currentRange, entry.strategy);
|
|
17
23
|
return expected === entry.peer.value ? null : expected;
|
|
18
24
|
});
|
|
19
25
|
}
|
package/cli/edits.js
CHANGED
|
@@ -5,34 +5,44 @@
|
|
|
5
5
|
* recomputed peerRange). A keep with peer drift rewrites only the peer literal
|
|
6
6
|
* to the resync target.
|
|
7
7
|
*
|
|
8
|
+
* Each edit is tagged with its package and unquoted range so `validateEdits`
|
|
9
|
+
* can check it against the registry before it is written.
|
|
10
|
+
*
|
|
8
11
|
* @internal
|
|
9
12
|
*/
|
|
10
13
|
function buildEdits(decisions) {
|
|
11
14
|
const edits = [];
|
|
12
15
|
for (const { item, chosen } of decisions) {
|
|
13
16
|
const { entry } = item;
|
|
17
|
+
const pkg = entry.pkg;
|
|
14
18
|
const insertAt = entry.rangeSpan[1];
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
span: entry.peer.span,
|
|
22
|
-
text: JSON.stringify(chosen.peerRange)
|
|
23
|
-
});
|
|
24
|
-
else if (!entry.peer && entry.strategy && chosen.peerRange) edits.push({
|
|
25
|
-
span: [insertAt, insertAt],
|
|
26
|
-
text: `, peer: ${JSON.stringify(chosen.peerRange)}`
|
|
27
|
-
});
|
|
28
|
-
} else if (entry.peer && item.driftPeer) edits.push({
|
|
29
|
-
span: entry.peer.span,
|
|
30
|
-
text: JSON.stringify(item.driftPeer)
|
|
19
|
+
const range = (span, value) => ({
|
|
20
|
+
span,
|
|
21
|
+
text: JSON.stringify(value),
|
|
22
|
+
pkg,
|
|
23
|
+
kind: "range",
|
|
24
|
+
value
|
|
31
25
|
});
|
|
32
|
-
|
|
26
|
+
const peer = (span, value) => ({
|
|
27
|
+
span,
|
|
28
|
+
text: JSON.stringify(value),
|
|
29
|
+
pkg,
|
|
30
|
+
kind: "peer",
|
|
31
|
+
value
|
|
32
|
+
});
|
|
33
|
+
const peerInsert = (value) => ({
|
|
33
34
|
span: [insertAt, insertAt],
|
|
34
|
-
text: `, peer: ${JSON.stringify(
|
|
35
|
+
text: `, peer: ${JSON.stringify(value)}`,
|
|
36
|
+
pkg,
|
|
37
|
+
kind: "peer",
|
|
38
|
+
value
|
|
35
39
|
});
|
|
40
|
+
if (chosen.kind !== "keep") {
|
|
41
|
+
edits.push(range(entry.rangeSpan, chosen.range));
|
|
42
|
+
if (entry.peer && chosen.peerRange) edits.push(peer(entry.peer.span, chosen.peerRange));
|
|
43
|
+
else if (!entry.peer && entry.strategy && chosen.peerRange) edits.push(peerInsert(chosen.peerRange));
|
|
44
|
+
} else if (entry.peer && item.driftPeer) edits.push(peer(entry.peer.span, item.driftPeer));
|
|
45
|
+
else if (!entry.peer && item.materializePeer) edits.push(peerInsert(item.materializePeer));
|
|
36
46
|
}
|
|
37
47
|
return edits;
|
|
38
48
|
}
|