rolldown-pnpm-config 0.1.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/LICENSE +21 -0
- package/README.md +127 -0
- package/bin/rolldown-pnpm-config.js +21 -0
- package/catalogs.js +27 -0
- package/cli/commands/export.js +118 -0
- package/cli/commands/preview.js +73 -0
- package/cli/commands/upgrade.js +437 -0
- package/cli/diff/build.js +122 -0
- package/cli/diff/render.js +103 -0
- package/cli/discover.js +128 -0
- package/cli/drift.js +22 -0
- package/cli/edits.js +41 -0
- package/cli/effective.js +42 -0
- package/cli/evaluate.js +91 -0
- package/cli/interop.js +285 -0
- package/cli/local-merge.js +75 -0
- package/cli/peer-range.js +34 -0
- package/cli/plan.js +66 -0
- package/cli/preview-views.js +34 -0
- package/cli/release-age.js +74 -0
- package/cli/resolve.js +109 -0
- package/cli/rewrite.js +22 -0
- package/cli/select-file.js +64 -0
- package/cli/summary.js +137 -0
- package/cli/ui/Preview.js +60 -0
- package/cli/ui/Walk.js +55 -0
- package/cli/ui/ansi.js +20 -0
- package/cli/ui/env.js +20 -0
- package/cli/ui/run-preview.js +23 -0
- package/cli/ui/run-walk.js +29 -0
- package/cli/ui/styled.js +27 -0
- package/cli/walk-plan.js +35 -0
- package/cli/walk-reducer.js +61 -0
- package/cli/workspace-file.js +58 -0
- package/cli/workspace-overlay.js +21 -0
- package/descriptors/build.js +248 -0
- package/descriptors/hoisting.js +175 -0
- package/descriptors/index.js +38 -0
- package/descriptors/lockfile.js +117 -0
- package/descriptors/misc.js +144 -0
- package/descriptors/network.js +108 -0
- package/descriptors/resolution.js +250 -0
- package/descriptors/runtime-cfg.js +90 -0
- package/descriptors/schemas.js +26 -0
- package/descriptors/workspace.js +116 -0
- package/index.d.ts +363 -0
- package/index.js +3 -0
- package/package.json +60 -0
- package/plugin/freeze.js +79 -0
- package/plugin/index.js +48 -0
- package/plugin/serialize.js +26 -0
- package/registry.js +8 -0
- package/runtime/ctx.js +39 -0
- package/runtime/enforcement.js +36 -0
- package/runtime/strategies/arrays.js +37 -0
- package/runtime/strategies/catalogs.js +36 -0
- package/runtime/strategies/maps.js +46 -0
- package/runtime/strategies/overrides.js +57 -0
- package/runtime/strategies/scalar.js +57 -0
- package/runtime/strategies/table.js +27 -0
- package/runtime/warnings.js +61 -0
- package/runtime.d.ts +111 -0
- package/runtime.js +54 -0
- package/tsdoc-metadata.json +11 -0
- package/virtual.d.ts +18 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { evaluatePluginConfig } from "../evaluate.js";
|
|
2
|
+
import { discoverCatalogEntries } from "../discover.js";
|
|
3
|
+
import { filterEntriesByCatalog, findConfigFiles, pickConfigCandidate } from "../select-file.js";
|
|
4
|
+
import { detectCapabilities } from "../ui/env.js";
|
|
5
|
+
import { derivePeerRange } from "../peer-range.js";
|
|
6
|
+
import { detectPeerDrift } from "../drift.js";
|
|
7
|
+
import { buildEdits } from "../edits.js";
|
|
8
|
+
import { buildInteropEdits, capVersions, interopEntryChanged, reentryCandidates, runInterop } from "../interop.js";
|
|
9
|
+
import { planEntry } from "../plan.js";
|
|
10
|
+
import { combineReleaseAge, filterByReleaseAge, parsePnpmGate, readConfigReleaseAge } from "../release-age.js";
|
|
11
|
+
import { RegistryResolver, RegistryResolverLive } from "../resolve.js";
|
|
12
|
+
import { applyEdits } from "../rewrite.js";
|
|
13
|
+
import { renderSummary } from "../summary.js";
|
|
14
|
+
import { runWalk } from "../ui/run-walk.js";
|
|
15
|
+
import { buildWalkItems } from "../walk-plan.js";
|
|
16
|
+
import { Data, Effect, Option } from "effect";
|
|
17
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
19
|
+
import { NodeContext } from "@effect/platform-node";
|
|
20
|
+
|
|
21
|
+
//#region src/cli/commands/upgrade.ts
|
|
22
|
+
/**
|
|
23
|
+
* Typed failure raised when the upgrade run cannot complete.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
var UpgradeError = class extends Data.TaggedError("UpgradeError") {};
|
|
28
|
+
/** Combine the config-declared and pnpm-resolved release-age gates (strictest of both). @internal */
|
|
29
|
+
function computeGate(source, file, resolver) {
|
|
30
|
+
return Effect.gen(function* () {
|
|
31
|
+
const { config } = yield* Effect.try(() => evaluatePluginConfig(source, file)).pipe(Effect.catchAll(() => Effect.succeed({ config: null })));
|
|
32
|
+
const cfg = readConfigReleaseAge(config);
|
|
33
|
+
const [age, exc] = yield* Effect.all([resolver.pnpmConfig("minimumReleaseAge").pipe(Effect.catchAll(() => Effect.succeed(null))), resolver.pnpmConfig("minimumReleaseAgeExclude").pipe(Effect.catchAll(() => Effect.succeed(null)))], { concurrency: "unbounded" });
|
|
34
|
+
return combineReleaseAge(cfg, parsePnpmGate(age, exc));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** Maximum number of per-package version+times fetches to issue concurrently. @internal */
|
|
38
|
+
const RESOLVE_CONCURRENCY = 12;
|
|
39
|
+
/**
|
|
40
|
+
* Fetch and age-gate the version list for each unique package.
|
|
41
|
+
*
|
|
42
|
+
* @param onProgress - Optional callback invoked after each package resolves with
|
|
43
|
+
* `(resolved, total)`. Useful for emitting CLI progress feedback. Called with
|
|
44
|
+
* `(0, total)` before any work starts so callers can emit the initial banner.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
function resolveGatedVersions(entries, resolver, gate, now, onProgress) {
|
|
49
|
+
const uniquePkgs = [...new Set(entries.map((e) => e.pkg))];
|
|
50
|
+
const total = uniquePkgs.length;
|
|
51
|
+
let resolved = 0;
|
|
52
|
+
onProgress?.(0, total);
|
|
53
|
+
return Effect.forEach(uniquePkgs, (pkg) => Effect.gen(function* () {
|
|
54
|
+
const vr = yield* resolver.versions(pkg).pipe(Effect.either);
|
|
55
|
+
if (vr._tag === "Left") {
|
|
56
|
+
onProgress?.(++resolved, total);
|
|
57
|
+
return [pkg, []];
|
|
58
|
+
}
|
|
59
|
+
const times = gate.ageMinutes > 0 ? yield* resolver.times(pkg).pipe(Effect.catchAll(() => Effect.succeed({}))) : {};
|
|
60
|
+
onProgress?.(++resolved, total);
|
|
61
|
+
return [pkg, filterByReleaseAge(vr.right, times, gate, pkg, now)];
|
|
62
|
+
}), { concurrency: 12 }).pipe(Effect.map((pairs) => new Map(pairs)));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Write a resolve-progress line to stderr. Overwrites the previous line with
|
|
66
|
+
* ANSI carriage return so the terminal shows a single updating counter instead
|
|
67
|
+
* of a flood of lines. The initial call (resolved === 0) writes a newline so
|
|
68
|
+
* the first subsequent overwrite lands on its own line.
|
|
69
|
+
*
|
|
70
|
+
* Only call when caps.interactive is true; this function is not gated itself.
|
|
71
|
+
*
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
function writeResolveProgress(resolved, total) {
|
|
75
|
+
if (resolved === 0) process.stderr.write(`Resolving ${total} package${total === 1 ? "" : "s"}...\n`);
|
|
76
|
+
else if (resolved === total) process.stderr.write(`\r Resolved ${resolved}/${total} \n`);
|
|
77
|
+
else process.stderr.write(`\r Resolved ${resolved}/${total}`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Non-interactive upgrade core: read the config, discover catalog entries,
|
|
81
|
+
* resolve + plan each, build edits for the latest-IN-RANGE candidate (and its
|
|
82
|
+
* recomputed peer literal), and write the file. Never selects a major bump.
|
|
83
|
+
*
|
|
84
|
+
* A package whose version list gates to empty (fetch failure / fully age-gated)
|
|
85
|
+
* is treated as a skip, except that a strategy entry can still resync or
|
|
86
|
+
* materialize its managed peer offline from the current range.
|
|
87
|
+
*
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
function runUpgrade(opts) {
|
|
91
|
+
return Effect.gen(function* () {
|
|
92
|
+
const source = yield* Effect.try({
|
|
93
|
+
try: () => readFileSync(opts.file, "utf8"),
|
|
94
|
+
catch: () => new UpgradeError({ message: `Cannot read ${opts.file}` })
|
|
95
|
+
});
|
|
96
|
+
const { entries, skipped } = yield* Effect.try({
|
|
97
|
+
try: () => discoverCatalogEntries(source, opts.file),
|
|
98
|
+
catch: (e) => new UpgradeError({ message: String(e) })
|
|
99
|
+
});
|
|
100
|
+
const gate = yield* computeGate(source, opts.file, opts.resolver);
|
|
101
|
+
const versionsByPkg = yield* resolveGatedVersions(entries, opts.resolver, gate, Date.now(), opts.onProgress);
|
|
102
|
+
const edits = [];
|
|
103
|
+
const changedSpans = /* @__PURE__ */ new Set();
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (entry.strategy === "interop") continue;
|
|
106
|
+
const versions = versionsByPkg.get(entry.pkg) ?? [];
|
|
107
|
+
if (versions.length === 0) {
|
|
108
|
+
const at = entry.rangeSpan[1];
|
|
109
|
+
if (entry.peer && entry.strategy) {
|
|
110
|
+
const expected = yield* detectPeerDrift(entry).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
111
|
+
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
|
+
});
|
|
126
|
+
changedSpans.add(entry.rangeSpan[0]);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
skipped.push(`${entry.catalog}.${entry.pkg}`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const inRange = (yield* planEntry(entry, versions).pipe(Effect.catchAll(() => Effect.succeed([])))).find((c) => c.kind === "in-range");
|
|
134
|
+
const at = entry.rangeSpan[1];
|
|
135
|
+
if (inRange) {
|
|
136
|
+
edits.push({
|
|
137
|
+
span: entry.rangeSpan,
|
|
138
|
+
text: JSON.stringify(inRange.range)
|
|
139
|
+
});
|
|
140
|
+
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
|
+
} else if (entry.peer && entry.strategy) {
|
|
159
|
+
const expected = yield* detectPeerDrift(entry).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
160
|
+
if (expected !== null) {
|
|
161
|
+
edits.push({
|
|
162
|
+
span: entry.peer.span,
|
|
163
|
+
text: JSON.stringify(expected)
|
|
164
|
+
});
|
|
165
|
+
changedSpans.add(entry.rangeSpan[0]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const interopEntries = entries.filter((e) => e.strategy === "interop");
|
|
170
|
+
const conflicts = [];
|
|
171
|
+
const byCatalog = /* @__PURE__ */ new Map();
|
|
172
|
+
for (const e of interopEntries) {
|
|
173
|
+
const list = byCatalog.get(e.catalog) ?? [];
|
|
174
|
+
list.push(e);
|
|
175
|
+
byCatalog.set(e.catalog, list);
|
|
176
|
+
}
|
|
177
|
+
for (const [, group] of byCatalog) {
|
|
178
|
+
const members = [];
|
|
179
|
+
for (const e of group) {
|
|
180
|
+
const versions = versionsByPkg.get(e.pkg) ?? [];
|
|
181
|
+
const inRange = (yield* planEntry(e, versions).pipe(Effect.catchAll(() => Effect.succeed([])))).find((c) => c.kind === "in-range");
|
|
182
|
+
const ceiling = inRange ? inRange.version : e.currentRange.replace(/^[\^~]/, "");
|
|
183
|
+
members.push({
|
|
184
|
+
pkg: e.pkg,
|
|
185
|
+
ceiling,
|
|
186
|
+
candidates: versions
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const result = yield* runInterop(members, opts.resolver);
|
|
190
|
+
edits.push(...buildInteropEdits(group, result));
|
|
191
|
+
for (const e of group) if (interopEntryChanged(e, result)) changedSpans.add(e.rangeSpan[0]);
|
|
192
|
+
conflicts.push(...result.conflicts);
|
|
193
|
+
}
|
|
194
|
+
if (edits.length > 0) {
|
|
195
|
+
const next = applyEdits(source, edits);
|
|
196
|
+
yield* Effect.try({
|
|
197
|
+
try: () => writeFileSync(opts.file, next, "utf8"),
|
|
198
|
+
catch: () => new UpgradeError({ message: `Cannot write ${opts.file}` })
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
updated: changedSpans.size,
|
|
203
|
+
skipped,
|
|
204
|
+
conflicts
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Apply the interactive result when interop members are present: the
|
|
210
|
+
* non-interop decisions go through `buildEdits`, the interop members through
|
|
211
|
+
* their separately-computed span edits. Interop members are EXCLUDED from
|
|
212
|
+
* `buildEdits` so the two never emit a range edit over the same span (which
|
|
213
|
+
* `applyEdits` would reject as overlapping).
|
|
214
|
+
*
|
|
215
|
+
* @internal
|
|
216
|
+
*/
|
|
217
|
+
function applyInteropAndDecisions(file, source, nonInteropDecisions, interopEdits) {
|
|
218
|
+
return Effect.gen(function* () {
|
|
219
|
+
const edits = [...buildEdits(nonInteropDecisions), ...interopEdits];
|
|
220
|
+
if (edits.length === 0) return;
|
|
221
|
+
const next = applyEdits(source, edits);
|
|
222
|
+
yield* Effect.try({
|
|
223
|
+
try: () => writeFileSync(file, next, "utf8"),
|
|
224
|
+
catch: () => new UpgradeError({ message: `Cannot write ${file}` })
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/** Project walk items to the non-interactive default decisions (latest-in-range, plus peer-only keeps). @internal */
|
|
229
|
+
function projectDecisions(items, full) {
|
|
230
|
+
const out = [];
|
|
231
|
+
for (const i of items) {
|
|
232
|
+
const inRange = i.candidates.find((c) => c.kind === "in-range");
|
|
233
|
+
if (inRange) {
|
|
234
|
+
out.push({
|
|
235
|
+
item: i,
|
|
236
|
+
chosen: inRange
|
|
237
|
+
});
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (i.driftPeer !== null || i.materializePeer !== null) {
|
|
241
|
+
const keep = i.candidates.find((c) => c.kind === "keep");
|
|
242
|
+
if (keep) {
|
|
243
|
+
out.push({
|
|
244
|
+
item: i,
|
|
245
|
+
chosen: keep
|
|
246
|
+
});
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (full) {
|
|
251
|
+
const keep = i.candidates.find((c) => c.kind === "keep");
|
|
252
|
+
if (keep) out.push({
|
|
253
|
+
item: i,
|
|
254
|
+
chosen: keep
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
/** Build the colored preview summary string without writing. @internal */
|
|
261
|
+
function runUpgradePreview(opts) {
|
|
262
|
+
return Effect.gen(function* () {
|
|
263
|
+
const source = yield* Effect.try({
|
|
264
|
+
try: () => readFileSync(opts.file, "utf8"),
|
|
265
|
+
catch: () => new UpgradeError({ message: `Cannot read ${opts.file}` })
|
|
266
|
+
});
|
|
267
|
+
const discovered = yield* Effect.try({
|
|
268
|
+
try: () => discoverCatalogEntries(source, opts.file),
|
|
269
|
+
catch: (e) => new UpgradeError({ message: String(e) })
|
|
270
|
+
});
|
|
271
|
+
const gate = yield* computeGate(source, opts.file, opts.resolver);
|
|
272
|
+
const versions = yield* resolveGatedVersions(discovered.entries, opts.resolver, gate, Date.now());
|
|
273
|
+
return renderSummary(projectDecisions(yield* buildWalkItems(discovered.entries, versions).pipe(Effect.catchAll((e) => Effect.fail(new UpgradeError({ message: e.message })))), opts.full), void 0, { color: opts.color ?? false });
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Resolve the target file: the passed path, or autodetect in cwd.
|
|
278
|
+
*
|
|
279
|
+
* @internal
|
|
280
|
+
*/
|
|
281
|
+
function resolveTargetFile(fileOpt) {
|
|
282
|
+
return Effect.gen(function* () {
|
|
283
|
+
const explicit = Option.getOrUndefined(fileOpt);
|
|
284
|
+
if (explicit !== void 0) return explicit;
|
|
285
|
+
const picked = pickConfigCandidate(yield* findConfigFiles(process.cwd()));
|
|
286
|
+
if (!picked.ok) return yield* Effect.fail(new UpgradeError({ message: picked.message }));
|
|
287
|
+
return picked.file;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
const fileArg = Args.file({
|
|
291
|
+
name: "file",
|
|
292
|
+
exists: "yes"
|
|
293
|
+
}).pipe(Args.optional);
|
|
294
|
+
const yesFlag = Options.boolean("yes").pipe(Options.withAlias("y"), Options.withDefault(false));
|
|
295
|
+
const dryRunFlag = Options.boolean("dry-run").pipe(Options.withDefault(false));
|
|
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));
|
|
299
|
+
/**
|
|
300
|
+
* The "upgrade" command. The default path runs the interactive walk;
|
|
301
|
+
* --yes applies latest-in-range non-interactively; --dry-run prints the
|
|
302
|
+
* summary without writing; --catalog restricts to a single catalog by name.
|
|
303
|
+
*
|
|
304
|
+
* @internal
|
|
305
|
+
*/
|
|
306
|
+
const upgradeCommand = Command.make("upgrade", {
|
|
307
|
+
file: fileArg,
|
|
308
|
+
yes: yesFlag,
|
|
309
|
+
dryRun: dryRunFlag,
|
|
310
|
+
catalog: catalogOption,
|
|
311
|
+
preview: previewFlag,
|
|
312
|
+
full: fullFlag
|
|
313
|
+
}, ({ file: fileOpt, yes, dryRun, catalog, preview, full }) => Effect.gen(function* () {
|
|
314
|
+
const file = yield* resolveTargetFile(fileOpt);
|
|
315
|
+
const resolver = yield* RegistryResolver;
|
|
316
|
+
const caps = detectCapabilities();
|
|
317
|
+
if (preview) {
|
|
318
|
+
const text = yield* runUpgradePreview({
|
|
319
|
+
file,
|
|
320
|
+
resolver,
|
|
321
|
+
full,
|
|
322
|
+
color: caps.color
|
|
323
|
+
});
|
|
324
|
+
yield* Effect.sync(() => process.stdout.write(`${text}\n`));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (yes) {
|
|
328
|
+
const result = yield* runUpgrade({
|
|
329
|
+
file,
|
|
330
|
+
resolver,
|
|
331
|
+
...caps.interactive ? { onProgress: writeResolveProgress } : {}
|
|
332
|
+
});
|
|
333
|
+
yield* Effect.sync(() => process.stdout.write(`Updated ${result.updated} package(s); skipped ${result.skipped.length}.\n`));
|
|
334
|
+
if (result.conflicts.length > 0) {
|
|
335
|
+
const lines = result.conflicts.map((c) => ` ${c.pkg} (kept ${c.ceiling}) blocked by ${c.blockedBy}`).join("\n");
|
|
336
|
+
yield* Effect.sync(() => process.stdout.write(`Interop conflicts (left at your pick):\n${lines}\n`));
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const source = yield* Effect.try({
|
|
341
|
+
try: () => readFileSync(file, "utf8"),
|
|
342
|
+
catch: () => new UpgradeError({ message: `Cannot read ${file}` })
|
|
343
|
+
});
|
|
344
|
+
const discovered = yield* Effect.try({
|
|
345
|
+
try: () => discoverCatalogEntries(source, file),
|
|
346
|
+
catch: (e) => new UpgradeError({ message: String(e) })
|
|
347
|
+
});
|
|
348
|
+
const catalogName = Option.getOrUndefined(catalog);
|
|
349
|
+
const entries = filterEntriesByCatalog(discovered.entries, catalogName);
|
|
350
|
+
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.catchAll((e) => Effect.fail(new UpgradeError({ message: e.message }))));
|
|
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
|
+
}
|
|
357
|
+
if (!caps.interactive) {
|
|
358
|
+
const text = renderSummary(projectDecisions(items, full), void 0, { color: caps.color });
|
|
359
|
+
yield* Effect.sync(() => process.stdout.write(`${text}\n\n(non-interactive terminal — run with --yes to apply, or in a TTY to choose)\n`));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const decisions = yield* runWalk(items);
|
|
363
|
+
const interopByCatalog = /* @__PURE__ */ new Map();
|
|
364
|
+
for (const e of entries) {
|
|
365
|
+
if (e.strategy !== "interop") continue;
|
|
366
|
+
const list = interopByCatalog.get(e.catalog) ?? [];
|
|
367
|
+
list.push(e);
|
|
368
|
+
interopByCatalog.set(e.catalog, list);
|
|
369
|
+
}
|
|
370
|
+
const nonInteropDecisions = decisions.filter((d) => d.item.entry.strategy !== "interop");
|
|
371
|
+
const interopEdits = [];
|
|
372
|
+
const adjustments = [];
|
|
373
|
+
const allConflicts = [];
|
|
374
|
+
let interopChanged = 0;
|
|
375
|
+
for (const [, group] of interopByCatalog) {
|
|
376
|
+
const pickOf = (pkg) => {
|
|
377
|
+
const d = decisions.find((dd) => dd.item.entry.pkg === pkg);
|
|
378
|
+
if (d) return d.chosen.version;
|
|
379
|
+
const ge = group.find((g) => g.pkg === pkg);
|
|
380
|
+
return ge ? ge.currentRange.replace(/^[\^~]/, "") : "";
|
|
381
|
+
};
|
|
382
|
+
let members = group.map((e) => ({
|
|
383
|
+
pkg: e.pkg,
|
|
384
|
+
ceiling: pickOf(e.pkg),
|
|
385
|
+
candidates: versions.get(e.pkg) ?? []
|
|
386
|
+
}));
|
|
387
|
+
const originalPick = new Map(members.map((m) => [m.pkg, m.ceiling]));
|
|
388
|
+
const peerCache = /* @__PURE__ */ new Map();
|
|
389
|
+
let result = yield* runInterop(members, resolver, peerCache);
|
|
390
|
+
for (let round = 0; round < members.length + 1; round++) {
|
|
391
|
+
const reentry = reentryCandidates(members, result);
|
|
392
|
+
if (reentry.length === 0) break;
|
|
393
|
+
const capEntries = group.filter((e) => reentry.some((rc) => rc.pkg === e.pkg));
|
|
394
|
+
const cappedVersions = /* @__PURE__ */ new Map();
|
|
395
|
+
for (const rc of reentry) {
|
|
396
|
+
const all = versions.get(rc.pkg) ?? [];
|
|
397
|
+
cappedVersions.set(rc.pkg, rc.cap === null ? all : yield* capVersions(all, rc.cap));
|
|
398
|
+
}
|
|
399
|
+
const reDecisions = yield* runWalk(yield* buildWalkItems(capEntries, cappedVersions).pipe(Effect.catchAll((err) => Effect.fail(new UpgradeError({ message: err.message })))));
|
|
400
|
+
const before = new Map(members.map((m) => [m.pkg, m.ceiling]));
|
|
401
|
+
members = members.map((m) => {
|
|
402
|
+
const rd = reDecisions.find((d) => d.item.entry.pkg === m.pkg);
|
|
403
|
+
return rd ? {
|
|
404
|
+
...m,
|
|
405
|
+
ceiling: rd.chosen.version
|
|
406
|
+
} : m;
|
|
407
|
+
});
|
|
408
|
+
if (!members.some((m) => before.get(m.pkg) !== m.ceiling)) break;
|
|
409
|
+
result = yield* runInterop(members, resolver, peerCache);
|
|
410
|
+
}
|
|
411
|
+
interopEdits.push(...buildInteropEdits(group, result));
|
|
412
|
+
allConflicts.push(...result.conflicts);
|
|
413
|
+
for (const e of group) {
|
|
414
|
+
if (interopEntryChanged(e, result)) interopChanged++;
|
|
415
|
+
const version = result.resolved.get(e.pkg);
|
|
416
|
+
const original = originalPick.get(e.pkg);
|
|
417
|
+
if (version === void 0 || original === void 0 || version === original) continue;
|
|
418
|
+
adjustments.push({
|
|
419
|
+
catalog: e.catalog,
|
|
420
|
+
pkg: e.pkg,
|
|
421
|
+
from: `${e.operator}${original}`,
|
|
422
|
+
to: `${e.operator}${version}`,
|
|
423
|
+
peer: result.peers.get(e.pkg) ?? `^${version}`
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
yield* Effect.sync(() => process.stdout.write(`${renderSummary(decisions, {
|
|
428
|
+
adjustments,
|
|
429
|
+
conflicts: allConflicts
|
|
430
|
+
}, { color: caps.color })}\n`));
|
|
431
|
+
yield* applyInteropAndDecisions(file, source, nonInteropDecisions, interopEdits);
|
|
432
|
+
const nonInteropChanged = nonInteropDecisions.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;
|
|
433
|
+
yield* Effect.sync(() => process.stdout.write(`Applied ${nonInteropChanged + interopChanged} change(s).\n`));
|
|
434
|
+
}).pipe(Effect.provide(RegistryResolverLive), Effect.provide(NodeContext.layer))).pipe(Command.withDescription("Upgrade catalog versions in a config file"));
|
|
435
|
+
|
|
436
|
+
//#endregion
|
|
437
|
+
export { UpgradeError, applyInteropAndDecisions, computeGate, projectDecisions, resolveGatedVersions, resolveTargetFile, runUpgrade, runUpgradePreview, upgradeCommand, writeResolveProgress };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
//#region src/cli/diff/build.ts
|
|
2
|
+
function isObject(v) {
|
|
3
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
4
|
+
}
|
|
5
|
+
/** Worst kind among children: changed if any differs, added/removed if uniform, else unchanged. */
|
|
6
|
+
function rollup(children) {
|
|
7
|
+
if (children.length === 0) return "unchanged";
|
|
8
|
+
if (children.every((c) => c.kind === "added")) return "added";
|
|
9
|
+
if (children.every((c) => c.kind === "removed")) return "removed";
|
|
10
|
+
return children.some((c) => c.kind !== "unchanged") ? "changed" : "unchanged";
|
|
11
|
+
}
|
|
12
|
+
/** Build every node under a value that exists only on one side (added or removed). */
|
|
13
|
+
function uniform(key, path, value, kind, tag) {
|
|
14
|
+
const here = [...path, key];
|
|
15
|
+
const side = kind === "added" ? { after: value } : { before: value };
|
|
16
|
+
if (isObject(value)) {
|
|
17
|
+
const children = Object.keys(value).map((k) => uniform(k, here, value[k], kind));
|
|
18
|
+
return {
|
|
19
|
+
key,
|
|
20
|
+
path: here,
|
|
21
|
+
kind,
|
|
22
|
+
...tag ? { tag } : {},
|
|
23
|
+
children
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
const children = value.map((el) => ({
|
|
28
|
+
...uniform(String(el), here, el, kind),
|
|
29
|
+
arrayElement: true
|
|
30
|
+
}));
|
|
31
|
+
return {
|
|
32
|
+
key,
|
|
33
|
+
path: here,
|
|
34
|
+
kind,
|
|
35
|
+
...tag ? { tag } : {},
|
|
36
|
+
children
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
key,
|
|
41
|
+
path: here,
|
|
42
|
+
kind,
|
|
43
|
+
...tag ? { tag } : {},
|
|
44
|
+
...side
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Diff two arrays as sets keyed by stringified element. */
|
|
48
|
+
function diffArray(key, path, before, after, tag) {
|
|
49
|
+
const here = [...path, key];
|
|
50
|
+
const b = new Set(before.map(String));
|
|
51
|
+
const a = new Set(after.map(String));
|
|
52
|
+
const children = [.../* @__PURE__ */ new Set([...b, ...a])].sort((x, y) => x.localeCompare(y)).map((el) => {
|
|
53
|
+
const inB = b.has(el);
|
|
54
|
+
const inA = a.has(el);
|
|
55
|
+
const kind = inB && inA ? "unchanged" : inA ? "added" : "removed";
|
|
56
|
+
const side = inA ? { after: el } : { before: el };
|
|
57
|
+
return {
|
|
58
|
+
key: el,
|
|
59
|
+
path: [...here, el],
|
|
60
|
+
kind,
|
|
61
|
+
arrayElement: true,
|
|
62
|
+
...side
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
key,
|
|
67
|
+
path: here,
|
|
68
|
+
kind: rollup(children),
|
|
69
|
+
...tag ? { tag } : {},
|
|
70
|
+
children
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function diffValue(key, path, before, after, tag) {
|
|
74
|
+
const here = [...path, key];
|
|
75
|
+
if (isObject(before) && isObject(after)) {
|
|
76
|
+
const children = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort((x, y) => x.localeCompare(y)).map((k) => {
|
|
77
|
+
if (!(k in before)) return uniform(k, here, after[k], "added");
|
|
78
|
+
if (!(k in after)) return uniform(k, here, before[k], "removed");
|
|
79
|
+
return diffValue(k, here, before[k], after[k]);
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
key,
|
|
83
|
+
path: here,
|
|
84
|
+
kind: rollup(children),
|
|
85
|
+
...tag ? { tag } : {},
|
|
86
|
+
children
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(before) && Array.isArray(after)) return diffArray(key, path, before, after, tag);
|
|
90
|
+
return {
|
|
91
|
+
key,
|
|
92
|
+
path: here,
|
|
93
|
+
kind: JSON.stringify(before) === JSON.stringify(after) ? "unchanged" : "changed",
|
|
94
|
+
...tag ? { tag } : {},
|
|
95
|
+
before,
|
|
96
|
+
after
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Compare two canonicalized workspace objects into a diff tree. Top-level keys
|
|
101
|
+
* carry a `tag`: `local` when the key was sourced from `config.local`,
|
|
102
|
+
* `unmanaged` when the key is not in the plugin-managed set.
|
|
103
|
+
*
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
function buildDiff(before, after, meta) {
|
|
107
|
+
const children = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort((x, y) => x.localeCompare(y)).map((k) => {
|
|
108
|
+
const tag = meta.localKeys.has(k) ? "local" : meta.managedKeys.has(k) ? void 0 : "unmanaged";
|
|
109
|
+
if (!(k in before)) return uniform(k, [], after[k], "added", tag);
|
|
110
|
+
if (!(k in after)) return uniform(k, [], before[k], "removed", tag);
|
|
111
|
+
return diffValue(k, [], before[k], after[k], tag);
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
key: "",
|
|
115
|
+
path: [],
|
|
116
|
+
kind: rollup(children),
|
|
117
|
+
children
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
export { buildDiff };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//#region src/cli/diff/render.ts
|
|
2
|
+
const CONTEXT = 2;
|
|
3
|
+
const GUTTER = {
|
|
4
|
+
added: "+",
|
|
5
|
+
removed: "-",
|
|
6
|
+
changed: "~",
|
|
7
|
+
unchanged: " "
|
|
8
|
+
};
|
|
9
|
+
const STYLE = {
|
|
10
|
+
added: "added",
|
|
11
|
+
removed: "removed",
|
|
12
|
+
changed: "changed",
|
|
13
|
+
unchanged: "unchanged"
|
|
14
|
+
};
|
|
15
|
+
function scalarText(v) {
|
|
16
|
+
return typeof v === "string" ? v : JSON.stringify(v);
|
|
17
|
+
}
|
|
18
|
+
function flatten(node, depth) {
|
|
19
|
+
const indent = depth;
|
|
20
|
+
const gutter = GUTTER[node.kind];
|
|
21
|
+
const style = STYLE[node.kind];
|
|
22
|
+
const tag = node.tag;
|
|
23
|
+
const changed = node.kind !== "unchanged";
|
|
24
|
+
const isArrayEl = node.arrayElement === true;
|
|
25
|
+
if (node.children) return [{
|
|
26
|
+
line: {
|
|
27
|
+
indent,
|
|
28
|
+
gutter,
|
|
29
|
+
segments: [{
|
|
30
|
+
text: `${node.key}:`,
|
|
31
|
+
style
|
|
32
|
+
}],
|
|
33
|
+
...tag ? { tag } : {}
|
|
34
|
+
},
|
|
35
|
+
changed
|
|
36
|
+
}, ...node.children.flatMap((c) => flatten(c, depth + 1))];
|
|
37
|
+
let text;
|
|
38
|
+
if (node.kind === "changed") text = `${node.key}: ${scalarText(node.before)} → ${scalarText(node.after)}`;
|
|
39
|
+
else if (isArrayEl) text = `- ${node.key}`;
|
|
40
|
+
else text = `${node.key}: ${scalarText(node.after ?? node.before)}`;
|
|
41
|
+
return [{
|
|
42
|
+
line: {
|
|
43
|
+
indent,
|
|
44
|
+
gutter,
|
|
45
|
+
segments: [{
|
|
46
|
+
text,
|
|
47
|
+
style
|
|
48
|
+
}],
|
|
49
|
+
...tag ? { tag } : {}
|
|
50
|
+
},
|
|
51
|
+
changed
|
|
52
|
+
}];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Render a diff tree to styled lines in canonical-YAML shape. Default collapses
|
|
56
|
+
* unchanged lines outside a 2-line window around changes into a single
|
|
57
|
+
* "… N unchanged" marker; `full` keeps every line.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
function renderExportDiff(root, opts) {
|
|
62
|
+
const flats = (root.children ?? []).flatMap((c) => flatten(c, 0));
|
|
63
|
+
if (opts.full) return flats.map((f) => f.line);
|
|
64
|
+
const keep = new Array(flats.length).fill(false);
|
|
65
|
+
flats.forEach((f, i) => {
|
|
66
|
+
if (!f.changed) return;
|
|
67
|
+
for (let j = Math.max(0, i - CONTEXT); j <= Math.min(flats.length - 1, i + CONTEXT); j++) keep[j] = true;
|
|
68
|
+
});
|
|
69
|
+
for (let i = 0; i < flats.length; i++) {
|
|
70
|
+
if (!keep[i]) continue;
|
|
71
|
+
let depth = flats[i].line.indent;
|
|
72
|
+
for (let j = i - 1; j >= 0 && depth > 0; j--) if (flats[j].line.indent < depth) {
|
|
73
|
+
keep[j] = true;
|
|
74
|
+
depth = flats[j].line.indent;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const out = [];
|
|
78
|
+
let dropped = 0;
|
|
79
|
+
const flushDropped = () => {
|
|
80
|
+
if (dropped > 0) {
|
|
81
|
+
out.push({
|
|
82
|
+
indent: 0,
|
|
83
|
+
gutter: " ",
|
|
84
|
+
segments: [{
|
|
85
|
+
text: `… ${dropped} unchanged`,
|
|
86
|
+
style: "unchanged"
|
|
87
|
+
}]
|
|
88
|
+
});
|
|
89
|
+
dropped = 0;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
flats.forEach((f, i) => {
|
|
93
|
+
if (keep[i]) {
|
|
94
|
+
flushDropped();
|
|
95
|
+
out.push(f.line);
|
|
96
|
+
} else dropped++;
|
|
97
|
+
});
|
|
98
|
+
flushDropped();
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
export { renderExportDiff };
|