rolldown-pnpm-config 0.8.0 → 1.0.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/cli/edits.js CHANGED
@@ -2,56 +2,68 @@ import { versionKeyOf } from "./version-key.js";
2
2
 
3
3
  //#region src/cli/edits.ts
4
4
  /**
5
- * Convert resolved decisions into span edits. A chosen upgrade rewrites the
6
- * range literal (and the existing peer literal when the candidate carries a
7
- * recomputed peerRange). A keep with peer drift rewrites only the peer literal
8
- * to the resync target.
9
- *
10
- * Each edit is tagged with its package and unquoted range so `validateEdits`
11
- * can check it against the registry before it is written.
5
+ * The edit constructors for one entry, each tagged with the entry's package
6
+ * and route-aware version key so `validateEdits` can check it against the
7
+ * registry before it is written. The single place that knows the insertion
8
+ * syntax and the key routing — both the interactive and the `--yes` paths
9
+ * build their edits through it.
12
10
  *
13
11
  * @internal
14
12
  */
15
- function buildEdits(decisions) {
16
- const edits = [];
17
- for (const { item, chosen } of decisions) {
18
- const { entry } = item;
19
- const pkg = entry.pkg;
20
- const versionKey = versionKeyOf(entry);
21
- const insertAt = entry.rangeSpan[1];
22
- const range = (span, value) => ({
23
- span,
13
+ function entryEdits(entry) {
14
+ const pkg = entry.pkg;
15
+ const versionKey = versionKeyOf(entry);
16
+ const insertAt = entry.rangeSpan[1];
17
+ return {
18
+ range: (value) => ({
19
+ span: entry.rangeSpan,
24
20
  text: JSON.stringify(value),
25
21
  pkg,
26
22
  versionKey,
27
23
  kind: "range",
28
24
  value
29
- });
30
- const peer = (span, value) => ({
31
- span,
25
+ }),
26
+ setPeer: (value) => entry.peer ? {
27
+ span: entry.peer.span,
32
28
  text: JSON.stringify(value),
33
29
  pkg,
34
30
  versionKey,
35
31
  kind: "peer",
36
32
  value
37
- });
38
- const peerInsert = (value) => ({
33
+ } : {
39
34
  span: [insertAt, insertAt],
40
35
  text: `, peer: ${JSON.stringify(value)}`,
41
36
  pkg,
42
37
  versionKey,
43
38
  kind: "peer",
44
39
  value
45
- });
40
+ }
41
+ };
42
+ }
43
+ /**
44
+ * Convert resolved decisions into span edits. A chosen upgrade rewrites the
45
+ * range literal (and the existing peer literal when the candidate carries a
46
+ * recomputed peerRange). A keep with peer drift rewrites only the peer literal
47
+ * to the resync target.
48
+ *
49
+ * Each edit is tagged with its package and unquoted range so `validateEdits`
50
+ * can check it against the registry before it is written.
51
+ *
52
+ * @internal
53
+ */
54
+ function buildEdits(decisions) {
55
+ const edits = [];
56
+ for (const { item, chosen } of decisions) {
57
+ const { entry } = item;
58
+ const { range, setPeer } = entryEdits(entry);
46
59
  if (chosen.kind !== "keep") {
47
- edits.push(range(entry.rangeSpan, chosen.range));
48
- if (entry.peer && chosen.peerRange) edits.push(peer(entry.peer.span, chosen.peerRange));
49
- else if (!entry.peer && entry.strategy && chosen.peerRange) edits.push(peerInsert(chosen.peerRange));
50
- } else if (entry.peer && item.driftPeer) edits.push(peer(entry.peer.span, item.driftPeer));
51
- else if (!entry.peer && item.materializePeer) edits.push(peerInsert(item.materializePeer));
60
+ edits.push(range(chosen.range));
61
+ if (chosen.peerRange && (entry.peer || entry.strategy)) edits.push(setPeer(chosen.peerRange));
62
+ } else if (entry.peer && item.driftPeer) edits.push(setPeer(item.driftPeer));
63
+ else if (!entry.peer && item.materializePeer) edits.push(setPeer(item.materializePeer));
52
64
  }
53
65
  return edits;
54
66
  }
55
67
 
56
68
  //#endregion
57
- export { buildEdits };
69
+ export { buildEdits, entryEdits };
package/cli/evaluate.js CHANGED
@@ -1,28 +1,7 @@
1
+ import { findPluginArg, keyName } from "./ast.js";
1
2
  import { parseSync } from "oxc-parser";
2
3
 
3
4
  //#region src/cli/evaluate.ts
4
- /** Find the first `PnpmConfigPlugin(...)` call's first argument (an object literal). */
5
- function findPluginArg(program) {
6
- let found;
7
- const visit = (node) => {
8
- if (found || node === null || typeof node !== "object") return;
9
- const n = node;
10
- if (n.type === "CallExpression") {
11
- const callee = n.callee;
12
- if (callee?.type === "Identifier" && callee.name === "PnpmConfigPlugin") {
13
- const args = n.arguments;
14
- if (args?.[0]?.type === "ObjectExpression") {
15
- found = args[0];
16
- return;
17
- }
18
- }
19
- }
20
- for (const value of Object.values(n)) if (Array.isArray(value)) value.forEach(visit);
21
- else if (value && typeof value === "object") visit(value);
22
- };
23
- visit(program);
24
- return found;
25
- }
26
5
  /** Evaluate a literal AST node into a plain JS value; unsupported nodes push to `errors`. */
27
6
  function evalNode(node, path, errors) {
28
7
  switch (node.type) {
@@ -46,8 +25,7 @@ function evalNode(node, path, errors) {
46
25
  errors.push(`${path}: spread/getter is not supported`);
47
26
  continue;
48
27
  }
49
- const key = prop.key;
50
- const name = key.type === "Identifier" ? key.name : key.type === "Literal" ? String(key.value) : void 0;
28
+ const name = keyName(prop.key);
51
29
  if (name === void 0) {
52
30
  errors.push(`${path}: computed key is not supported`);
53
31
  continue;
@@ -1,12 +1,8 @@
1
+ import { bareVersion, parseRange, parseVersion } from "../semver-util.js";
1
2
  import "./interop.js";
2
3
  import { Effect } from "effect";
3
- import { Range, SemVer } from "@effected/semver";
4
4
 
5
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
6
  /**
11
7
  * Recompute each member's peer floor and conflict for the current selection —
12
8
  * synchronously, from a pre-built {@link GroupModel}. Mirrors `deriveFloors`
@@ -24,7 +20,7 @@ function computeGroupPeers(model, selected) {
24
20
  if (v === void 0) continue;
25
21
  for (const { dep, range } of model.peerReqs.get(`${m}@${v}`) ?? []) {
26
22
  const list = declaredFloors.get(dep) ?? [];
27
- list.push(floorOf(range));
23
+ list.push(bareVersion(range));
28
24
  declaredFloors.set(dep, list);
29
25
  }
30
26
  }
@@ -96,19 +92,15 @@ function buildGroupModel(candidatesByPkg, fetchPeer) {
96
92
  range
97
93
  });
98
94
  rngStrings.add(range);
99
- verStrings.add(floorOf(range));
95
+ verStrings.add(bareVersion(range));
100
96
  }
101
97
  peerReqs.set(`${pkg}@${v}`, reqs);
102
98
  }
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
99
  return {
108
100
  members,
109
101
  peerReqs,
110
- ver,
111
- rng
102
+ ver: new Map([...verStrings].map((s) => [s, parseVersion(s)])),
103
+ rng: new Map([...rngStrings].map((s) => [s, parseRange(s)]))
112
104
  };
113
105
  });
114
106
  }
package/cli/interop.js CHANGED
@@ -1,12 +1,27 @@
1
+ import { bareVersion, parseRange, parseVersion } from "../semver-util.js";
1
2
  import { Effect } from "effect";
2
- import { Range, SemVer } from "@effected/semver";
3
3
 
4
4
  //#region src/cli/interop.ts
5
5
  /** Maximum number of concurrent peerDependencies fetches inside runInterop. @internal */
6
6
  const INTEROP_PEER_CONCURRENCY = 8;
7
- /** Strip a range operator to its bare version digits (e.g. `^3.17.0` → `3.17.0`). */
8
- function floorOf(range) {
9
- return range.replace(/^[\^~>=\s]+/, "").split(/\s/)[0] ?? range;
7
+ /**
8
+ * Build an Effectful memoized peer-deps fetcher over `resolver`: a cache hit
9
+ * returns immediately; a miss calls the resolver (degrading to `{}` on
10
+ * failure), stores the result in `cache`, and returns it. A `(pkg, version)`
11
+ * peerDeps lookup is immutable, so a shared `cache` may outlive one call.
12
+ *
13
+ * @internal
14
+ */
15
+ function makePeerFetcher(resolver, cache = /* @__PURE__ */ new Map()) {
16
+ return (pkg, v) => {
17
+ const k = `${pkg}@${v}`;
18
+ const cached = cache.get(k);
19
+ if (cached !== void 0) return Effect.succeed(cached);
20
+ return resolver.peerDependencies(pkg, v).pipe(Effect.orElseSucceed(() => ({})), Effect.map((deps) => {
21
+ cache.set(k, deps);
22
+ return deps;
23
+ }));
24
+ };
10
25
  }
11
26
  /**
12
27
  * Derive each member's caret-capped peer floor: the lowest floor any in-group
@@ -23,33 +38,35 @@ function deriveFloors(resolved, fetchPeer) {
23
38
  for (const [dep, range] of Object.entries(peers)) {
24
39
  if (!resolved.has(dep)) continue;
25
40
  const list = floors.get(dep) ?? [];
26
- list.push(floorOf(range));
41
+ list.push(bareVersion(range));
27
42
  floors.set(dep, list);
28
43
  }
29
44
  }
30
45
  const out = /* @__PURE__ */ new Map();
31
46
  for (const [pkg, version] of resolved) {
32
- const declared = floors.get(pkg);
33
- if (declared?.length) {
34
- const valid = (yield* Effect.forEach(declared, (f) => SemVer.parse(f).pipe(Effect.map((sv) => ({
35
- f,
36
- sv
37
- })), Effect.catch(() => Effect.succeed(null))))).filter((x) => x !== null);
38
- valid.sort((a, b) => a.sv.compare(b.sv));
39
- out.set(pkg, `^${valid[0]?.f ?? version}`);
40
- } else out.set(pkg, `^${version}`);
47
+ const lowest = lowestVersion(floors.get(pkg) ?? []);
48
+ out.set(pkg, `^${lowest ?? version}`);
41
49
  }
42
50
  return out;
43
51
  });
44
52
  }
53
+ /** The lowest parseable version string in `list`, or null when none parses. */
54
+ function lowestVersion(list) {
55
+ let best = null;
56
+ for (const f of list) {
57
+ const sv = parseVersion(f);
58
+ if (sv !== null && (best === null || sv.compare(best.sv) < 0)) best = {
59
+ f,
60
+ sv
61
+ };
62
+ }
63
+ return best?.f ?? null;
64
+ }
45
65
  /** Does `version` satisfy `range`? Unparseable input is treated as not-satisfied. */
46
66
  function satisfies(version, range) {
47
- return Effect.gen(function* () {
48
- const r = yield* Range.parse(range).pipe(Effect.catch(() => Effect.succeed(null)));
49
- if (!r) return false;
50
- const v = yield* SemVer.parse(version).pipe(Effect.catch(() => Effect.succeed(null)));
51
- return v ? r.test(v) : false;
52
- });
67
+ const r = parseRange(range);
68
+ const v = parseVersion(version);
69
+ return r !== null && v !== null && r.test(v);
53
70
  }
54
71
  /**
55
72
  * In-group peers of (pkg@version) that the current resolution violates, as
@@ -63,7 +80,7 @@ function violations(pkg, version, resolved, memberSet, fetchPeer) {
63
80
  if (!memberSet.has(dep)) continue;
64
81
  const rv = resolved.get(dep);
65
82
  if (rv === void 0) continue;
66
- if (!(yield* satisfies(rv, range))) out.push(`${dep}@${range}`);
83
+ if (!satisfies(rv, range)) out.push(`${dep}@${range}`);
67
84
  }
68
85
  return out;
69
86
  });
@@ -80,23 +97,23 @@ function resolveGroup(members, fetchPeer) {
80
97
  const memberSet = new Set(members.map((m) => m.pkg));
81
98
  const resolved = new Map(members.map((m) => [m.pkg, m.ceiling]));
82
99
  const ceilingOf = new Map(members.map((m) => [m.pkg, m.ceiling]));
83
- const leq = (a, b) => Effect.gen(function* () {
84
- const av = yield* SemVer.parse(a).pipe(Effect.catch(() => Effect.succeed(null)));
85
- const bv = yield* SemVer.parse(b).pipe(Effect.catch(() => Effect.succeed(null)));
86
- return av && bv ? av.compare(bv) <= 0 : false;
87
- });
100
+ const eligibleOf = /* @__PURE__ */ new Map();
101
+ for (const m of members) {
102
+ const ceiling = parseVersion(m.ceiling);
103
+ const eligible = ceiling ? m.candidates.map((v) => ({
104
+ v,
105
+ sv: parseVersion(v)
106
+ })).filter((x) => x.sv !== null && x.sv.compare(ceiling) <= 0).sort((a, b) => b.sv.compare(a.sv)).map((x) => x.v) : [];
107
+ eligibleOf.set(m.pkg, eligible);
108
+ }
88
109
  const maxIter = members.reduce((n, m) => n + m.candidates.length, 0) + members.length + 1;
89
110
  for (let i = 0; i < maxIter; i++) {
90
111
  let changed = false;
91
112
  for (const m of members) {
92
113
  const cur = resolved.get(m.pkg);
93
114
  if ((yield* violations(m.pkg, cur, resolved, memberSet, fetchPeer)).length === 0) continue;
94
- const ceiling = ceilingOf.get(m.pkg);
95
- const eligible = [];
96
- for (const c of m.candidates) if (yield* leq(c, ceiling)) eligible.push(c);
97
- const sorted = yield* sortDesc(eligible);
98
115
  let pick = null;
99
- for (const c of sorted) if ((yield* violations(m.pkg, c, resolved, memberSet, fetchPeer)).length === 0) {
116
+ for (const c of eligibleOf.get(m.pkg) ?? []) if ((yield* violations(m.pkg, c, resolved, memberSet, fetchPeer)).length === 0) {
100
117
  pick = c;
101
118
  break;
102
119
  }
@@ -124,20 +141,6 @@ function resolveGroup(members, fetchPeer) {
124
141
  };
125
142
  });
126
143
  }
127
- /** Sort version strings descending; unparseable ones sink to the end. */
128
- function sortDesc(versions) {
129
- return Effect.gen(function* () {
130
- const parsed = yield* Effect.forEach(versions, (v) => SemVer.parse(v).pipe(Effect.map((sv) => ({
131
- v,
132
- sv
133
- })), Effect.catch(() => Effect.succeed({
134
- v,
135
- sv: null
136
- }))));
137
- parsed.sort((a, b) => a.sv && b.sv ? b.sv.compare(a.sv) : a.sv ? -1 : 1);
138
- return parsed.map((p) => p.v);
139
- });
140
- }
141
144
  /**
142
145
  * Fetch the peerDependencies needed to reconcile one catalog interop group,
143
146
  * then run the pure resolve + floor derivation. Failures degrade to empty
@@ -152,41 +155,28 @@ function sortDesc(versions) {
152
155
  * O(N × |candidates|) to O(N + |downgraded members| × depth).
153
156
  *
154
157
  * A `(pkg, version)` peerDeps lookup is immutable, so the optional `cache` may
155
- * be shared across the interactive re-entry rounds: each round only fetches the
156
- * keys a prior round did not, sparing the sequential `pnpm view` calls for
157
- * versions already seen. Omitting it yields a fresh per-call cache.
158
+ * be shared across calls: each call only fetches the keys a prior one did not.
159
+ * Omitting it yields a fresh per-call cache.
158
160
  *
159
161
  * @internal
160
162
  */
161
163
  function runInterop(members, resolver, cache = /* @__PURE__ */ new Map()) {
162
164
  return Effect.gen(function* () {
163
- const key = (pkg, v) => `${pkg}@${v}`;
164
- const fetchPeer = (pkg, v) => {
165
- const k = key(pkg, v);
166
- const cached = cache.get(k);
167
- if (cached !== void 0) return Effect.succeed(cached);
168
- return resolver.peerDependencies(pkg, v).pipe(Effect.catch(() => Effect.succeed({}))).pipe(Effect.map((deps) => {
169
- cache.set(k, deps);
170
- return deps;
171
- }));
172
- };
165
+ const fetchPeer = makePeerFetcher(resolver, cache);
173
166
  const seen = /* @__PURE__ */ new Set();
174
167
  const toFetch = [];
175
168
  for (const m of members) {
176
- const k = key(m.pkg, m.ceiling);
169
+ const k = `${m.pkg}@${m.ceiling}`;
177
170
  if (seen.has(k) || cache.has(k)) continue;
178
171
  seen.add(k);
179
172
  toFetch.push([m.pkg, m.ceiling]);
180
173
  }
181
174
  yield* Effect.forEach(toFetch, ([pkg, v]) => fetchPeer(pkg, v), { concurrency: 8 });
182
175
  const { resolved, conflicts } = yield* resolveGroup(members, fetchPeer);
183
- const peers = yield* deriveFloors(resolved, fetchPeer);
184
- const peerDepsOf = (pkg, v) => cache.get(key(pkg, v)) ?? {};
185
176
  return {
186
177
  resolved,
187
- peers,
188
- conflicts,
189
- peerDepsOf
178
+ peers: yield* deriveFloors(resolved, fetchPeer),
179
+ conflicts
190
180
  };
191
181
  });
192
182
  }
@@ -234,4 +224,4 @@ function buildInteropEdits(entries, result) {
234
224
  }
235
225
 
236
226
  //#endregion
237
- export { buildInteropEdits, deriveFloors, interopEntryChanged, resolveGroup, runInterop };
227
+ export { buildInteropEdits, deriveFloors, interopEntryChanged, makePeerFetcher, resolveGroup, runInterop };
@@ -0,0 +1,40 @@
1
+ import { evaluatePluginConfig } from "./evaluate.js";
2
+ import { findWorkspaceFile, parseWorkspace } from "./workspace-file.js";
3
+ import { Effect, Predicate } from "effect";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+
7
+ //#region src/cli/load-config.ts
8
+ /**
9
+ * The prologue shared by `export` and `preview`: read and statically evaluate
10
+ * the config file (failing on a missing call or non-literal values), locate
11
+ * the workspace file, and parse it when present. `mkError` wraps each failure
12
+ * message in the command's own error type.
13
+ *
14
+ * @internal
15
+ */
16
+ function loadConfigAndWorkspace(opts, mkError) {
17
+ return Effect.gen(function* () {
18
+ const configSource = yield* Effect.try({
19
+ try: () => readFileSync(opts.configFile, "utf8"),
20
+ catch: () => mkError(`Cannot read ${opts.configFile}`)
21
+ });
22
+ const { config, errors } = evaluatePluginConfig(configSource, opts.configFile);
23
+ if (config === null) return yield* Effect.fail(mkError(`No PnpmConfigPlugin call found in ${opts.configFile}`));
24
+ if (errors.length > 0) return yield* Effect.fail(mkError(`Non-literal config values: ${errors.join("; ")}`));
25
+ const path = opts.workspacePath ?? findWorkspaceFile(process.cwd()) ?? join(process.cwd(), "pnpm-workspace.yaml");
26
+ const parsed = existsSync(path) ? yield* Effect.try({
27
+ try: () => parseWorkspace(readFileSync(path, "utf8")),
28
+ catch: (e) => mkError(`Cannot read or parse ${path}: ${String(e)}`)
29
+ }) : {};
30
+ return {
31
+ config,
32
+ localCfg: Predicate.isObject(config.local) ? config.local : void 0,
33
+ path,
34
+ parsed
35
+ };
36
+ });
37
+ }
38
+
39
+ //#endregion
40
+ export { loadConfigAndWorkspace };
@@ -1,3 +1,5 @@
1
+ import { Predicate } from "effect";
2
+
1
3
  //#region src/cli/local-merge.ts
2
4
  /** Default protocols whose existing-file override entries are preserved. @internal */
3
5
  const DEFAULT_PRESERVE = [
@@ -19,13 +21,10 @@ const DIRECTIVE_KEYS = /* @__PURE__ */ new Set([
19
21
  * @internal
20
22
  */
21
23
  function isLocalDirective(v) {
22
- if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
24
+ if (!Predicate.isObject(v)) return false;
23
25
  const keys = Object.keys(v);
24
26
  return keys.length > 0 && keys.every((k) => DIRECTIVE_KEYS.has(k));
25
27
  }
26
- function isRecord(v) {
27
- return v !== null && typeof v === "object" && !Array.isArray(v);
28
- }
29
28
  /** Union/difference two records or two arrays; managed is the left operand. */
30
29
  function combine(managed, value, strategy) {
31
30
  if (Array.isArray(managed) || Array.isArray(value)) {
@@ -35,8 +34,8 @@ function combine(managed, value, strategy) {
35
34
  const drop = new Set(v.map((x) => JSON.stringify(x)));
36
35
  return m.filter((x) => !drop.has(JSON.stringify(x)));
37
36
  }
38
- const m = isRecord(managed) ? managed : {};
39
- const v = isRecord(value) ? value : {};
37
+ const m = Predicate.isObject(managed) ? managed : {};
38
+ const v = Predicate.isObject(value) ? value : {};
40
39
  if (strategy === "union") return {
41
40
  ...m,
42
41
  ...v
@@ -62,8 +61,8 @@ function applyLocalDirective(managed, raw, parsed, field) {
62
61
  else result = managed;
63
62
  if (field === "overrides") {
64
63
  const protocols = directive.preserve ?? DEFAULT_PRESERVE;
65
- const base = isRecord(result) ? { ...result } : {};
66
- if (isRecord(parsed)) {
64
+ const base = Predicate.isObject(result) ? { ...result } : {};
65
+ if (Predicate.isObject(parsed)) {
67
66
  for (const [k, val] of Object.entries(parsed)) if (typeof val === "string" && protocols.some((p) => val.startsWith(`${p}:`))) base[k] = val;
68
67
  }
69
68
  if (Object.keys(base).length === 0 && managed === void 0) return void 0;
package/cli/plan.js CHANGED
@@ -1,10 +1,9 @@
1
+ import { bareVersion, parseRange, parseVersion } from "../semver-util.js";
1
2
  import { derivePeerRange } from "./peer-range.js";
2
- import { Effect } from "effect";
3
+ import { Effect, Option } from "effect";
3
4
  import { Range, SemVer } from "@effected/semver";
4
5
 
5
6
  //#region src/cli/plan.ts
6
- /** Parse a version, returning null instead of failing (filters junk tags). */
7
- const parseOrNull = (v) => SemVer.parse(v).pipe(Effect.catch(() => Effect.succeed(null)));
8
7
  /**
9
8
  * Compute the candidate versions for one catalog entry against the list of
10
9
  * published versions. Order: latest in-range (when newer than current), latest
@@ -16,71 +15,58 @@ const parseOrNull = (v) => SemVer.parse(v).pipe(Effect.catch(() => Effect.succee
16
15
  */
17
16
  function planEntry(entry, versions) {
18
17
  return Effect.gen(function* () {
19
- const range = yield* Range.parse(entry.currentRange).pipe(Effect.catch(() => Effect.succeed(null)));
20
- const currentStripped = entry.currentRange.replace(/^[\^~]/, "");
21
- const current = yield* parseOrNull(currentStripped);
18
+ const range = parseRange(entry.currentRange);
19
+ const current = parseVersion(bareVersion(entry.currentRange));
22
20
  const currentMajor = current?.major ?? 0;
23
21
  const track = current && current.prerelease.length > 0 ? String(current.prerelease[0]) : null;
24
22
  const onTrack = (v) => track !== null && v.prerelease.length > 0 && String(v.prerelease[0]) === track;
25
- const parsed = [];
26
- for (const v of versions) {
27
- const sv = yield* parseOrNull(v);
28
- if (sv && (sv.isStable || onTrack(sv))) parsed.push(sv);
29
- }
30
- parsed.sort((a, b) => a.compare(b));
31
- const maxOf = (list) => list.length ? list[list.length - 1] : null;
32
- const inRangeMax = range ? maxOf(parsed.filter((v) => range.test(v))) : null;
33
- const sameMajorMax = maxOf(parsed.filter((v) => v.major === currentMajor));
34
- const overallMax = maxOf(parsed);
23
+ const parsed = versions.map(parseVersion).filter((sv) => sv !== null && (sv.isStable || onTrack(sv)));
24
+ const inRangeMax = range ? Option.getOrNull(Range.maxSatisfying(parsed, range)) : null;
25
+ const sameMajorMax = Option.getOrNull(SemVer.max(parsed.filter((v) => v.major === currentMajor)));
26
+ const overallMax = Option.getOrNull(SemVer.max(parsed));
35
27
  const withPeer = (version) => entry.strategy && entry.strategy !== "interop" ? derivePeerRange(`${entry.operator}${version}`, entry.strategy).pipe(Effect.map((d) => d.range)) : Effect.succeed(void 0);
28
+ const candidate = (kind, sv) => Effect.map(withPeer(sv.toString()), (peerRange) => ({
29
+ kind,
30
+ range: `${entry.operator}${sv}`,
31
+ version: sv.toString(),
32
+ isMajor: sv.major > currentMajor,
33
+ ...peerRange ? { peerRange } : {}
34
+ }));
36
35
  const candidates = [];
37
- if (inRangeMax && (current === null || inRangeMax.gt(current))) {
38
- const version = inRangeMax.toString();
39
- const peerRange = yield* withPeer(version);
40
- candidates.push({
41
- kind: "in-range",
42
- range: `${entry.operator}${version}`,
43
- version,
44
- isMajor: inRangeMax.major > currentMajor,
45
- ...peerRange ? { peerRange } : {}
46
- });
47
- }
36
+ if (inRangeMax && (current === null || inRangeMax.gt(current))) candidates.push(yield* candidate("in-range", inRangeMax));
48
37
  if (sameMajorMax !== null) {
49
38
  const beatsCurrent = current === null || sameMajorMax.gt(current);
50
39
  const beatsInRange = inRangeMax === null || sameMajorMax.gt(inRangeMax);
51
40
  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
- }
64
- if (overallMax && (current === null || overallMax.gt(current)) && (!inRangeMax || overallMax.gt(inRangeMax))) {
65
- const version = overallMax.toString();
66
- const peerRange = yield* withPeer(version);
67
- candidates.push({
68
- kind: "latest",
69
- range: `${entry.operator}${version}`,
70
- version,
71
- isMajor: overallMax.major > currentMajor,
72
- ...peerRange ? { peerRange } : {}
73
- });
41
+ if (beatsCurrent && beatsInRange && belowOverall) candidates.push(yield* candidate("minor", sameMajorMax));
74
42
  }
43
+ if (overallMax && (current === null || overallMax.gt(current)) && (!inRangeMax || overallMax.gt(inRangeMax))) candidates.push(yield* candidate("latest", overallMax));
75
44
  candidates.push({
76
45
  kind: "keep",
77
46
  range: entry.currentRange,
78
- version: entry.currentRange.replace(/^[\^~]/, ""),
47
+ version: bareVersion(entry.currentRange),
79
48
  isMajor: false
80
49
  });
81
50
  return candidates;
82
51
  });
83
52
  }
53
+ /**
54
+ * The non-interactive default pick for an entry: the latest in-range
55
+ * candidate, or — for a workspace-sourced entry — the sole non-keep
56
+ * candidate. A workspace entry tracks its workspace's single next version,
57
+ * which for a 0.x caret routinely falls OUTSIDE the current range (`^0.2.0`
58
+ * does not contain 0.3.0); the never-cross-a-range rule protects against
59
+ * surprise REGISTRY majors, and the workspace version is this repo's own
60
+ * declared next release. Undefined when nothing but keep is on offer.
61
+ *
62
+ * Shared by `--yes`/`--check` (runUpgrade) and the `--preview` / non-TTY
63
+ * projection (projectDecisions) so the three can never disagree.
64
+ *
65
+ * @internal
66
+ */
67
+ function defaultPick(entry, candidates) {
68
+ return entry.source === "workspace" ? candidates.find((c) => c.kind !== "keep") : candidates.find((c) => c.kind === "in-range");
69
+ }
84
70
 
85
71
  //#endregion
86
- export { planEntry };
72
+ export { defaultPick, planEntry };
@@ -1,3 +1,5 @@
1
+ import { isWrappedField } from "../plugin/freeze.js";
2
+
1
3
  //#region src/cli/release-age.ts
2
4
  /**
3
5
  * Read a release-age gate from config and pnpm and combine it with
@@ -7,10 +9,7 @@
7
9
  * only turns this repo's two config sources into partial contributions.
8
10
  */
9
11
  /** Unwrap a managed field that may be a bare value or a `{ value, enforcement }` FieldInput. */
10
- function fieldValue(raw) {
11
- if (raw && typeof raw === "object" && !Array.isArray(raw) && "value" in raw) return raw.value;
12
- return raw;
13
- }
12
+ const fieldValue = (raw) => isWrappedField(raw) ? raw.value : raw;
14
13
  /** Read the release-age gate declared in a statically-evaluated PnpmConfigPlugin config. @internal */
15
14
  function readConfigReleaseAge(config) {
16
15
  if (!config) return null;