rolldown-pnpm-config 0.4.1 → 0.5.1
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/bin/rolldown-pnpm-config.js +1 -1
- package/catalogs.js +9 -4
- package/cli/commands/export.js +3 -2
- package/cli/commands/preview.js +5 -1
- package/cli/commands/upgrade.js +57 -72
- package/cli/diff/render.js +4 -3
- package/cli/interop-live.js +117 -0
- package/cli/interop.js +1 -49
- package/cli/plan.js +17 -0
- package/cli/preview-views.js +7 -7
- package/cli/release-age.js +7 -34
- package/cli/simulated-view.js +105 -0
- package/cli/ui/Preview.js +16 -14
- package/cli/ui/Walk.js +20 -3
- package/cli/ui/ansi.js +1 -1
- package/cli/ui/legend.js +80 -0
- package/cli/ui/run-preview.js +1 -1
- package/cli/ui/run-walk.js +3 -4
- package/cli/ui/styled.js +14 -5
- package/cli/walk-reducer.js +22 -5
- package/index.d.ts +34 -1
- package/package.json +7 -6
- package/plugin/allowed-versions.js +60 -0
- package/plugin/freeze.js +6 -1
|
@@ -12,7 +12,7 @@ const root = Command.make("rolldown-pnpm-config").pipe(Command.withSubcommands([
|
|
|
12
12
|
exportCommand,
|
|
13
13
|
previewCommand
|
|
14
14
|
]));
|
|
15
|
-
Command.run(root, { version: "0.
|
|
15
|
+
Command.run(root, { version: "0.5.1" }).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
|
|
16
16
|
|
|
17
17
|
//#endregion
|
|
18
18
|
export { };
|
package/catalogs.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Normalize declarative catalog input into the resolved `{ catalog → pkg → range }`
|
|
4
4
|
* map consumed by the runtime. Pure: the base catalog uses each package's
|
|
5
|
-
* `range` (or bare string); a
|
|
6
|
-
* carrying a materialized `peer`, using that value verbatim.
|
|
7
|
-
*
|
|
5
|
+
* `range` (or bare string); a peers catalog is emitted only for packages
|
|
6
|
+
* carrying a materialized `peer`, using that value verbatim. The peers map is
|
|
7
|
+
* emitted under BOTH `<name>Peers` (camelCase, legacy) and `<name>:peers`
|
|
8
|
+
* (colon-delimited, preferred) during this transition; the camelCase form is
|
|
9
|
+
* removed in a later branch. `strategy` is CLI-only and ignored here.
|
|
8
10
|
*
|
|
9
11
|
* @internal
|
|
10
12
|
*/
|
|
@@ -18,7 +20,10 @@ function normalizeCatalogs(input) {
|
|
|
18
20
|
if (typeof spec === "object" && spec.peer !== void 0) peers[pkg] = spec.peer;
|
|
19
21
|
}
|
|
20
22
|
out[name] = base;
|
|
21
|
-
if (Object.keys(peers).length > 0)
|
|
23
|
+
if (Object.keys(peers).length > 0) {
|
|
24
|
+
out[`${name}Peers`] = peers;
|
|
25
|
+
out[`${name}:peers`] = peers;
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
28
|
return out;
|
|
24
29
|
}
|
package/cli/commands/export.js
CHANGED
|
@@ -11,6 +11,7 @@ import { evaluatePluginConfig } from "../evaluate.js";
|
|
|
11
11
|
import { findConfigFiles, pickConfigCandidate } from "../select-file.js";
|
|
12
12
|
import { toAnsi } from "../ui/ansi.js";
|
|
13
13
|
import { detectCapabilities } from "../ui/env.js";
|
|
14
|
+
import { legendLines } from "../ui/legend.js";
|
|
14
15
|
import { canonicalize, findWorkspaceFile, parseWorkspace, renderWorkspace } from "../workspace-file.js";
|
|
15
16
|
import { overlayWorkspace } from "../workspace-overlay.js";
|
|
16
17
|
import { Data, Effect, Option } from "effect";
|
|
@@ -139,8 +140,8 @@ const exportCommand = Command.make("export", {
|
|
|
139
140
|
if (dryRun) {
|
|
140
141
|
const caps = detectCapabilities();
|
|
141
142
|
process.stdout.write(`${result.path} (dry run — not written)\n\n`);
|
|
142
|
-
|
|
143
|
-
process.stdout.write(
|
|
143
|
+
const legend = caps.color ? `${toAnsi(legendLines(), { color: caps.color })}\n\n` : "";
|
|
144
|
+
process.stdout.write(`${legend}${toAnsi(result.diff, { color: caps.color })}\n`);
|
|
144
145
|
} else process.stdout.write(`Exported to ${result.path}\n`);
|
|
145
146
|
for (const k of result.report.staleEntries) process.stderr.write(`warning: patch entry "${k}" has no file on disk\n`);
|
|
146
147
|
for (const k of result.report.keyMismatches) process.stderr.write(`warning: patch entry "${k}" does not match its filename\n`);
|
package/cli/commands/preview.js
CHANGED
|
@@ -4,6 +4,7 @@ import { evaluatePluginConfig } from "../evaluate.js";
|
|
|
4
4
|
import { findConfigFiles, pickConfigCandidate } from "../select-file.js";
|
|
5
5
|
import { toAnsi } from "../ui/ansi.js";
|
|
6
6
|
import { detectCapabilities } from "../ui/env.js";
|
|
7
|
+
import { legendLines } from "../ui/legend.js";
|
|
7
8
|
import { findWorkspaceFile, parseWorkspace } from "../workspace-file.js";
|
|
8
9
|
import { WORKSPACE_FIELDS } from "./export.js";
|
|
9
10
|
import { buildPreviewViews } from "../preview-views.js";
|
|
@@ -66,7 +67,10 @@ const previewCommand = Command.make("preview", { path: pathArg }, ({ path }) =>
|
|
|
66
67
|
});
|
|
67
68
|
const caps = detectCapabilities();
|
|
68
69
|
if (caps.interactive) yield* runPreview(views);
|
|
69
|
-
else yield* Effect.sync(() =>
|
|
70
|
+
else yield* Effect.sync(() => {
|
|
71
|
+
const legend = caps.color ? `${toAnsi(legendLines(), { color: caps.color })}\n\n` : "";
|
|
72
|
+
process.stdout.write(`${legend}${toAnsi(views.changes, { color: caps.color })}\n`);
|
|
73
|
+
});
|
|
70
74
|
})).pipe(Command.withDescription("Interactively preview how pnpm-workspace.yaml would change"));
|
|
71
75
|
|
|
72
76
|
//#endregion
|
package/cli/commands/upgrade.js
CHANGED
|
@@ -5,9 +5,10 @@ import { detectCapabilities } from "../ui/env.js";
|
|
|
5
5
|
import { derivePeerRange } from "../peer-range.js";
|
|
6
6
|
import { detectPeerDrift } from "../drift.js";
|
|
7
7
|
import { buildEdits } from "../edits.js";
|
|
8
|
-
import { buildInteropEdits,
|
|
8
|
+
import { buildInteropEdits, interopEntryChanged, runInterop } from "../interop.js";
|
|
9
|
+
import { buildGroupModel, computeGroupPeers } from "../interop-live.js";
|
|
9
10
|
import { planEntry } from "../plan.js";
|
|
10
|
-
import {
|
|
11
|
+
import { parsePnpmGate, readConfigReleaseAge } from "../release-age.js";
|
|
11
12
|
import { RegistryResolver, RegistryResolverLive } from "../resolve.js";
|
|
12
13
|
import { applyEdits } from "../rewrite.js";
|
|
13
14
|
import { renderSummary } from "../summary.js";
|
|
@@ -18,6 +19,7 @@ import { Data, Effect, Option, Result } from "effect";
|
|
|
18
19
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
19
20
|
import { NodeServices } from "@effect/platform-node";
|
|
20
21
|
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
22
|
+
import { ReleaseAgeGate } from "@effected/npm";
|
|
21
23
|
|
|
22
24
|
//#region src/cli/commands/upgrade.ts
|
|
23
25
|
/**
|
|
@@ -32,7 +34,8 @@ function computeGate(source, file, resolver) {
|
|
|
32
34
|
const { config } = yield* Effect.try(() => evaluatePluginConfig(source, file)).pipe(Effect.catch(() => Effect.succeed({ config: null })));
|
|
33
35
|
const cfg = readConfigReleaseAge(config);
|
|
34
36
|
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" });
|
|
35
|
-
|
|
37
|
+
const contributions = [cfg, parsePnpmGate(age, exc)].filter((g) => g !== null);
|
|
38
|
+
return ReleaseAgeGate.combine(...contributions);
|
|
36
39
|
});
|
|
37
40
|
}
|
|
38
41
|
/** Maximum number of per-package version+times fetches to issue concurrently. @internal */
|
|
@@ -65,7 +68,7 @@ function resolveGatedVersions(entries, resolver, gate, now, onProgress) {
|
|
|
65
68
|
onProgress?.(++resolved, total);
|
|
66
69
|
return [
|
|
67
70
|
pkg,
|
|
68
|
-
|
|
71
|
+
[...gate.filterVersions(vr.success, times, pkg, now)],
|
|
69
72
|
vr.success
|
|
70
73
|
];
|
|
71
74
|
}), { concurrency: 12 }).pipe(Effect.map((triples) => ({
|
|
@@ -256,19 +259,6 @@ function applyInteropAndDecisions(file, source, nonInteropEdits, interopEdits) {
|
|
|
256
259
|
});
|
|
257
260
|
}
|
|
258
261
|
/**
|
|
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
262
|
* The message printed instead of entering the interactive table when nothing
|
|
273
263
|
* is actionable: either no catalog packages were discovered at all, or every
|
|
274
264
|
* discovered package is already up to date.
|
|
@@ -420,13 +410,11 @@ const upgradeCommand = Command.make("upgrade", {
|
|
|
420
410
|
yield* Effect.sync(() => process.stdout.write(`${text}${warn}\n\n${note}\n`));
|
|
421
411
|
return;
|
|
422
412
|
}
|
|
423
|
-
|
|
424
|
-
if (actionable.length === 0) {
|
|
413
|
+
if (items.length === 0) {
|
|
425
414
|
const warn = versions.unresolved.length > 0 ? `⚠ ${unresolvedMessage(versions.unresolved)}\n\n` : "";
|
|
426
|
-
yield* Effect.sync(() => process.stdout.write(`${warn}${nothingToUpgradeMessage(
|
|
415
|
+
yield* Effect.sync(() => process.stdout.write(`${warn}${nothingToUpgradeMessage(0)}`));
|
|
427
416
|
return;
|
|
428
417
|
}
|
|
429
|
-
const decisions = yield* runWalk(actionable, dryRun, versions.unresolved);
|
|
430
418
|
const interopByCatalog = /* @__PURE__ */ new Map();
|
|
431
419
|
for (const e of entries) {
|
|
432
420
|
if (e.strategy !== "interop") continue;
|
|
@@ -434,67 +422,64 @@ const upgradeCommand = Command.make("upgrade", {
|
|
|
434
422
|
list.push(e);
|
|
435
423
|
interopByCatalog.set(e.catalog, list);
|
|
436
424
|
}
|
|
425
|
+
const peerCache = /* @__PURE__ */ new Map();
|
|
426
|
+
const fetchPeer = (pkg, v) => {
|
|
427
|
+
const k = `${pkg}@${v}`;
|
|
428
|
+
const cached = peerCache.get(k);
|
|
429
|
+
if (cached !== void 0) return Effect.succeed(cached);
|
|
430
|
+
return resolver.peerDependencies(pkg, v).pipe(Effect.catch(() => Effect.succeed({})), Effect.map((deps) => {
|
|
431
|
+
peerCache.set(k, deps);
|
|
432
|
+
return deps;
|
|
433
|
+
}));
|
|
434
|
+
};
|
|
435
|
+
if (interopByCatalog.size > 0) yield* Effect.sync(() => process.stderr.write("Resolving peer dependencies…\n"));
|
|
436
|
+
const interopModels = /* @__PURE__ */ new Map();
|
|
437
|
+
for (const [catalog, group] of interopByCatalog) {
|
|
438
|
+
const candByPkg = /* @__PURE__ */ new Map();
|
|
439
|
+
for (const e of group) {
|
|
440
|
+
const it = items.find((i) => i.entry.catalog === catalog && i.entry.pkg === e.pkg);
|
|
441
|
+
candByPkg.set(e.pkg, it ? it.candidates.map((c) => c.version) : [e.currentRange.replace(/^[\^~]/, "")]);
|
|
442
|
+
}
|
|
443
|
+
interopModels.set(catalog, yield* buildGroupModel(candByPkg, fetchPeer));
|
|
444
|
+
}
|
|
445
|
+
const decisions = yield* runWalk(items, dryRun, versions.unresolved, interopModels);
|
|
437
446
|
const nonInteropDecisions = decisions.filter((d) => d.item.entry.strategy !== "interop");
|
|
438
447
|
const interopEdits = [];
|
|
439
|
-
const adjustments = [];
|
|
440
448
|
const allConflicts = [];
|
|
441
449
|
let interopChanged = 0;
|
|
442
|
-
for (const [, group] of interopByCatalog) {
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
let members = group.map((e) => ({
|
|
450
|
-
pkg: e.pkg,
|
|
451
|
-
ceiling: pickOf(e.pkg),
|
|
452
|
-
candidates: versions.gated.get(e.pkg) ?? []
|
|
453
|
-
}));
|
|
454
|
-
const originalPick = new Map(members.map((m) => [m.pkg, m.ceiling]));
|
|
455
|
-
const peerCache = /* @__PURE__ */ new Map();
|
|
456
|
-
let result = yield* runInterop(members, resolver, peerCache);
|
|
457
|
-
for (let round = 0; round < members.length + 1; round++) {
|
|
458
|
-
const reentry = reentryCandidates(members, result);
|
|
459
|
-
if (reentry.length === 0) break;
|
|
460
|
-
const capEntries = group.filter((e) => reentry.some((rc) => rc.pkg === e.pkg));
|
|
461
|
-
const cappedVersions = /* @__PURE__ */ new Map();
|
|
462
|
-
for (const rc of reentry) {
|
|
463
|
-
const all = versions.gated.get(rc.pkg) ?? [];
|
|
464
|
-
cappedVersions.set(rc.pkg, rc.cap === null ? all : yield* capVersions(all, rc.cap));
|
|
465
|
-
}
|
|
466
|
-
const reDecisions = yield* runWalk(yield* buildWalkItems(capEntries, cappedVersions).pipe(Effect.catch((err) => Effect.fail(new UpgradeError({ message: err.message })))));
|
|
467
|
-
const before = new Map(members.map((m) => [m.pkg, m.ceiling]));
|
|
468
|
-
members = members.map((m) => {
|
|
469
|
-
const rd = reDecisions.find((d) => d.item.entry.pkg === m.pkg);
|
|
470
|
-
return rd ? {
|
|
471
|
-
...m,
|
|
472
|
-
ceiling: rd.chosen.version
|
|
473
|
-
} : m;
|
|
474
|
-
});
|
|
475
|
-
if (!members.some((m) => before.get(m.pkg) !== m.ceiling)) break;
|
|
476
|
-
result = yield* runInterop(members, resolver, peerCache);
|
|
450
|
+
for (const [catalog, group] of interopByCatalog) {
|
|
451
|
+
const model = interopModels.get(catalog);
|
|
452
|
+
if (model === void 0) continue;
|
|
453
|
+
const selected = /* @__PURE__ */ new Map();
|
|
454
|
+
for (const e of group) {
|
|
455
|
+
const d = decisions.find((dd) => dd.item.entry.catalog === catalog && dd.item.entry.pkg === e.pkg);
|
|
456
|
+
selected.set(e.pkg, d ? d.chosen.version : e.currentRange.replace(/^[\^~]/, ""));
|
|
477
457
|
}
|
|
478
|
-
|
|
479
|
-
|
|
458
|
+
const { peer, conflict } = computeGroupPeers(model, selected);
|
|
459
|
+
interopEdits.push(...buildInteropEdits(group, {
|
|
460
|
+
resolved: selected,
|
|
461
|
+
peers: peer,
|
|
462
|
+
conflicts: [],
|
|
463
|
+
peerDepsOf: () => ({})
|
|
464
|
+
}));
|
|
465
|
+
for (const [pkg, blockedBy] of conflict) allConflicts.push({
|
|
466
|
+
pkg,
|
|
467
|
+
ceiling: selected.get(pkg) ?? "",
|
|
468
|
+
blockedBy
|
|
469
|
+
});
|
|
480
470
|
for (const e of group) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
pkg: e.pkg,
|
|
488
|
-
from: `${e.operator}${original}`,
|
|
489
|
-
to: `${e.operator}${version}`,
|
|
490
|
-
peer: result.peers.get(e.pkg) ?? `^${version}`
|
|
491
|
-
});
|
|
471
|
+
const version = selected.get(e.pkg);
|
|
472
|
+
if (version === void 0) continue;
|
|
473
|
+
const rangeChanged = `${e.operator}${version}` !== e.currentRange;
|
|
474
|
+
const newPeer = peer.get(e.pkg);
|
|
475
|
+
const peerChanged = newPeer !== void 0 && (e.peer ? newPeer !== e.peer.value : true);
|
|
476
|
+
if (rangeChanged || peerChanged) interopChanged++;
|
|
492
477
|
}
|
|
493
478
|
}
|
|
494
479
|
const { accepted, rejected } = yield* validateEdits(buildEdits(nonInteropDecisions), versions.raw);
|
|
495
480
|
const acceptedPkgs = new Set(accepted.map((e) => e.pkg));
|
|
496
481
|
yield* Effect.sync(() => process.stdout.write(`${renderSummary(decisions, {
|
|
497
|
-
adjustments,
|
|
482
|
+
adjustments: [],
|
|
498
483
|
conflicts: allConflicts
|
|
499
484
|
}, { color: caps.color }, rejected)}\n`));
|
|
500
485
|
if (!dryRun) yield* applyInteropAndDecisions(file, source, accepted, interopEdits);
|
|
@@ -504,4 +489,4 @@ const upgradeCommand = Command.make("upgrade", {
|
|
|
504
489
|
}).pipe(Effect.provide(RegistryResolverLive), Effect.provide(NodeServices.layer))).pipe(Command.withDescription("Upgrade catalog versions in a config file"));
|
|
505
490
|
|
|
506
491
|
//#endregion
|
|
507
|
-
export { UpgradeError,
|
|
492
|
+
export { UpgradeError, applyInteropAndDecisions, computeGate, countChangedDecisions, nothingToUpgradeMessage, projectDecisions, resolveGatedVersions, resolveTargetFile, runUpgrade, runUpgradePreview, unresolvedMessage, upgradeCommand, writeResolveProgress };
|
package/cli/diff/render.js
CHANGED
|
@@ -15,12 +15,13 @@ const STYLE = {
|
|
|
15
15
|
function scalarText(v) {
|
|
16
16
|
return typeof v === "string" ? v : JSON.stringify(v);
|
|
17
17
|
}
|
|
18
|
-
function flatten(node, depth) {
|
|
18
|
+
function flatten(node, depth, inheritedUnmanaged = false) {
|
|
19
19
|
const indent = depth;
|
|
20
20
|
const gutter = GUTTER[node.kind];
|
|
21
|
-
const style = STYLE[node.kind];
|
|
22
21
|
const tag = node.tag;
|
|
23
22
|
const changed = node.kind !== "unchanged";
|
|
23
|
+
const unmanaged = inheritedUnmanaged || node.tag === "unmanaged" && node.kind === "unchanged";
|
|
24
|
+
const style = node.kind === "unchanged" && unmanaged ? "unmanaged" : STYLE[node.kind];
|
|
24
25
|
const isArrayEl = node.arrayElement === true;
|
|
25
26
|
if (node.children) return [{
|
|
26
27
|
line: {
|
|
@@ -33,7 +34,7 @@ function flatten(node, depth) {
|
|
|
33
34
|
...tag ? { tag } : {}
|
|
34
35
|
},
|
|
35
36
|
changed
|
|
36
|
-
}, ...node.children.flatMap((c) => flatten(c, depth + 1))];
|
|
37
|
+
}, ...node.children.flatMap((c) => flatten(c, depth + 1, unmanaged))];
|
|
37
38
|
let text;
|
|
38
39
|
if (node.kind === "changed") text = `${node.key}: ${scalarText(node.before)} → ${scalarText(node.after)}`;
|
|
39
40
|
else if (isArrayEl) text = `- ${node.key}`;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import "./interop.js";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { Range, SemVer } from "@effected/semver";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/interop-live.ts
|
|
6
|
+
/** Strip a range operator to its bare version digits (e.g. `^3.17.0` → `3.17.0`). */
|
|
7
|
+
function floorOf(range) {
|
|
8
|
+
return range.replace(/^[\^~>=\s]+/, "").split(/\s/)[0] ?? range;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Recompute each member's peer floor and conflict for the current selection —
|
|
12
|
+
* synchronously, from a pre-built {@link GroupModel}. Mirrors `deriveFloors`
|
|
13
|
+
* (peer floor is the LOWEST floor any in-group member declares for a package,
|
|
14
|
+
* else `^<its selected version>`) and `violations` (a member conflicts when its
|
|
15
|
+
* selected version's in-group peer requirement is not satisfied by the current
|
|
16
|
+
* pick of that dep). Never changes a selection — detection only.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
function computeGroupPeers(model, selected) {
|
|
21
|
+
const declaredFloors = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const m of model.members) {
|
|
23
|
+
const v = selected.get(m);
|
|
24
|
+
if (v === void 0) continue;
|
|
25
|
+
for (const { dep, range } of model.peerReqs.get(`${m}@${v}`) ?? []) {
|
|
26
|
+
const list = declaredFloors.get(dep) ?? [];
|
|
27
|
+
list.push(floorOf(range));
|
|
28
|
+
declaredFloors.set(dep, list);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const peer = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const m of model.members) {
|
|
33
|
+
const v = selected.get(m);
|
|
34
|
+
if (v === void 0) continue;
|
|
35
|
+
const declared = declaredFloors.get(m);
|
|
36
|
+
if (declared?.length) {
|
|
37
|
+
let minStr = declared[0];
|
|
38
|
+
let minSv = model.ver.get(minStr) ?? null;
|
|
39
|
+
for (const f of declared.slice(1)) {
|
|
40
|
+
const sv = model.ver.get(f) ?? null;
|
|
41
|
+
if (sv && (minSv === null || sv.compare(minSv) < 0)) {
|
|
42
|
+
minSv = sv;
|
|
43
|
+
minStr = f;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
peer.set(m, `^${minStr}`);
|
|
47
|
+
} else peer.set(m, `^${v}`);
|
|
48
|
+
}
|
|
49
|
+
const conflict = /* @__PURE__ */ new Map();
|
|
50
|
+
for (const m of model.members) {
|
|
51
|
+
const v = selected.get(m);
|
|
52
|
+
if (v === void 0) continue;
|
|
53
|
+
const bad = [];
|
|
54
|
+
for (const { dep, range } of model.peerReqs.get(`${m}@${v}`) ?? []) {
|
|
55
|
+
const dv = selected.get(dep);
|
|
56
|
+
if (dv === void 0) continue;
|
|
57
|
+
const r = model.rng.get(range) ?? null;
|
|
58
|
+
const sv = model.ver.get(dv) ?? null;
|
|
59
|
+
if (!(r && sv && r.test(sv))) bad.push(`${dep} ${range}`);
|
|
60
|
+
}
|
|
61
|
+
if (bad.length) conflict.set(m, bad.join(", "));
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
peer,
|
|
65
|
+
conflict
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build a {@link GroupModel} for one interop catalog group: fetch the
|
|
70
|
+
* peerDependencies of every member's candidate version (in-group deps only) via
|
|
71
|
+
* the shared memoized `fetchPeer`, then pre-parse every version and range string
|
|
72
|
+
* so the table can recompute floors/conflicts without further Effects.
|
|
73
|
+
*
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
function buildGroupModel(candidatesByPkg, fetchPeer) {
|
|
77
|
+
return Effect.gen(function* () {
|
|
78
|
+
const members = new Set(candidatesByPkg.keys());
|
|
79
|
+
const peerReqs = /* @__PURE__ */ new Map();
|
|
80
|
+
const verStrings = /* @__PURE__ */ new Set();
|
|
81
|
+
const rngStrings = /* @__PURE__ */ new Set();
|
|
82
|
+
const pairs = [];
|
|
83
|
+
for (const [pkg, versions] of candidatesByPkg) for (const v of versions) pairs.push([pkg, v]);
|
|
84
|
+
const fetched = yield* Effect.forEach(pairs, ([pkg, v]) => fetchPeer(pkg, v).pipe(Effect.map((deps) => [
|
|
85
|
+
pkg,
|
|
86
|
+
v,
|
|
87
|
+
deps
|
|
88
|
+
])), { concurrency: 8 });
|
|
89
|
+
for (const [pkg, v, deps] of fetched) {
|
|
90
|
+
verStrings.add(v);
|
|
91
|
+
const reqs = [];
|
|
92
|
+
for (const [dep, range] of Object.entries(deps)) {
|
|
93
|
+
if (!members.has(dep)) continue;
|
|
94
|
+
reqs.push({
|
|
95
|
+
dep,
|
|
96
|
+
range
|
|
97
|
+
});
|
|
98
|
+
rngStrings.add(range);
|
|
99
|
+
verStrings.add(floorOf(range));
|
|
100
|
+
}
|
|
101
|
+
peerReqs.set(`${pkg}@${v}`, reqs);
|
|
102
|
+
}
|
|
103
|
+
const ver = /* @__PURE__ */ new Map();
|
|
104
|
+
for (const s of verStrings) ver.set(s, yield* SemVer.parse(s).pipe(Effect.catch(() => Effect.succeed(null))));
|
|
105
|
+
const rng = /* @__PURE__ */ new Map();
|
|
106
|
+
for (const s of rngStrings) rng.set(s, yield* Range.parse(s).pipe(Effect.catch(() => Effect.succeed(null))));
|
|
107
|
+
return {
|
|
108
|
+
members,
|
|
109
|
+
peerReqs,
|
|
110
|
+
ver,
|
|
111
|
+
rng
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
export { buildGroupModel, computeGroupPeers };
|
package/cli/interop.js
CHANGED
|
@@ -190,35 +190,6 @@ function runInterop(members, resolver, cache = /* @__PURE__ */ new Map()) {
|
|
|
190
190
|
};
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
|
-
/**
|
|
194
|
-
* The members to re-prompt in the interactive re-entry: each downgraded or
|
|
195
|
-
* conflicted dependent (capped at its resolved version) PLUS the in-group peer
|
|
196
|
-
* targets those dependents depend on (uncapped, so the user can RAISE the anchor
|
|
197
|
-
* instead of accepting the downgrade). `cap` is the version to cap candidates at,
|
|
198
|
-
* or null for an uncapped anchor.
|
|
199
|
-
*
|
|
200
|
-
* @internal
|
|
201
|
-
*/
|
|
202
|
-
function reentryCandidates(members, result) {
|
|
203
|
-
const memberSet = new Set(members.map((m) => m.pkg));
|
|
204
|
-
const byPkg = new Map(members.map((m) => [m.pkg, m]));
|
|
205
|
-
const conflicted = new Set(result.conflicts.map((c) => c.pkg));
|
|
206
|
-
const out = /* @__PURE__ */ new Map();
|
|
207
|
-
for (const m of members) {
|
|
208
|
-
const r = result.resolved.get(m.pkg);
|
|
209
|
-
if (r === void 0) continue;
|
|
210
|
-
if (r !== m.ceiling || conflicted.has(m.pkg)) out.set(m.pkg, r);
|
|
211
|
-
}
|
|
212
|
-
for (const pkg of [...out.keys()]) {
|
|
213
|
-
const m = byPkg.get(pkg);
|
|
214
|
-
if (!m) continue;
|
|
215
|
-
for (const dep of Object.keys(result.peerDepsOf(pkg, m.ceiling))) if (memberSet.has(dep) && !out.has(dep)) out.set(dep, null);
|
|
216
|
-
}
|
|
217
|
-
return [...out].map(([pkg, cap]) => ({
|
|
218
|
-
pkg,
|
|
219
|
-
cap
|
|
220
|
-
}));
|
|
221
|
-
}
|
|
222
193
|
/** True when an interop member's resolved version/peer differs from what's in source. @internal */
|
|
223
194
|
function interopEntryChanged(e, result) {
|
|
224
195
|
const version = result.resolved.get(e.pkg);
|
|
@@ -261,25 +232,6 @@ function buildInteropEdits(entries, result) {
|
|
|
261
232
|
}
|
|
262
233
|
return edits;
|
|
263
234
|
}
|
|
264
|
-
/**
|
|
265
|
-
* Keep only the versions less than or equal to `max` (SemVer comparison). Used
|
|
266
|
-
* to cap the candidate list of a re-prompted interop member. An unparseable
|
|
267
|
-
* `max` leaves the list unchanged.
|
|
268
|
-
*
|
|
269
|
-
* @internal
|
|
270
|
-
*/
|
|
271
|
-
function capVersions(list, max) {
|
|
272
|
-
return Effect.gen(function* () {
|
|
273
|
-
const mv = yield* SemVer.parse(max).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
274
|
-
if (!mv) return [...list];
|
|
275
|
-
const out = [];
|
|
276
|
-
for (const v of list) {
|
|
277
|
-
const sv = yield* SemVer.parse(v).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
278
|
-
if (sv && sv.compare(mv) <= 0) out.push(v);
|
|
279
|
-
}
|
|
280
|
-
return out;
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
235
|
|
|
284
236
|
//#endregion
|
|
285
|
-
export { buildInteropEdits,
|
|
237
|
+
export { buildInteropEdits, deriveFloors, interopEntryChanged, resolveGroup, runInterop };
|
package/cli/plan.js
CHANGED
|
@@ -30,6 +30,7 @@ function planEntry(entry, versions) {
|
|
|
30
30
|
parsed.sort((a, b) => a.compare(b));
|
|
31
31
|
const maxOf = (list) => list.length ? list[list.length - 1] : null;
|
|
32
32
|
const inRangeMax = range ? maxOf(parsed.filter((v) => range.test(v))) : null;
|
|
33
|
+
const sameMajorMax = maxOf(parsed.filter((v) => v.major === currentMajor));
|
|
33
34
|
const overallMax = maxOf(parsed);
|
|
34
35
|
const withPeer = (version) => entry.strategy && entry.strategy !== "interop" ? derivePeerRange(`${entry.operator}${version}`, entry.strategy).pipe(Effect.map((d) => d.range)) : Effect.succeed(void 0);
|
|
35
36
|
const candidates = [];
|
|
@@ -44,6 +45,22 @@ function planEntry(entry, versions) {
|
|
|
44
45
|
...peerRange ? { peerRange } : {}
|
|
45
46
|
});
|
|
46
47
|
}
|
|
48
|
+
if (sameMajorMax !== null) {
|
|
49
|
+
const beatsCurrent = current === null || sameMajorMax.gt(current);
|
|
50
|
+
const beatsInRange = inRangeMax === null || sameMajorMax.gt(inRangeMax);
|
|
51
|
+
const belowOverall = overallMax?.gt(sameMajorMax) ?? false;
|
|
52
|
+
if (beatsCurrent && beatsInRange && belowOverall) {
|
|
53
|
+
const version = sameMajorMax.toString();
|
|
54
|
+
const peerRange = yield* withPeer(version);
|
|
55
|
+
candidates.push({
|
|
56
|
+
kind: "minor",
|
|
57
|
+
range: `${entry.operator}${version}`,
|
|
58
|
+
version,
|
|
59
|
+
isMajor: sameMajorMax.major > currentMajor,
|
|
60
|
+
...peerRange ? { peerRange } : {}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
47
64
|
if (overallMax && (current === null || overallMax.gt(current)) && (!inRangeMax || overallMax.gt(inRangeMax))) {
|
|
48
65
|
const version = overallMax.toString();
|
|
49
66
|
const peerRange = yield* withPeer(version);
|
package/cli/preview-views.js
CHANGED
|
@@ -4,12 +4,14 @@ import { effectiveManaged, vanillaManaged } from "./effective.js";
|
|
|
4
4
|
import { canonicalize } from "./workspace-file.js";
|
|
5
5
|
import { overlayWorkspace } from "./workspace-overlay.js";
|
|
6
6
|
import { WORKSPACE_FIELDS } from "./commands/export.js";
|
|
7
|
+
import { renderSimulated } from "./simulated-view.js";
|
|
7
8
|
|
|
8
9
|
//#region src/cli/preview-views.ts
|
|
9
10
|
/**
|
|
10
|
-
* Build the three preview views as styled lines: Changes (parsed→merged,
|
|
11
|
-
* local + preserve + excludeByRepo), Full (same tree, full verbosity), and
|
|
12
|
-
* Simulated (
|
|
11
|
+
* Build the three preview views as styled lines: Changes (parsed→merged diff,
|
|
12
|
+
* with local + preserve + excludeByRepo), Full (same tree, full verbosity), and
|
|
13
|
+
* Simulated (the fresh-consumer output rendered as a plain calculated file — not
|
|
14
|
+
* a diff — with per-field merge/overwrite + enforcement annotations).
|
|
13
15
|
*
|
|
14
16
|
* @internal
|
|
15
17
|
*/
|
|
@@ -20,13 +22,11 @@ function buildPreviewViews(input) {
|
|
|
20
22
|
};
|
|
21
23
|
const merged = overlayWorkspace(effectiveManaged(input.managed, input.local, input.parsed, input.manifest, input.rootName), input.parsed);
|
|
22
24
|
const vanilla = vanillaManaged(input.managed, input.manifest, input.rootName);
|
|
23
|
-
const
|
|
24
|
-
const changesTree = buildDiff(before, canonicalize(merged), meta);
|
|
25
|
-
const simulatedTree = buildDiff(before, canonicalize(vanilla), meta);
|
|
25
|
+
const changesTree = buildDiff(canonicalize(input.parsed), canonicalize(merged), meta);
|
|
26
26
|
return {
|
|
27
27
|
changes: renderExportDiff(changesTree, { full: false }),
|
|
28
28
|
full: renderExportDiff(changesTree, { full: true }),
|
|
29
|
-
simulated:
|
|
29
|
+
simulated: renderSimulated(vanilla, input.manifest)
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
package/cli/release-age.js
CHANGED
|
@@ -1,38 +1,11 @@
|
|
|
1
1
|
//#region src/cli/release-age.ts
|
|
2
|
-
/** Combine two sources: strictest age (max), widest exempt set (union). @internal */
|
|
3
|
-
function combineReleaseAge(a, b) {
|
|
4
|
-
const ages = [a?.ageMinutes, b?.ageMinutes].filter((n) => typeof n === "number");
|
|
5
|
-
return {
|
|
6
|
-
ageMinutes: ages.length ? Math.max(0, ...ages) : 0,
|
|
7
|
-
exclude: [.../* @__PURE__ */ new Set([...a?.exclude ?? [], ...b?.exclude ?? []])]
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
/** Match a package name against exact names or `*`-globs (e.g. `@effect/*`). @internal */
|
|
11
|
-
function matchesExclude(pkg, patterns) {
|
|
12
|
-
for (const pat of patterns) {
|
|
13
|
-
if (pat === pkg) return true;
|
|
14
|
-
if (pat.includes("*")) {
|
|
15
|
-
if (new RegExp(`^${pat.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`).test(pkg)) return true;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
2
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
3
|
+
* Read a release-age gate from config and pnpm and combine it with
|
|
4
|
+
* {@link @effected/npm#ReleaseAgeGate.combine}. The gate model itself
|
|
5
|
+
* (`ReleaseAgeGate`, `PartialReleaseAgeGate`, the age filter, and the
|
|
6
|
+
* `@pnpm/matcher` exclude semantics) lives in `@effected/npm`; this module
|
|
7
|
+
* only turns this repo's two config sources into partial contributions.
|
|
25
8
|
*/
|
|
26
|
-
function filterByReleaseAge(versions, times, gate, pkg, now) {
|
|
27
|
-
if (gate.ageMinutes <= 0 || matchesExclude(pkg, gate.exclude)) return [...versions];
|
|
28
|
-
const cutoff = now - gate.ageMinutes * 6e4;
|
|
29
|
-
return versions.filter((v) => {
|
|
30
|
-
const t = times[v];
|
|
31
|
-
if (t === void 0) return false;
|
|
32
|
-
const published = Date.parse(t);
|
|
33
|
-
return Number.isFinite(published) && published <= cutoff;
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
9
|
/** Unwrap a managed field that may be a bare value or a `{ value, enforcement }` FieldInput. */
|
|
37
10
|
function fieldValue(raw) {
|
|
38
11
|
if (raw && typeof raw === "object" && !Array.isArray(raw) && "value" in raw) return raw.value;
|
|
@@ -48,7 +21,7 @@ function readConfigReleaseAge(config) {
|
|
|
48
21
|
if (Array.isArray(exc)) out.exclude = exc.filter((x) => typeof x === "string");
|
|
49
22
|
return out.ageMinutes === void 0 && out.exclude === void 0 ? null : out;
|
|
50
23
|
}
|
|
51
|
-
/** Parse `pnpm config get minimumReleaseAge[Exclude]` stdout into a
|
|
24
|
+
/** Parse `pnpm config get minimumReleaseAge[Exclude]` stdout into a PartialReleaseAgeGate. @internal */
|
|
52
25
|
function parsePnpmGate(age, exclude) {
|
|
53
26
|
const out = {};
|
|
54
27
|
const trimmedAge = age?.trim();
|
|
@@ -71,4 +44,4 @@ function parsePnpmGate(age, exclude) {
|
|
|
71
44
|
}
|
|
72
45
|
|
|
73
46
|
//#endregion
|
|
74
|
-
export {
|
|
47
|
+
export { parsePnpmGate, readConfigReleaseAge };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { canonicalize } from "./workspace-file.js";
|
|
2
|
+
|
|
3
|
+
//#region src/cli/simulated-view.ts
|
|
4
|
+
/**
|
|
5
|
+
* How each merge strategy combines a managed field with the consumer's local
|
|
6
|
+
* value, in the vocabulary the Simulated view shows: `merge` (values combined)
|
|
7
|
+
* or `overwrite` (the plugin value replaces the local one). Keyed by manifest
|
|
8
|
+
* strategy name; a new strategy defaults to `merge` and should be classified here.
|
|
9
|
+
*/
|
|
10
|
+
const STRATEGY_VERB = {
|
|
11
|
+
scalar: "overwrite",
|
|
12
|
+
securityFlag: "overwrite",
|
|
13
|
+
securityMin: "overwrite",
|
|
14
|
+
catalogs: "merge",
|
|
15
|
+
mapChildWins: "merge",
|
|
16
|
+
arrayUnion: "merge",
|
|
17
|
+
arrayRecordUnion: "merge",
|
|
18
|
+
overrides: "merge",
|
|
19
|
+
peerDependencyRules: "merge",
|
|
20
|
+
allowBuilds: "merge"
|
|
21
|
+
};
|
|
22
|
+
/** The enforcement suffix, e.g. " · warn" / " · error"; empty when silent. */
|
|
23
|
+
function enforcementSegs(e) {
|
|
24
|
+
if (e === "warn") return [{
|
|
25
|
+
text: " · warn",
|
|
26
|
+
style: "changed"
|
|
27
|
+
}];
|
|
28
|
+
if (e === "error") return [{
|
|
29
|
+
text: " · error",
|
|
30
|
+
style: "warn"
|
|
31
|
+
}];
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
/** The trailing "(merge)" / "(overwrite · error)" annotation for one field. */
|
|
35
|
+
function annotation(entry) {
|
|
36
|
+
if (!entry) return [];
|
|
37
|
+
const verb = STRATEGY_VERB[entry.strategy] ?? "merge";
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
text: " (",
|
|
41
|
+
style: "unchanged"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
text: verb,
|
|
45
|
+
style: verb
|
|
46
|
+
},
|
|
47
|
+
...enforcementSegs(entry.enforcement),
|
|
48
|
+
{
|
|
49
|
+
text: ")",
|
|
50
|
+
style: "unchanged"
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
function scalarText(v) {
|
|
55
|
+
return typeof v === "string" ? v : JSON.stringify(v);
|
|
56
|
+
}
|
|
57
|
+
/** Flatten one key/value into YAML-shaped plain lines; `ann` annotates the head. */
|
|
58
|
+
function flatten(key, value, depth, ann) {
|
|
59
|
+
if (Array.isArray(value)) return [{
|
|
60
|
+
indent: depth,
|
|
61
|
+
gutter: " ",
|
|
62
|
+
segments: [{
|
|
63
|
+
text: `${key}:`,
|
|
64
|
+
style: "plain"
|
|
65
|
+
}, ...ann]
|
|
66
|
+
}, ...value.map((el) => ({
|
|
67
|
+
indent: depth + 1,
|
|
68
|
+
gutter: " ",
|
|
69
|
+
segments: [{
|
|
70
|
+
text: `- ${scalarText(el)}`,
|
|
71
|
+
style: "plain"
|
|
72
|
+
}]
|
|
73
|
+
}))];
|
|
74
|
+
if (value !== null && typeof value === "object") return [{
|
|
75
|
+
indent: depth,
|
|
76
|
+
gutter: " ",
|
|
77
|
+
segments: [{
|
|
78
|
+
text: `${key}:`,
|
|
79
|
+
style: "plain"
|
|
80
|
+
}, ...ann]
|
|
81
|
+
}, ...Object.entries(value).flatMap(([k, v]) => flatten(k, v, depth + 1, []))];
|
|
82
|
+
return [{
|
|
83
|
+
indent: depth,
|
|
84
|
+
gutter: " ",
|
|
85
|
+
segments: [{
|
|
86
|
+
text: `${key}: ${scalarText(value)}`,
|
|
87
|
+
style: "plain"
|
|
88
|
+
}, ...ann]
|
|
89
|
+
}];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Render the Simulated view: the calculated fresh-consumer config as a plain
|
|
93
|
+
* pnpm-workspace.yaml listing (NOT a diff against the local file — nothing is
|
|
94
|
+
* added or removed), each top-level field annotated with how the plugin would
|
|
95
|
+
* combine it (`merge`/`overwrite`) and its enforcement (`warn`/`error`).
|
|
96
|
+
*
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
function renderSimulated(vanilla, manifest) {
|
|
100
|
+
const canon = canonicalize(vanilla);
|
|
101
|
+
return Object.entries(canon).flatMap(([k, v]) => flatten(k, v, 0, annotation(manifest[k])));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
export { renderSimulated };
|
package/cli/ui/Preview.js
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
|
+
import { legendLines, simulatedLegendLines } from "./legend.js";
|
|
1
2
|
import { Box, Text, useApp, useInput } from "ink";
|
|
2
3
|
import { createElement, useState } from "react";
|
|
3
4
|
import { Tab, Tabs } from "ink-tab";
|
|
4
5
|
|
|
5
6
|
//#region src/cli/ui/Preview.ts
|
|
6
|
-
const
|
|
7
|
-
added: "green",
|
|
8
|
-
removed: "red",
|
|
9
|
-
changed: "yellow",
|
|
10
|
-
warn: "red",
|
|
11
|
-
local: "magenta",
|
|
12
|
-
unmanaged: "
|
|
13
|
-
unchanged:
|
|
14
|
-
plain:
|
|
7
|
+
const INK_PROPS = {
|
|
8
|
+
added: { color: "green" },
|
|
9
|
+
removed: { color: "red" },
|
|
10
|
+
changed: { color: "yellow" },
|
|
11
|
+
warn: { color: "red" },
|
|
12
|
+
local: { color: "magenta" },
|
|
13
|
+
unmanaged: { color: "#585858" },
|
|
14
|
+
unchanged: { dimColor: true },
|
|
15
|
+
plain: {},
|
|
16
|
+
merge: { color: "cyan" },
|
|
17
|
+
overwrite: { color: "magenta" }
|
|
15
18
|
};
|
|
16
19
|
const TabC = Tab;
|
|
17
20
|
const TabsC = Tabs;
|
|
18
21
|
function renderLines(lines) {
|
|
19
22
|
return createElement(Box, { flexDirection: "column" }, ...lines.map((l, i) => {
|
|
20
23
|
const indent = " ".repeat(l.indent);
|
|
21
|
-
const tag = l.tag ?
|
|
24
|
+
const tag = l.tag === "local" ? " (local)" : "";
|
|
22
25
|
const body = l.segments.map((s, j) => {
|
|
23
|
-
const color = INK_COLOR[s.style];
|
|
24
26
|
return createElement(Text, {
|
|
25
27
|
key: j,
|
|
26
|
-
...
|
|
28
|
+
...INK_PROPS[s.style] ?? {}
|
|
27
29
|
}, s.text);
|
|
28
30
|
});
|
|
29
31
|
return createElement(Text, { key: i }, `${l.gutter} ${indent}`, ...body, tag);
|
|
@@ -39,7 +41,7 @@ function Preview({ views, onExit }) {
|
|
|
39
41
|
const app = useApp();
|
|
40
42
|
const [active, setActive] = useState("changes");
|
|
41
43
|
useInput((input, key) => {
|
|
42
|
-
if (input === "q" || key.escape) {
|
|
44
|
+
if (input === "q" || key.escape || key.return) {
|
|
43
45
|
onExit();
|
|
44
46
|
app.exit();
|
|
45
47
|
}
|
|
@@ -53,7 +55,7 @@ function Preview({ views, onExit }) {
|
|
|
53
55
|
}, "Full"), createElement(TabC, {
|
|
54
56
|
name: "simulated",
|
|
55
57
|
key: "simulated"
|
|
56
|
-
}, "Simulated")), renderLines(views[active]));
|
|
58
|
+
}, "Simulated")), renderLines(active === "simulated" ? simulatedLegendLines() : legendLines()), renderLines(views[active]));
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
//#endregion
|
package/cli/ui/Walk.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { computeGroupPeers } from "../interop-live.js";
|
|
2
|
+
import { cellColor, displayCandidates, initTable, peerFor, tableDecisions, tableStep, truncateEnd } from "../walk-reducer.js";
|
|
2
3
|
import { Box, Text, useApp, useInput } from "ink";
|
|
3
4
|
import { createElement, useEffect, useState } from "react";
|
|
4
5
|
|
|
@@ -15,7 +16,7 @@ const VIEWPORT = 20;
|
|
|
15
16
|
*
|
|
16
17
|
* @internal
|
|
17
18
|
*/
|
|
18
|
-
function Walk({ items, onDone, dryRun = false, unresolved = [] }) {
|
|
19
|
+
function Walk({ items, onDone, dryRun = false, unresolved = [], interopModels = /* @__PURE__ */ new Map() }) {
|
|
19
20
|
const app = useApp();
|
|
20
21
|
const [state, setState] = useState(() => initTable(items));
|
|
21
22
|
useEffect(() => {
|
|
@@ -43,8 +44,20 @@ function Walk({ items, onDone, dryRun = false, unresolved = [] }) {
|
|
|
43
44
|
const cellWidth = Math.max(...items.flatMap((i) => displayCandidates(i).map((c) => c.range.length + (c.isMajor ? 8 : 0))));
|
|
44
45
|
const maxCells = Math.max(...items.map((i) => displayCandidates(i).length));
|
|
45
46
|
const blankCell = `${" ".repeat(2 + cellWidth)} `;
|
|
47
|
+
const termCols = process.stdout.columns ?? 100;
|
|
48
|
+
const fixedPrefix = 2 + (pkgWidth + 2) + maxCells * (cellWidth + 4) + 2;
|
|
46
49
|
const start = Math.max(0, Math.min(state.cursor - Math.floor(VIEWPORT / 2), items.length - VIEWPORT));
|
|
47
50
|
const visible = items.slice(Math.max(0, start), Math.max(0, start) + VIEWPORT);
|
|
51
|
+
const interopPeers = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const [catalog, model] of interopModels) {
|
|
53
|
+
const selected = /* @__PURE__ */ new Map();
|
|
54
|
+
items.forEach((it, idx) => {
|
|
55
|
+
if (it.entry.catalog !== catalog) return;
|
|
56
|
+
const cand = displayCandidates(it)[state.picks[idx] ?? 0];
|
|
57
|
+
if (cand) selected.set(it.entry.pkg, cand.version);
|
|
58
|
+
});
|
|
59
|
+
interopPeers.set(catalog, computeGroupPeers(model, selected));
|
|
60
|
+
}
|
|
48
61
|
const rows = [];
|
|
49
62
|
let lastCatalog = null;
|
|
50
63
|
visible.forEach((item, offset) => {
|
|
@@ -72,7 +85,11 @@ function Walk({ items, onDone, dryRun = false, unresolved = [] }) {
|
|
|
72
85
|
});
|
|
73
86
|
for (let ci = candidates.length; ci < maxCells; ci++) cells.push(createElement(Text, { key: `blank-${ci}` }, blankCell));
|
|
74
87
|
const chosen = candidates[pick];
|
|
75
|
-
|
|
88
|
+
const gp = interopPeers.get(item.entry.catalog);
|
|
89
|
+
const peerText = gp?.peer.get(item.entry.pkg) ?? (chosen === void 0 ? "—" : peerFor(item, chosen));
|
|
90
|
+
const conflict = gp?.conflict.get(item.entry.pkg);
|
|
91
|
+
const room = Math.max(8, termCols - fixedPrefix - peerText.length - 3);
|
|
92
|
+
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 }, `│ ${peerText}`), conflict ? createElement(Text, { color: "red" }, truncateEnd(` ⚠ needs ${conflict}`, room)) : null, item.peerWarning ? createElement(Text, { color: "red" }, truncateEnd(` ⚠ ${item.peerWarning.message}`, room)) : null));
|
|
76
93
|
});
|
|
77
94
|
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);
|
|
78
95
|
}
|
package/cli/ui/ansi.js
CHANGED
|
@@ -12,7 +12,7 @@ function toAnsi(lines, opts) {
|
|
|
12
12
|
return lines.map((l) => {
|
|
13
13
|
const indent = " ".repeat(l.indent);
|
|
14
14
|
const body = l.segments.map((s) => paint(s.text, s.style, opts.color)).join("");
|
|
15
|
-
return `${l.gutter} ${indent}${body}${tagSuffix(l.tag)}`;
|
|
15
|
+
return `${l.gutter} ${indent}${body}${tagSuffix(l.tag, opts.color)}`;
|
|
16
16
|
}).join("\n");
|
|
17
17
|
}
|
|
18
18
|
|
package/cli/ui/legend.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/cli/ui/legend.ts
|
|
2
|
+
/**
|
|
3
|
+
* The color legend for the config diff (the `preview` command and
|
|
4
|
+
* `export --dry-run`). A single styled line whose swatches each carry the
|
|
5
|
+
* matching `ChangeStyle`, so the legend automatically tracks the palette.
|
|
6
|
+
* Callers prepend it only when color is on — with color off the swatches are
|
|
7
|
+
* indistinguishable and the legend is just noise.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
function legendLines() {
|
|
12
|
+
return [{
|
|
13
|
+
indent: 0,
|
|
14
|
+
gutter: " ",
|
|
15
|
+
segments: [
|
|
16
|
+
{
|
|
17
|
+
text: "Legend: ",
|
|
18
|
+
style: "plain"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
text: "■ added ",
|
|
22
|
+
style: "added"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
text: "■ removed ",
|
|
26
|
+
style: "removed"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
text: "■ modified ",
|
|
30
|
+
style: "changed"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
text: "■ unchanged ",
|
|
34
|
+
style: "unchanged"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
text: "■ unmanaged",
|
|
38
|
+
style: "unmanaged"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The legend for the Simulated view. That view is not a diff — nothing is added
|
|
45
|
+
* or removed — so it gets its own vocabulary: how each field is combined
|
|
46
|
+
* (`merge`/`overwrite`) and how divergence is enforced (`warn`/`error`).
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
function simulatedLegendLines() {
|
|
51
|
+
return [{
|
|
52
|
+
indent: 0,
|
|
53
|
+
gutter: " ",
|
|
54
|
+
segments: [
|
|
55
|
+
{
|
|
56
|
+
text: "Legend: ",
|
|
57
|
+
style: "plain"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
text: "■ merge ",
|
|
61
|
+
style: "merge"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
text: "■ overwrite ",
|
|
65
|
+
style: "overwrite"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
text: "■ warn ",
|
|
69
|
+
style: "changed"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
text: "■ error",
|
|
73
|
+
style: "warn"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
export { legendLines, simulatedLegendLines };
|
package/cli/ui/run-preview.js
CHANGED
|
@@ -15,7 +15,7 @@ function runPreview(views) {
|
|
|
15
15
|
render(createElement(Preview, {
|
|
16
16
|
views,
|
|
17
17
|
onExit: () => {}
|
|
18
|
-
})).waitUntilExit().then(() => resume(Effect.void));
|
|
18
|
+
})).waitUntilExit().then(() => resume(Effect.void)).catch((err) => resume(Effect.die(err)));
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
|
package/cli/ui/run-walk.js
CHANGED
|
@@ -11,19 +11,18 @@ import { createElement } from "react";
|
|
|
11
11
|
*
|
|
12
12
|
* @internal
|
|
13
13
|
*/
|
|
14
|
-
function runWalk(items, dryRun = false, unresolved = []) {
|
|
14
|
+
function runWalk(items, dryRun = false, unresolved = [], interopModels = /* @__PURE__ */ new Map()) {
|
|
15
15
|
return Effect.callback((resume) => {
|
|
16
16
|
let collected = [];
|
|
17
17
|
render(createElement(Walk, {
|
|
18
18
|
items,
|
|
19
19
|
dryRun,
|
|
20
20
|
unresolved,
|
|
21
|
+
interopModels,
|
|
21
22
|
onDone: (d) => {
|
|
22
23
|
collected = d;
|
|
23
24
|
}
|
|
24
|
-
})).waitUntilExit().then(() =>
|
|
25
|
-
resume(Effect.succeed([...collected]));
|
|
26
|
-
});
|
|
25
|
+
})).waitUntilExit().then(() => resume(Effect.succeed([...collected]))).catch((err) => resume(Effect.die(err)));
|
|
27
26
|
});
|
|
28
27
|
}
|
|
29
28
|
|
package/cli/ui/styled.js
CHANGED
|
@@ -6,9 +6,11 @@ const ANSI_OPEN = {
|
|
|
6
6
|
changed: "\x1B[33m",
|
|
7
7
|
warn: "\x1B[31m",
|
|
8
8
|
local: "\x1B[35m",
|
|
9
|
-
unmanaged: "\x1B[
|
|
9
|
+
unmanaged: "\x1B[38;5;240m",
|
|
10
10
|
unchanged: "\x1B[2m",
|
|
11
|
-
plain: ""
|
|
11
|
+
plain: "",
|
|
12
|
+
merge: "\x1B[36m",
|
|
13
|
+
overwrite: "\x1B[35m"
|
|
12
14
|
};
|
|
13
15
|
const ANSI_RESET = "\x1B[0m";
|
|
14
16
|
/** Apply (or omit) the SGR code for a style. @internal */
|
|
@@ -16,10 +18,17 @@ function paint(text, style, color) {
|
|
|
16
18
|
if (!color || style === "plain" || ANSI_OPEN[style] === "") return text;
|
|
17
19
|
return `${ANSI_OPEN[style]}${text}${ANSI_RESET}`;
|
|
18
20
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The trailing annotation for a tag, e.g. " (local)". With color on, the
|
|
23
|
+
* `unmanaged` tag is dropped — the dedicated gray shade plus the legend already
|
|
24
|
+
* convey it — but it is kept when color is off, where the shade is invisible and
|
|
25
|
+
* the tag is the only signal (the tags-stay-meaningful-without-color contract).
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
function tagSuffix(tag, color) {
|
|
21
30
|
if (tag === "local") return " (local)";
|
|
22
|
-
if (tag === "unmanaged") return " (unmanaged)";
|
|
31
|
+
if (tag === "unmanaged") return color ? "" : " (unmanaged)";
|
|
23
32
|
return "";
|
|
24
33
|
}
|
|
25
34
|
|
package/cli/walk-reducer.js
CHANGED
|
@@ -2,9 +2,22 @@
|
|
|
2
2
|
const ORDER = {
|
|
3
3
|
keep: 0,
|
|
4
4
|
"in-range": 1,
|
|
5
|
-
|
|
5
|
+
minor: 2,
|
|
6
|
+
latest: 3
|
|
6
7
|
};
|
|
7
8
|
/**
|
|
9
|
+
* Truncate `s` to at most `max` display columns, appending `…` when clipped.
|
|
10
|
+
* Keeps a long peer-conflict annotation from wrapping and breaking the table's
|
|
11
|
+
* column alignment. A `max` of 1 or less yields just the ellipsis.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
function truncateEnd(s, max) {
|
|
16
|
+
if (s.length <= max) return s;
|
|
17
|
+
if (max <= 1) return "…";
|
|
18
|
+
return `${s.slice(0, max - 1)}…`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
8
21
|
* The row's options in display order: keep first (always index 0, always the
|
|
9
22
|
* default), then the in-range bump, then the latest-overall bump. `planEntry`
|
|
10
23
|
* emits them in the opposite order, with keep last.
|
|
@@ -15,14 +28,18 @@ function displayCandidates(item) {
|
|
|
15
28
|
return [...item.candidates].sort((a, b) => ORDER[a.kind] - ORDER[b.kind]);
|
|
16
29
|
}
|
|
17
30
|
/**
|
|
18
|
-
* Initialize the table:
|
|
19
|
-
* the
|
|
31
|
+
* Initialize the table: keep selected on every row (so the default state applies
|
|
32
|
+
* nothing), and the cursor on the first actionable row. Since every discovered
|
|
33
|
+
* row is shown — up-to-date rows included, as context — the cursor skips past any
|
|
34
|
+
* leading inert keep-only rows to land where the user can act; it falls back to
|
|
35
|
+
* row 0 when nothing is actionable.
|
|
20
36
|
*
|
|
21
37
|
* @internal
|
|
22
38
|
*/
|
|
23
39
|
function initTable(items) {
|
|
40
|
+
const first = items.findIndex((i) => !i.upToDate);
|
|
24
41
|
return {
|
|
25
|
-
cursor: 0,
|
|
42
|
+
cursor: first === -1 ? 0 : first,
|
|
26
43
|
picks: items.map(() => 0),
|
|
27
44
|
done: false,
|
|
28
45
|
cancelled: false
|
|
@@ -113,4 +130,4 @@ function cellColor(candidate, selected) {
|
|
|
113
130
|
}
|
|
114
131
|
|
|
115
132
|
//#endregion
|
|
116
|
-
export { cellColor, displayCandidates, initTable, peerFor, tableDecisions, tableStep };
|
|
133
|
+
export { cellColor, displayCandidates, initTable, peerFor, tableDecisions, tableStep, truncateEnd };
|
package/index.d.ts
CHANGED
|
@@ -29,6 +29,33 @@ interface CatalogDeclaration {
|
|
|
29
29
|
readonly packages: Record<string, CatalogPackageSpec>;
|
|
30
30
|
}
|
|
31
31
|
//#endregion
|
|
32
|
+
//#region src/plugin/allowed-versions.d.ts
|
|
33
|
+
/**
|
|
34
|
+
* A directive that derives `peerDependencyRules.allowedVersions` rules from a
|
|
35
|
+
* catalog. For each catalog entry with an exact pinned version, a version-
|
|
36
|
+
* qualified rule `"<name>@<pin>><peer>"` is emitted, valued at the `peer`
|
|
37
|
+
* package's own catalog version (optionally re-prefixed) — retiring the "unmet
|
|
38
|
+
* peer on a fast prerelease line" warning class without ever masking a
|
|
39
|
+
* genuinely-unmet range: pnpm applies a qualified rule only when the parent
|
|
40
|
+
* instance's version satisfies the exact qualifier, so a same-named satellite on
|
|
41
|
+
* a different version line keeps its real complaint.
|
|
42
|
+
*
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
interface AllowedVersionsFromCatalogs {
|
|
46
|
+
/** The catalog whose entries supply the rules. */
|
|
47
|
+
readonly catalog: string;
|
|
48
|
+
/** The peer package each rule targets; its own version becomes the rule value and it gets no rule of its own. */
|
|
49
|
+
readonly peer: string;
|
|
50
|
+
/**
|
|
51
|
+
* Operator applied to the derived peer value. Omit to use the peer's catalog
|
|
52
|
+
* value verbatim (`4.0.0-beta.99` stays exact, `^3.0.0` stays `^3.0.0`); set
|
|
53
|
+
* `"^"` / `"~"` / `">="` etc. to strip the existing operator and apply this
|
|
54
|
+
* one; set `null` or `""` to strip to an exact version.
|
|
55
|
+
*/
|
|
56
|
+
readonly prefix?: string | null;
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
32
59
|
//#region src/runtime/types.d.ts
|
|
33
60
|
/**
|
|
34
61
|
* How a field's divergences are enforced: silent, console warning, or a thrown
|
|
@@ -130,6 +157,12 @@ interface PluginConfig {
|
|
|
130
157
|
readonly allowedVersions?: Record<string, string>;
|
|
131
158
|
readonly ignoreMissing?: string[];
|
|
132
159
|
readonly allowAny?: string[];
|
|
160
|
+
/**
|
|
161
|
+
* Derive version-qualified `allowedVersions` rules from a catalog (or several).
|
|
162
|
+
* Resolved and merged into `allowedVersions` at build time, then stripped; a
|
|
163
|
+
* manually authored `allowedVersions` entry wins on a key clash.
|
|
164
|
+
*/
|
|
165
|
+
readonly allowedVersionsFromCatalogs?: AllowedVersionsFromCatalogs | readonly AllowedVersionsFromCatalogs[];
|
|
133
166
|
}>;
|
|
134
167
|
/** Whether dependency build scripts are blocked unless explicitly allowed. */
|
|
135
168
|
readonly strictDepBuilds?: FieldInput<boolean>;
|
|
@@ -373,5 +406,5 @@ interface PluginConfig {
|
|
|
373
406
|
*/
|
|
374
407
|
declare function PnpmConfigPlugin(config: PluginConfig): Plugin;
|
|
375
408
|
//#endregion
|
|
376
|
-
export { type CatalogDeclaration, type CatalogPackageSpec, type Enforcement, type FieldInput, type LocalDirective, type PeerStrategy, type PluginConfig, PnpmConfigPlugin };
|
|
409
|
+
export { type AllowedVersionsFromCatalogs, type CatalogDeclaration, type CatalogPackageSpec, type Enforcement, type FieldInput, type LocalDirective, type PeerStrategy, type PluginConfig, PnpmConfigPlugin };
|
|
377
410
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rolldown-pnpm-config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A dogfooding example of our plugin",
|
|
6
6
|
"repository": {
|
|
@@ -35,13 +35,14 @@
|
|
|
35
35
|
"rolldown-pnpm-config": "bin/rolldown-pnpm-config.js"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@effect/platform-node": "4.0.0-beta.
|
|
39
|
-
"@effected/
|
|
40
|
-
"@effected/
|
|
41
|
-
"
|
|
38
|
+
"@effect/platform-node": "4.0.0-beta.99",
|
|
39
|
+
"@effected/npm": "^0.3.0",
|
|
40
|
+
"@effected/semver": "^0.2.0",
|
|
41
|
+
"@effected/yaml": "^0.5.0",
|
|
42
|
+
"effect": "4.0.0-beta.99",
|
|
42
43
|
"ink": "^7.1.1",
|
|
43
44
|
"ink-tab": "^5.2.0",
|
|
44
|
-
"oxc-parser": "^0.
|
|
45
|
+
"oxc-parser": "^0.141.0",
|
|
45
46
|
"react": "^19.2.7",
|
|
46
47
|
"std-env": "^4.2.0",
|
|
47
48
|
"std-osc8": "^0.2.0"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/plugin/allowed-versions.ts
|
|
2
|
+
/** An exact version: no range operator, no wildcard — `X.Y.Z` with an optional prerelease. */
|
|
3
|
+
const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
4
|
+
/** The install range of a catalog spec (bare string or `{ range }` object). */
|
|
5
|
+
const rangeOf = (spec) => typeof spec === "string" ? spec : spec.range;
|
|
6
|
+
/** Strip a leading range operator to the bare version digits (e.g. `^3.17.0` → `3.17.0`). */
|
|
7
|
+
const bareVersion = (range) => range.replace(/^[\^~>=<\s]+/, "").split(/\s/)[0] ?? range;
|
|
8
|
+
/** Apply the `prefix` transform to a peer value: verbatim, re-prefixed, or stripped to exact. */
|
|
9
|
+
function applyPrefix(peerValue, prefix) {
|
|
10
|
+
if (prefix === void 0) return peerValue;
|
|
11
|
+
const bare = bareVersion(peerValue);
|
|
12
|
+
return prefix === null || prefix === "" ? bare : `${prefix}${bare}`;
|
|
13
|
+
}
|
|
14
|
+
/** Derive the rules for one directive. Pure; throws when the catalog or peer is absent. */
|
|
15
|
+
function deriveOne(catalogs, { catalog, peer, prefix }) {
|
|
16
|
+
const decl = catalogs[catalog];
|
|
17
|
+
if (decl === void 0) throw new Error(`allowedVersionsFromCatalogs: catalog "${catalog}" is not declared`);
|
|
18
|
+
const peerSpec = decl.packages[peer];
|
|
19
|
+
if (peerSpec === void 0) throw new Error(`allowedVersionsFromCatalogs: peer "${peer}" is not in catalog "${catalog}"`);
|
|
20
|
+
const value = applyPrefix(rangeOf(peerSpec), prefix);
|
|
21
|
+
const table = {};
|
|
22
|
+
for (const [name, spec] of Object.entries(decl.packages)) {
|
|
23
|
+
if (name === peer) continue;
|
|
24
|
+
const range = rangeOf(spec);
|
|
25
|
+
if (!EXACT_VERSION.test(range)) continue;
|
|
26
|
+
table[`${name}@${range}>${peer}`] = value;
|
|
27
|
+
}
|
|
28
|
+
return table;
|
|
29
|
+
}
|
|
30
|
+
/** Merge the derived rules for one or more directives (later directives win on a key clash). */
|
|
31
|
+
function deriveAllowedVersions(catalogs, directives) {
|
|
32
|
+
const list = Array.isArray(directives) ? directives : [directives];
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const d of list) Object.assign(out, deriveOne(catalogs, d));
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a `peerDependencyRules` value carrying an `allowedVersionsFromCatalogs`
|
|
39
|
+
* directive: derive the rules from the catalogs, merge them into `allowedVersions`
|
|
40
|
+
* (an explicitly authored entry wins on a key clash), and strip the directive so
|
|
41
|
+
* the cleaned value passes the `peerDependencyRules` schema. Any value without the
|
|
42
|
+
* directive passes through untouched.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
function resolvePeerDependencyRules(value, catalogs) {
|
|
47
|
+
if (value === null || typeof value !== "object" || !("allowedVersionsFromCatalogs" in value)) return value;
|
|
48
|
+
const { allowedVersionsFromCatalogs, ...rest } = value;
|
|
49
|
+
const merged = {
|
|
50
|
+
...deriveAllowedVersions(catalogs, allowedVersionsFromCatalogs),
|
|
51
|
+
...rest.allowedVersions ?? {}
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
...rest,
|
|
55
|
+
...Object.keys(merged).length > 0 ? { allowedVersions: merged } : {}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
export { deriveAllowedVersions, resolvePeerDependencyRules };
|
package/plugin/freeze.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizeCatalogs } from "../catalogs.js";
|
|
2
2
|
import { DESCRIPTORS, deriveSchemas } from "../descriptors/index.js";
|
|
3
3
|
import { FIELD_REGISTRY } from "../registry.js";
|
|
4
|
+
import { resolvePeerDependencyRules } from "./allowed-versions.js";
|
|
4
5
|
import { Data, Effect, Schema } from "effect";
|
|
5
6
|
|
|
6
7
|
//#region src/plugin/freeze.ts
|
|
@@ -60,7 +61,11 @@ function freeze(config) {
|
|
|
60
61
|
if (raw === void 0) continue;
|
|
61
62
|
const decl = normalizeField(raw);
|
|
62
63
|
const schema = FIELD_SCHEMAS[field];
|
|
63
|
-
|
|
64
|
+
const value = field === "peerDependencyRules" ? yield* Effect.try({
|
|
65
|
+
try: () => resolvePeerDependencyRules(decl.value, config.catalogs),
|
|
66
|
+
catch: (error) => new ConfigError({ message: error instanceof Error ? error.message : String(error) })
|
|
67
|
+
}) : decl.value;
|
|
68
|
+
base[field] = schema ? yield* Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError((error) => new ConfigError({ message: `Invalid ${field}: ${String(error)}` }))) : value;
|
|
64
69
|
manifest[field] = {
|
|
65
70
|
strategy: reg.strategy,
|
|
66
71
|
enforcement: decl.enforcement ?? reg.enforcement,
|