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/bin/rolldown-pnpm-config.js +1 -1
- package/cli/ast.js +54 -0
- package/cli/commands/export.js +14 -33
- package/cli/commands/preview.js +7 -20
- package/cli/commands/upgrade.js +72 -142
- package/cli/diff/build.js +3 -3
- package/cli/diff/render.js +2 -1
- package/cli/discover.js +5 -38
- package/cli/edits.js +40 -28
- package/cli/evaluate.js +2 -24
- package/cli/interop-live.js +5 -13
- package/cli/interop.js +55 -65
- package/cli/load-config.js +40 -0
- package/cli/local-merge.js +7 -8
- package/cli/plan.js +37 -51
- package/cli/release-age.js +3 -4
- package/cli/resolve.js +40 -63
- package/cli/simulated-view.js +1 -3
- package/cli/summary.js +13 -45
- package/cli/ui/Walk.js +3 -12
- package/cli/ui/env.js +3 -5
- package/cli/ui/run-preview.js +6 -7
- package/cli/ui/run-walk.js +7 -4
- package/cli/validate.js +12 -16
- package/cli/walk-plan.js +2 -2
- package/cli/walk-reducer.js +29 -1
- package/cli/workspace-file.js +3 -3
- package/cli/workspace-resolve.js +9 -17
- package/package.json +6 -7
- package/patches/build.js +24 -10
- package/plugin/allowed-versions.js +2 -2
- package/plugin/freeze.js +13 -6
- package/runtime/strategies/arrays.js +2 -1
- package/runtime/strategies/catalogs.js +5 -14
- package/runtime/strategies/overrides.js +11 -4
- package/runtime/strategies/scalar.js +4 -4
- package/runtime/strategies/table.js +1 -1
- package/semver-util.js +29 -0
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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(
|
|
48
|
-
if (entry.peer
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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;
|
package/cli/interop-live.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
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(
|
|
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
|
|
33
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 (!
|
|
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
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
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
|
|
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
|
|
156
|
-
*
|
|
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
|
|
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 =
|
|
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 };
|
package/cli/local-merge.js
CHANGED
|
@@ -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 (
|
|
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 =
|
|
39
|
-
const v =
|
|
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 =
|
|
66
|
-
if (
|
|
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 =
|
|
20
|
-
const
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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 };
|
package/cli/release-age.js
CHANGED
|
@@ -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
|
-
|
|
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;
|