rolldown-pnpm-config 0.8.1 → 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.
@@ -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.8.1" }).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
15
+ Command.run(root, { version: "1.0.0" }).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
16
16
 
17
17
  //#endregion
18
18
  export { };
package/cli/ast.js ADDED
@@ -0,0 +1,54 @@
1
+ //#region src/cli/ast.ts
2
+ /**
3
+ * The static name of an ObjectExpression property key: an Identifier's name
4
+ * or a Literal's stringified value; undefined for a computed key.
5
+ *
6
+ * @internal
7
+ */
8
+ function keyName(key) {
9
+ return key.type === "Identifier" ? key.name : key.type === "Literal" ? String(key.value) : void 0;
10
+ }
11
+ /**
12
+ * Find a property value by key name in an ObjectExpression node. Handles both
13
+ * Identifier keys (unquoted) and Literal keys (quoted).
14
+ *
15
+ * @internal
16
+ */
17
+ function prop(obj, key) {
18
+ for (const p of obj.properties ?? []) {
19
+ if (p.type !== "Property") continue;
20
+ if (keyName(p.key) === key) return p.value;
21
+ }
22
+ }
23
+ /**
24
+ * Find the first `PnpmConfigPlugin(...)` call's first argument (an object
25
+ * literal). The single AST walker behind both static discovery (`upgrade`)
26
+ * and static evaluation (`export` / `preview`), so the two always agree on
27
+ * which call they found.
28
+ *
29
+ * @internal
30
+ */
31
+ function findPluginArg(program) {
32
+ let found;
33
+ const visit = (node) => {
34
+ if (found || node === null || typeof node !== "object") return;
35
+ const n = node;
36
+ if (n.type === "CallExpression") {
37
+ const callee = n.callee;
38
+ if (callee?.type === "Identifier" && callee.name === "PnpmConfigPlugin") {
39
+ const args = n.arguments;
40
+ if (args?.[0]?.type === "ObjectExpression") {
41
+ found = args[0];
42
+ return;
43
+ }
44
+ }
45
+ }
46
+ for (const value of Object.values(n)) if (Array.isArray(value)) value.forEach(visit);
47
+ else if (value && typeof value === "object") visit(value);
48
+ };
49
+ visit(program);
50
+ return found;
51
+ }
52
+
53
+ //#endregion
54
+ export { findPluginArg, keyName, prop };
@@ -1,5 +1,4 @@
1
- import { discoverPatches } from "../../patches/discover.js";
2
- import { isRewriteDirective, readLocalPatchesDir, withResolvedBuildPatches } from "../../patches/build.js";
1
+ import { discoverOwnedPatches, withResolvedBuildPatches } from "../../patches/build.js";
3
2
  import { DESCRIPTORS } from "../../descriptors/index.js";
4
3
  import { freeze } from "../../plugin/freeze.js";
5
4
  import { resolveRootName } from "../../runtime/ctx.js";
@@ -7,16 +6,16 @@ import { reconcilePatches } from "../../patches/reconcile.js";
7
6
  import { buildDiff } from "../diff/build.js";
8
7
  import { renderExportDiff } from "../diff/render.js";
9
8
  import { effectiveManaged } from "../effective.js";
10
- import { evaluatePluginConfig } from "../evaluate.js";
9
+ import { canonicalize, renderWorkspace } from "../workspace-file.js";
10
+ import { loadConfigAndWorkspace } from "../load-config.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
14
  import { legendLines } from "../ui/legend.js";
15
- import { canonicalize, findWorkspaceFile, parseWorkspace, renderWorkspace } from "../workspace-file.js";
16
15
  import { overlayWorkspace } from "../workspace-overlay.js";
17
- import { Data, Effect, Option } from "effect";
18
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
19
- import { dirname, join, relative } from "node:path";
16
+ import { Data, Effect, Option, Predicate } from "effect";
17
+ import { existsSync, writeFileSync } from "node:fs";
18
+ import { dirname, relative } from "node:path";
20
19
  import { Argument, Command, Flag } from "effect/unstable/cli";
21
20
 
22
21
  //#region src/cli/commands/export.ts
@@ -43,43 +42,25 @@ const WORKSPACE_FIELDS = new Set(Object.entries(DESCRIPTORS).filter(([, d]) => d
43
42
  */
44
43
  function runExport(opts) {
45
44
  return Effect.gen(function* () {
46
- const configSource = yield* Effect.try({
47
- try: () => readFileSync(opts.configFile, "utf8"),
48
- catch: () => new ExportError({ message: `Cannot read ${opts.configFile}` })
49
- });
50
- const { config, errors } = evaluatePluginConfig(configSource, opts.configFile);
51
- if (config === null) return yield* Effect.fail(new ExportError({ message: `No PnpmConfigPlugin call found in ${opts.configFile}` }));
52
- if (errors.length > 0) return yield* Effect.fail(new ExportError({ message: `Non-literal config values: ${errors.join("; ")}` }));
53
- const resolvedConfig = withResolvedBuildPatches(config, dirname(opts.configFile));
45
+ const { config, localCfg, path, parsed } = yield* loadConfigAndWorkspace(opts, (message) => new ExportError({ message }));
46
+ const pluginConfig = config;
47
+ const owned = discoverOwnedPatches(pluginConfig, dirname(opts.configFile));
48
+ const resolvedConfig = withResolvedBuildPatches(pluginConfig, dirname(opts.configFile), owned);
54
49
  const { base, manifest } = yield* freeze(resolvedConfig).pipe(Effect.mapError((e) => new ExportError({ message: e.message })));
55
50
  const managed = {};
56
51
  for (const [k, v] of Object.entries(base)) if (WORKSPACE_FIELDS.has(k)) managed[k] = v;
57
- const path = opts.workspacePath ?? findWorkspaceFile(process.cwd()) ?? join(process.cwd(), "pnpm-workspace.yaml");
58
- const parsed = existsSync(path) ? yield* Effect.try({
59
- try: () => parseWorkspace(readFileSync(path, "utf8")),
60
- catch: (e) => new ExportError({ message: `Cannot read or parse ${path}: ${String(e)}` })
61
- }) : {};
62
52
  const rootName = resolveRootName({ dir: dirname(path) });
63
- const localCfg = config.local && typeof config.local === "object" ? config.local : void 0;
64
53
  const effective = effectiveManaged(managed, localCfg, parsed, manifest, rootName);
65
- const rawPatched = config.patchedDependencies;
66
- const explicitPatchMap = rawPatched !== void 0 && !isRewriteDirective(rawPatched);
54
+ const explicitPatchMap = owned === void 0;
67
55
  const contributed = {};
68
56
  if (explicitPatchMap) {
69
- const explicit = effective.patchedDependencies;
70
- if (explicit !== null && typeof explicit === "object" && !Array.isArray(explicit)) Object.assign(contributed, explicit);
57
+ if (Predicate.isObject(effective.patchedDependencies)) Object.assign(contributed, effective.patchedDependencies);
71
58
  } else {
72
- const localPatchesDir = readLocalPatchesDir(config);
73
- const owned = discoverPatches({
74
- baseDir: dirname(opts.configFile),
75
- name: typeof config.name === "string" ? config.name : "",
76
- ...localPatchesDir !== void 0 ? { localPatchesDir } : {}
77
- });
78
59
  const workspaceRoot = dirname(path);
79
60
  for (const p of owned) contributed[p.key] = relative(workspaceRoot, p.absPath).split(/[\\/]/).join("/");
80
61
  }
81
62
  if (Object.keys(contributed).length > 0) effective.patchedDependencies = {
82
- ...(parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed.patchedDependencies : void 0) ?? {},
63
+ ...parsed.patchedDependencies ?? {},
83
64
  ...contributed
84
65
  };
85
66
  const report = reconcilePatches({
@@ -89,7 +70,7 @@ function runExport(opts) {
89
70
  });
90
71
  const merged = overlayWorkspace(effective, parsed);
91
72
  const rendered = renderWorkspace(merged);
92
- const localKeys = new Set(config.local && typeof config.local === "object" ? Object.keys(config.local) : []);
73
+ const localKeys = new Set(Object.keys(localCfg ?? {}));
93
74
  const tree = buildDiff(canonicalize(parsed), canonicalize(merged), {
94
75
  localKeys,
95
76
  managedKeys: WORKSPACE_FIELDS
@@ -1,17 +1,14 @@
1
1
  import { freeze } from "../../plugin/freeze.js";
2
2
  import { resolveRootName } from "../../runtime/ctx.js";
3
- import { evaluatePluginConfig } from "../evaluate.js";
3
+ import { loadConfigAndWorkspace } from "../load-config.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
7
  import { legendLines } from "../ui/legend.js";
8
- import { findWorkspaceFile, parseWorkspace } from "../workspace-file.js";
9
8
  import { WORKSPACE_FIELDS } from "./export.js";
10
9
  import { buildPreviewViews } from "../preview-views.js";
11
- import { runPreview } from "../ui/run-preview.js";
12
10
  import { Data, Effect, Option } from "effect";
13
- import { existsSync, readFileSync } from "node:fs";
14
- import { dirname, join } from "node:path";
11
+ import { dirname } from "node:path";
15
12
  import { Argument, Command } from "effect/unstable/cli";
16
13
 
17
14
  //#region src/cli/commands/preview.ts
@@ -25,22 +22,10 @@ var PreviewError = class extends Data.TaggedError("PreviewError") {};
25
22
  */
26
23
  function runPreviewViews(opts) {
27
24
  return Effect.gen(function* () {
28
- const configSource = yield* Effect.try({
29
- try: () => readFileSync(opts.configFile, "utf8"),
30
- catch: () => new PreviewError({ message: `Cannot read ${opts.configFile}` })
31
- });
32
- const { config, errors } = evaluatePluginConfig(configSource, opts.configFile);
33
- if (config === null) return yield* Effect.fail(new PreviewError({ message: `No PnpmConfigPlugin call found in ${opts.configFile}` }));
34
- if (errors.length > 0) return yield* Effect.fail(new PreviewError({ message: `Non-literal config values: ${errors.join("; ")}` }));
25
+ const { config, localCfg, path, parsed } = yield* loadConfigAndWorkspace(opts, (message) => new PreviewError({ message }));
35
26
  const { base, manifest } = yield* freeze(config).pipe(Effect.mapError((e) => new PreviewError({ message: e.message })));
36
27
  const managed = {};
37
28
  for (const [k, v] of Object.entries(base)) if (WORKSPACE_FIELDS.has(k)) managed[k] = v;
38
- const path = opts.workspacePath ?? findWorkspaceFile(process.cwd()) ?? join(process.cwd(), "pnpm-workspace.yaml");
39
- const parsed = existsSync(path) ? yield* Effect.try({
40
- try: () => parseWorkspace(readFileSync(path, "utf8")),
41
- catch: (e) => new PreviewError({ message: `Cannot read or parse ${path}: ${String(e)}` })
42
- }) : {};
43
- const localCfg = config.local && typeof config.local === "object" ? config.local : void 0;
44
29
  return buildPreviewViews({
45
30
  managed,
46
31
  ...localCfg ? { local: localCfg } : {},
@@ -68,8 +53,10 @@ const previewCommand = Command.make("preview", { path: pathArg }, ({ path }) =>
68
53
  ...workspacePath !== void 0 ? { workspacePath } : {}
69
54
  });
70
55
  const caps = detectCapabilities();
71
- if (caps.interactive) yield* runPreview(views);
72
- else yield* Effect.sync(() => {
56
+ if (caps.interactive) {
57
+ const { runPreview } = yield* Effect.promise(() => import("../ui/run-preview.js"));
58
+ yield* runPreview(views);
59
+ } else yield* Effect.sync(() => {
73
60
  const legend = caps.color ? `${toAnsi(legendLines(), { color: caps.color })}\n\n` : "";
74
61
  process.stdout.write(`${legend}${toAnsi(views.changes, { color: caps.color })}\n`);
75
62
  });
@@ -1,19 +1,18 @@
1
+ import { bareVersion } from "../../semver-util.js";
1
2
  import { evaluatePluginConfig } from "../evaluate.js";
2
3
  import { discoverCatalogEntries } from "../discover.js";
3
4
  import { filterEntriesByCatalog, findConfigFiles, pickConfigCandidate } from "../select-file.js";
4
5
  import { detectCapabilities } from "../ui/env.js";
5
- import { derivePeerRange } from "../peer-range.js";
6
- import { detectPeerDrift } from "../drift.js";
7
6
  import { versionKeyOf } from "../version-key.js";
8
- import { buildEdits } from "../edits.js";
9
- import { buildInteropEdits, interopEntryChanged, runInterop } from "../interop.js";
7
+ import { buildEdits, entryEdits } from "../edits.js";
8
+ import { buildInteropEdits, interopEntryChanged, makePeerFetcher, runInterop } from "../interop.js";
10
9
  import { buildGroupModel, computeGroupPeers } from "../interop-live.js";
11
- import { planEntry } from "../plan.js";
10
+ import { derivePeerRange } from "../peer-range.js";
11
+ import { defaultPick, planEntry } from "../plan.js";
12
12
  import { parsePnpmGate, readConfigReleaseAge } from "../release-age.js";
13
13
  import { RegistryResolver, RegistryResolverLive } from "../resolve.js";
14
14
  import { applyEdits } from "../rewrite.js";
15
15
  import { renderSummary } from "../summary.js";
16
- import { runWalk } from "../ui/run-walk.js";
17
16
  import { validateEdits } from "../validate.js";
18
17
  import { buildWalkItems } from "../walk-plan.js";
19
18
  import { findWorkspaceRoot, makeWorkspaceResolver } from "../workspace-resolve.js";
@@ -31,12 +30,30 @@ import { ReleaseAgeGate } from "@effected/npm";
31
30
  * @internal
32
31
  */
33
32
  var UpgradeError = class extends Data.TaggedError("UpgradeError") {};
33
+ /** Read a config file and statically discover its catalog entries. @internal */
34
+ function readCatalogSource(file) {
35
+ return Effect.gen(function* () {
36
+ const source = yield* Effect.try({
37
+ try: () => readFileSync(file, "utf8"),
38
+ catch: () => new UpgradeError({ message: `Cannot read ${file}` })
39
+ });
40
+ const { entries, skipped } = yield* Effect.try({
41
+ try: () => discoverCatalogEntries(source, file),
42
+ catch: (e) => new UpgradeError({ message: String(e) })
43
+ });
44
+ return {
45
+ source,
46
+ entries,
47
+ skipped
48
+ };
49
+ });
50
+ }
34
51
  /** Combine the config-declared and pnpm-resolved release-age gates (strictest of both). @internal */
35
52
  function computeGate(source, file, resolver) {
36
53
  return Effect.gen(function* () {
37
- const { config } = yield* Effect.try(() => evaluatePluginConfig(source, file)).pipe(Effect.catch(() => Effect.succeed({ config: null })));
54
+ const { config } = yield* Effect.try(() => evaluatePluginConfig(source, file)).pipe(Effect.orElseSucceed(() => ({ config: null })));
38
55
  const cfg = readConfigReleaseAge(config);
39
- 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" });
56
+ const [age, exc] = yield* Effect.all([resolver.pnpmConfig("minimumReleaseAge").pipe(Effect.orElseSucceed(() => null)), resolver.pnpmConfig("minimumReleaseAgeExclude").pipe(Effect.orElseSucceed(() => null))], { concurrency: "unbounded" });
40
57
  const contributions = [cfg, parsePnpmGate(age, exc)].filter((g) => g !== null);
41
58
  return ReleaseAgeGate.combine(...contributions);
42
59
  });
@@ -70,27 +87,22 @@ function resolveGatedVersions(entries, resolver, gate, now, onProgress, workspac
70
87
  let resolved = 0;
71
88
  onProgress?.(0, total);
72
89
  return Effect.forEach([...pairs], ([key, { pkg, fromWorkspace }]) => Effect.gen(function* () {
73
- const vr = yield* (fromWorkspace && workspace !== void 0 ? workspace : resolver).versions(pkg).pipe(Effect.result);
74
- if (Result.isFailure(vr)) {
75
- onProgress?.(++resolved, total);
76
- return [
77
- key,
78
- pkg,
79
- [],
80
- []
81
- ];
82
- }
83
- if (fromWorkspace) {
84
- onProgress?.(++resolved, total);
85
- return [
86
- key,
87
- pkg,
88
- vr.success,
89
- vr.success
90
- ];
91
- }
92
- const times = gate.ageMinutes > 0 ? yield* resolver.times(pkg).pipe(Effect.catch(() => Effect.succeed({}))) : {};
90
+ const routed = fromWorkspace && workspace !== void 0 ? workspace : resolver;
91
+ const needTimes = !fromWorkspace && gate.ageMinutes > 0;
92
+ const [vr, times] = yield* Effect.all([routed.versions(pkg).pipe(Effect.result), needTimes ? resolver.times(pkg).pipe(Effect.orElseSucceed(() => ({}))) : Effect.succeed({})], { concurrency: "unbounded" });
93
93
  onProgress?.(++resolved, total);
94
+ if (Result.isFailure(vr)) return [
95
+ key,
96
+ pkg,
97
+ [],
98
+ []
99
+ ];
100
+ if (fromWorkspace) return [
101
+ key,
102
+ pkg,
103
+ vr.success,
104
+ vr.success
105
+ ];
94
106
  return [
95
107
  key,
96
108
  pkg,
@@ -137,14 +149,7 @@ function writeResolveProgress(resolved, total) {
137
149
  */
138
150
  function runUpgrade(opts) {
139
151
  return Effect.gen(function* () {
140
- const source = yield* Effect.try({
141
- try: () => readFileSync(opts.file, "utf8"),
142
- catch: () => new UpgradeError({ message: `Cannot read ${opts.file}` })
143
- });
144
- const { entries, skipped } = yield* Effect.try({
145
- try: () => discoverCatalogEntries(source, opts.file),
146
- catch: (e) => new UpgradeError({ message: String(e) })
147
- });
152
+ const { source, entries, skipped } = yield* readCatalogSource(opts.file);
148
153
  const gate = yield* computeGate(source, opts.file, opts.resolver);
149
154
  const versionsByPkg = yield* resolveGatedVersions(entries, opts.resolver, gate, Date.now(), opts.onProgress, opts.workspaceResolver);
150
155
  if (versionsByPkg.unresolved.length > 0) return yield* Effect.fail(new UpgradeError({ message: unresolvedMessage(versionsByPkg.unresolved) }));
@@ -167,70 +172,21 @@ function runUpgrade(opts) {
167
172
  };
168
173
  for (const entry of entries) {
169
174
  if (entry.strategy === "interop") continue;
170
- const versionKey = versionKeyOf(entry);
171
- const versions = versionsByPkg.gated.get(versionKey) ?? [];
172
- const pkg = entry.pkg;
173
- const rangeEdit = (span, value) => ({
174
- span,
175
- text: JSON.stringify(value),
176
- pkg,
177
- versionKey,
178
- kind: "range",
179
- value
180
- });
181
- const peerEdit = (span, value) => ({
182
- span,
183
- text: JSON.stringify(value),
184
- pkg,
185
- versionKey,
186
- kind: "peer",
187
- value
188
- });
189
- const peerInsert = (at, value) => ({
190
- span: [at, at],
191
- text: `, peer: ${JSON.stringify(value)}`,
192
- pkg,
193
- versionKey,
194
- kind: "peer",
195
- value
196
- });
197
- const derived = entry.strategy ? yield* derivePeerRange(entry.currentRange, entry.strategy).pipe(Effect.catch(() => Effect.succeed(null))) : null;
175
+ const versions = versionsByPkg.gated.get(versionKeyOf(entry)) ?? [];
176
+ const { range, setPeer } = entryEdits(entry);
177
+ const derived = entry.strategy ? yield* derivePeerRange(entry.currentRange, entry.strategy).pipe(Effect.orElseSucceed(() => null)) : null;
198
178
  if (derived?.warning) warnings.push(`${entry.pkg}: ${derived.warning.message}`);
199
- if (versions.length === 0) {
200
- const at = entry.rangeSpan[1];
201
- if (entry.peer && entry.strategy) {
202
- const expected = yield* detectPeerDrift(entry).pipe(Effect.catch(() => Effect.succeed(null)));
203
- if (expected !== null) {
204
- edits.push(peerEdit(entry.peer.span, expected));
205
- markChanged(entry);
206
- continue;
207
- }
208
- } else if (!entry.peer && entry.strategy && derived !== null) {
209
- edits.push(peerInsert(at, derived.range));
210
- markChanged(entry);
211
- continue;
212
- }
213
- skipped.push(`${entry.catalog}.${entry.pkg}`);
214
- continue;
215
- }
216
- const candidates = yield* planEntry(entry, versions).pipe(Effect.catch(() => Effect.succeed([])));
217
- const inRange = entry.source === "workspace" ? candidates.find((c) => c.kind !== "keep") : candidates.find((c) => c.kind === "in-range");
218
- const at = entry.rangeSpan[1];
219
- if (inRange) {
220
- edits.push(rangeEdit(entry.rangeSpan, inRange.range));
221
- markChanged(entry, inRange.range);
222
- if (entry.peer && inRange.peerRange) edits.push(peerEdit(entry.peer.span, inRange.peerRange));
223
- else if (!entry.peer && entry.strategy && inRange.peerRange) edits.push(peerInsert(at, inRange.peerRange));
224
- } else if (!entry.peer && entry.strategy && derived !== null) {
225
- edits.push(peerInsert(at, derived.range));
179
+ const peerOnly = derived === null ? null : entry.peer ? derived.range === entry.peer.value ? null : derived.range : derived.range;
180
+ const candidates = versions.length === 0 ? [] : yield* planEntry(entry, versions).pipe(Effect.orElseSucceed(() => []));
181
+ const pick = defaultPick(entry, candidates);
182
+ if (pick) {
183
+ edits.push(range(pick.range));
184
+ markChanged(entry, pick.range);
185
+ if (pick.peerRange) edits.push(setPeer(pick.peerRange));
186
+ } else if (peerOnly !== null) {
187
+ edits.push(setPeer(peerOnly));
226
188
  markChanged(entry);
227
- } else if (entry.peer && entry.strategy) {
228
- const expected = yield* detectPeerDrift(entry).pipe(Effect.catch(() => Effect.succeed(null)));
229
- if (expected !== null) {
230
- edits.push(peerEdit(entry.peer.span, expected));
231
- markChanged(entry);
232
- }
233
- }
189
+ } else if (versions.length === 0) skipped.push(`${entry.catalog}.${entry.pkg}`);
234
190
  }
235
191
  const interopEntries = entries.filter((e) => e.strategy === "interop");
236
192
  const conflicts = [];
@@ -244,8 +200,8 @@ function runUpgrade(opts) {
244
200
  const members = [];
245
201
  for (const e of group) {
246
202
  const versions = versionsByPkg.gated.get(versionKeyOf(e)) ?? [];
247
- const inRange = (yield* planEntry(e, versions).pipe(Effect.catch(() => Effect.succeed([])))).find((c) => c.kind === "in-range");
248
- const ceiling = inRange ? inRange.version : e.currentRange.replace(/^[\^~]/, "");
203
+ const inRange = (yield* planEntry(e, versions).pipe(Effect.orElseSucceed(() => []))).find((c) => c.kind === "in-range");
204
+ const ceiling = inRange ? inRange.version : bareVersion(e.currentRange);
249
205
  members.push({
250
206
  pkg: e.pkg,
251
207
  ceiling,
@@ -336,11 +292,11 @@ function unresolvedMessage(unresolved) {
336
292
  function projectDecisions(items, full) {
337
293
  const out = [];
338
294
  for (const i of items) {
339
- const inRange = i.entry.source === "workspace" ? i.candidates.find((c) => c.kind !== "keep") : i.candidates.find((c) => c.kind === "in-range");
340
- if (inRange) {
295
+ const pick = defaultPick(i.entry, i.candidates);
296
+ if (pick) {
341
297
  out.push({
342
298
  item: i,
343
- chosen: inRange
299
+ chosen: pick
344
300
  });
345
301
  continue;
346
302
  }
@@ -367,17 +323,10 @@ function projectDecisions(items, full) {
367
323
  /** Build the colored preview summary string without writing. @internal */
368
324
  function runUpgradePreview(opts) {
369
325
  return Effect.gen(function* () {
370
- const source = yield* Effect.try({
371
- try: () => readFileSync(opts.file, "utf8"),
372
- catch: () => new UpgradeError({ message: `Cannot read ${opts.file}` })
373
- });
374
- const discovered = yield* Effect.try({
375
- try: () => discoverCatalogEntries(source, opts.file),
376
- catch: (e) => new UpgradeError({ message: String(e) })
377
- });
326
+ const { source, entries } = yield* readCatalogSource(opts.file);
378
327
  const gate = yield* computeGate(source, opts.file, opts.resolver);
379
- const versions = yield* resolveGatedVersions(discovered.entries, opts.resolver, gate, Date.now(), void 0, opts.workspaceResolver);
380
- const items = yield* buildWalkItems(discovered.entries, versions.gated).pipe(Effect.catch((e) => Effect.fail(new UpgradeError({ message: e.message }))));
328
+ const versions = yield* resolveGatedVersions(entries, opts.resolver, gate, Date.now(), void 0, opts.workspaceResolver);
329
+ const items = yield* buildWalkItems(entries, versions.gated).pipe(Effect.mapError((e) => new UpgradeError({ message: e.message })));
381
330
  const text = renderSummary(projectDecisions(items, opts.full), void 0, { color: opts.color ?? false });
382
331
  return versions.unresolved.length > 0 ? `${text}\n⚠ ${unresolvedMessage(versions.unresolved)}` : text;
383
332
  });
@@ -642,18 +591,11 @@ const upgradeCommand = Command.make("upgrade", {
642
591
  }
643
592
  return;
644
593
  }
645
- const source = yield* Effect.try({
646
- try: () => readFileSync(file, "utf8"),
647
- catch: () => new UpgradeError({ message: `Cannot read ${file}` })
648
- });
649
- const discovered = yield* Effect.try({
650
- try: () => discoverCatalogEntries(source, file),
651
- catch: (e) => new UpgradeError({ message: String(e) })
652
- });
594
+ const { source, entries: discovered } = yield* readCatalogSource(file);
653
595
  const catalogName = Option.getOrUndefined(catalog);
654
- const entries = filterEntriesByCatalog(discovered.entries, catalogName);
596
+ const entries = filterEntriesByCatalog(discovered, catalogName);
655
597
  const versions = yield* resolveGatedVersions(entries, resolver, yield* computeGate(source, file, resolver), Date.now(), caps.interactive ? writeResolveProgress : void 0, workspaceResolver);
656
- const items = yield* buildWalkItems(entries, versions.gated).pipe(Effect.catch((e) => Effect.fail(new UpgradeError({ message: e.message }))));
598
+ const items = yield* buildWalkItems(entries, versions.gated).pipe(Effect.mapError((e) => new UpgradeError({ message: e.message })));
657
599
  if (!caps.interactive) {
658
600
  const text = renderSummary(projectDecisions(items, full), void 0, { color: caps.color });
659
601
  const note = dryRun ? "(dry run — nothing written)" : "(non-interactive terminal — run with --yes to apply, or in a TTY to choose)";
@@ -673,26 +615,18 @@ const upgradeCommand = Command.make("upgrade", {
673
615
  list.push(e);
674
616
  interopByCatalog.set(e.catalog, list);
675
617
  }
676
- const peerCache = /* @__PURE__ */ new Map();
677
- const fetchPeer = (pkg, v) => {
678
- const k = `${pkg}@${v}`;
679
- const cached = peerCache.get(k);
680
- if (cached !== void 0) return Effect.succeed(cached);
681
- return resolver.peerDependencies(pkg, v).pipe(Effect.catch(() => Effect.succeed({})), Effect.map((deps) => {
682
- peerCache.set(k, deps);
683
- return deps;
684
- }));
685
- };
618
+ const fetchPeer = makePeerFetcher(resolver);
686
619
  if (interopByCatalog.size > 0) yield* Effect.sync(() => process.stderr.write("Resolving peer dependencies…\n"));
687
620
  const interopModels = /* @__PURE__ */ new Map();
688
621
  for (const [catalog, group] of interopByCatalog) {
689
622
  const candByPkg = /* @__PURE__ */ new Map();
690
623
  for (const e of group) {
691
624
  const it = items.find((i) => i.entry.catalog === catalog && i.entry.pkg === e.pkg);
692
- candByPkg.set(e.pkg, it ? it.candidates.map((c) => c.version) : [e.currentRange.replace(/^[\^~]/, "")]);
625
+ candByPkg.set(e.pkg, it ? it.candidates.map((c) => c.version) : [bareVersion(e.currentRange)]);
693
626
  }
694
627
  interopModels.set(catalog, yield* buildGroupModel(candByPkg, fetchPeer));
695
628
  }
629
+ const { runWalk } = yield* Effect.promise(() => import("../ui/run-walk.js"));
696
630
  const decisions = yield* runWalk(items, dryRun, versions.unresolved, interopModels);
697
631
  const nonInteropDecisions = decisions.filter((d) => d.item.entry.strategy !== "interop");
698
632
  const interopEdits = [];
@@ -704,14 +638,13 @@ const upgradeCommand = Command.make("upgrade", {
704
638
  const selected = /* @__PURE__ */ new Map();
705
639
  for (const e of group) {
706
640
  const d = decisions.find((dd) => dd.item.entry.catalog === catalog && dd.item.entry.pkg === e.pkg);
707
- selected.set(e.pkg, d ? d.chosen.version : e.currentRange.replace(/^[\^~]/, ""));
641
+ selected.set(e.pkg, d ? d.chosen.version : bareVersion(e.currentRange));
708
642
  }
709
643
  const { peer, conflict } = computeGroupPeers(model, selected);
710
644
  interopEdits.push(...buildInteropEdits(group, {
711
645
  resolved: selected,
712
646
  peers: peer,
713
- conflicts: [],
714
- peerDepsOf: () => ({})
647
+ conflicts: []
715
648
  }));
716
649
  for (const [pkg, blockedBy] of conflict) allConflicts.push({
717
650
  pkg,
@@ -730,10 +663,7 @@ const upgradeCommand = Command.make("upgrade", {
730
663
  const planned = buildEdits(nonInteropDecisions);
731
664
  const { accepted, rejected } = yield* validateEdits(planned, versions.raw);
732
665
  const acceptedPkgs = new Set(accepted.map((e) => e.pkg));
733
- yield* Effect.sync(() => process.stdout.write(`${renderSummary(decisions, {
734
- adjustments: [],
735
- conflicts: allConflicts
736
- }, { color: caps.color }, rejected)}\n`));
666
+ yield* Effect.sync(() => process.stdout.write(`${renderSummary(decisions, { conflicts: allConflicts }, { color: caps.color }, rejected)}\n`));
737
667
  if (!dryRun) yield* applyInteropAndDecisions(file, source, accepted, interopEdits);
738
668
  const changed = countChangedDecisions(nonInteropDecisions.filter((d) => acceptedPkgs.has(d.item.entry.pkg))) + interopChanged;
739
669
  yield* Effect.sync(() => process.stdout.write(dryRun ? `Dry run — no changes written. ${changed} change(s) would be applied.\n` : `Applied ${changed} change(s).\n`));
@@ -741,4 +671,4 @@ const upgradeCommand = Command.make("upgrade", {
741
671
  }).pipe(Effect.provide(RegistryResolverLive), Effect.provide(NodeServices.layer))).pipe(Command.withDescription("Upgrade catalog versions in a config file"));
742
672
 
743
673
  //#endregion
744
- export { UpgradeError, applyInteropAndDecisions, checkFailureOutcome, checkJsonOutcome, checkOutcome, computeGate, countChangedDecisions, nothingToUpgradeMessage, projectDecisions, resolveGatedVersions, resolveTargetFile, runUpgrade, runUpgradePreview, unresolvedMessage, upgradeCommand, upgradeJsonOutcome, validateJsonMode, writeResolveProgress };
674
+ export { UpgradeError, applyInteropAndDecisions, checkFailureOutcome, checkJsonOutcome, checkOutcome, computeGate, countChangedDecisions, nothingToUpgradeMessage, projectDecisions, readCatalogSource, resolveGatedVersions, resolveTargetFile, runUpgrade, runUpgradePreview, unresolvedMessage, upgradeCommand, upgradeJsonOutcome, validateJsonMode, writeResolveProgress };
package/cli/diff/build.js CHANGED
@@ -1,7 +1,7 @@
1
+ import { Predicate } from "effect";
2
+
1
3
  //#region src/cli/diff/build.ts
2
- function isObject(v) {
3
- return v !== null && typeof v === "object" && !Array.isArray(v);
4
- }
4
+ const isObject = Predicate.isObject;
5
5
  /** Worst kind among children: changed if any differs, added/removed if uniform, else unchanged. */
6
6
  function rollup(children) {
7
7
  if (children.length === 0) return "unchanged";
@@ -12,6 +12,7 @@ const STYLE = {
12
12
  changed: "changed",
13
13
  unchanged: "unchanged"
14
14
  };
15
+ /** A scalar as YAML-shaped text: strings verbatim, everything else JSON. @internal */
15
16
  function scalarText(v) {
16
17
  return typeof v === "string" ? v : JSON.stringify(v);
17
18
  }
@@ -101,4 +102,4 @@ function renderExportDiff(root, opts) {
101
102
  }
102
103
 
103
104
  //#endregion
104
- export { renderExportDiff };
105
+ export { renderExportDiff, scalarText };
package/cli/discover.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { findPluginArg, keyName, prop } from "./ast.js";
1
2
  import { Data } from "effect";
2
3
  import { parseSync } from "oxc-parser";
3
4
 
@@ -16,40 +17,6 @@ function operatorOf(range) {
16
17
  return "";
17
18
  }
18
19
  /**
19
- * Find a property value by key name in an ObjectExpression node.
20
- * Handles both Identifier keys (unquoted) and Literal keys (quoted).
21
- */
22
- function prop(obj, key) {
23
- const properties = obj.properties ?? [];
24
- for (const p of properties) {
25
- if (p.type !== "Property") continue;
26
- const k = p.key;
27
- if ((k.type === "Identifier" ? k.name : k.type === "Literal" ? String(k.value) : void 0) === key) return p.value;
28
- }
29
- }
30
- /** Find the first `PnpmConfigPlugin(...)` CallExpression's first argument object. */
31
- function findPluginArg(program) {
32
- let found;
33
- const visit = (node) => {
34
- if (found || node === null || typeof node !== "object") return;
35
- const n = node;
36
- if (n.type === "CallExpression") {
37
- const callee = n.callee;
38
- if (callee?.type === "Identifier" && callee.name === "PnpmConfigPlugin") {
39
- const args = n.arguments;
40
- if (args?.[0]?.type === "ObjectExpression") {
41
- found = args[0];
42
- return;
43
- }
44
- }
45
- }
46
- for (const value of Object.values(n)) if (Array.isArray(value)) value.forEach(visit);
47
- else if (value && typeof value === "object") visit(value);
48
- };
49
- visit(program);
50
- return found;
51
- }
52
- /**
53
20
  * Statically discover the catalog version literals in a config source. Locates
54
21
  * the single `PnpmConfigPlugin(...)` call and walks `.catalogs.<name>.packages`.
55
22
  * Each package whose range is a simple-operator string literal yields a
@@ -76,16 +43,16 @@ function discoverCatalogEntries(source, filename) {
76
43
  };
77
44
  for (const catProp of catalogs.properties ?? []) {
78
45
  if (catProp.type !== "Property") continue;
79
- const catKey = catProp.key;
80
- const catalog = catKey.type === "Identifier" ? catKey.name : String(catKey.value);
46
+ const catalog = keyName(catProp.key);
47
+ if (catalog === void 0) continue;
81
48
  const decl = catProp.value;
82
49
  if (decl.type !== "ObjectExpression") continue;
83
50
  const packages = prop(decl, "packages");
84
51
  if (packages?.type !== "ObjectExpression") continue;
85
52
  for (const pkgProp of packages.properties ?? []) {
86
53
  if (pkgProp.type !== "Property") continue;
87
- const pkgKey = pkgProp.key;
88
- const pkg = pkgKey.type === "Identifier" ? pkgKey.name : String(pkgKey.value);
54
+ const pkg = keyName(pkgProp.key);
55
+ if (pkg === void 0) continue;
89
56
  const value = pkgProp.value;
90
57
  let rangeNode;
91
58
  let peerNode;