rolldown-pnpm-config 2.1.7 → 2.2.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/README.md CHANGED
@@ -78,7 +78,7 @@ The bundled `rolldown-pnpm-config upgrade` command rewrites the version ranges i
78
78
  ```bash
79
79
  npx rolldown-pnpm-config upgrade
80
80
  # opens a radio-group table, one row per package, then on <Enter>:
81
- # Applied <n> change(s).
81
+ # Applied <n> changes.
82
82
  ```
83
83
 
84
84
  The command shows an interactive table by default — every catalog package at once, one row per package, `●`/`○` bubbles, modeled on `pnpm up -i`. `--yes` takes the latest in-range version for every package without prompting and fails hard on any warning or unresolvable package name; `--dry-run` runs the identical table and skips only the write; `--catalog <name>` restricts the table to one catalog. Pass `--preview` for a non-interactive projection of what an upgrade would do, with `--full` to show up-to-date entries too. The output is colorized in a supporting terminal. For packages that declare a `strategy`, the command also resyncs their materialized peer range, preserving any prerelease identifier rather than rebuilding it from parsed version parts. See [upgrading catalogs](https://github.com/spencerbeggs/rolldown-pnpm-config/blob/main/docs/05-upgrading-catalogs.md) for the full surface.
@@ -14,10 +14,10 @@ import { printView } from "../render/print.js";
14
14
  import { failureDoc } from "../render/report.js";
15
15
  import { findConfigFiles, pickConfigCandidate } from "../select-file.js";
16
16
  import { overlayWorkspace } from "../workspace-overlay.js";
17
- import { Data, Effect, Option, Predicate } from "effect";
18
- import { existsSync, writeFileSync } from "node:fs";
17
+ import { Data, Effect, FileSystem, Option, Predicate } from "effect";
19
18
  import { dirname, relative } from "node:path";
20
19
  import { CliDoc, CliMessage } from "@effected/cli";
20
+ import { existsSync } from "node:fs";
21
21
  import { Argument, Command, Flag } from "effect/cli";
22
22
 
23
23
  // src/cli/commands/export.ts
@@ -90,10 +90,7 @@ function runExport(opts) {
90
90
  diff,
91
91
  report
92
92
  };
93
- yield* Effect.try({
94
- try: () => writeFileSync(path, rendered, "utf8"),
95
- catch: () => new ExportError({ message: `Cannot write ${path}` })
96
- });
93
+ yield* (yield* FileSystem.FileSystem).writeFileString(path, rendered).pipe(Effect.mapError(() => new ExportError({ message: `Cannot write ${path}` })));
97
94
  return {
98
95
  path,
99
96
  rendered,
@@ -18,10 +18,9 @@ import { summaryDoc } from "../summary.js";
18
18
  import { validateEdits } from "../validate.js";
19
19
  import { buildWalkItems } from "../walk-plan.js";
20
20
  import { findWorkspaceRoot, makeWorkspaceResolver } from "../workspace-resolve.js";
21
- import { Clock, Console, Data, Effect, Option, Result, Runtime } from "effect";
22
- import { readFileSync, writeFileSync } from "node:fs";
21
+ import { Clock, Data, Effect, FileSystem, Option, Result } from "effect";
23
22
  import { dirname } from "node:path";
24
- import { CliDoc, CliExit, CliInteractive, CliMessage, Doc } from "@effected/cli";
23
+ import { CliDoc, CliExit, CliFormat, CliInteractive, CliMessage, Doc, Fmt } from "@effected/cli";
25
24
  import { Argument, Command, Flag } from "effect/cli";
26
25
  import { CliUi } from "@effected/cli/ui";
27
26
  import { ReleaseAgeGate } from "@effected/npm";
@@ -40,12 +39,12 @@ var UpgradeError = class extends Data.TaggedError("UpgradeError") {
40
39
  };
41
40
  /**
42
41
  * An invalid flag combination: a usage error, so it exits 64 (BSD
43
- * `EX_USAGE`) rather than the 1 a resolution failure exits with.
42
+ * `EX_USAGE`) rather than the 1 a resolution failure exits with. The code is
43
+ * mapped by tag in `EXIT_CODES`, which `main` hands to `CliRuntime.main`.
44
44
  *
45
45
  * @internal
46
46
  */
47
47
  var UpgradeUsageError = class extends Data.TaggedError("UpgradeUsageError") {
48
- [Runtime.errorExitCode] = 64;
49
48
  /** The failure report `CliRuntime.main` prints: the message, without the class name. */
50
49
  [CliDoc]() {
51
50
  return failureDoc(this.message);
@@ -71,16 +70,17 @@ var CheckFailedError = class extends Data.TaggedError("CheckFailedError") {
71
70
  return failureDoc(`${CHECK_FAILED_LABEL}\n${indent(this.message)}`);
72
71
  }
73
72
  };
73
+ /** Write the rewritten config source back to `file`. */
74
+ function writeSource(file, next) {
75
+ return Effect.flatMap(FileSystem.FileSystem, (fs) => fs.writeFileString(file, next).pipe(Effect.mapError(() => new UpgradeError({ message: `Cannot write ${file}` }))));
76
+ }
74
77
  /** Read a config file and statically discover its catalog entries. @internal */
75
78
  function readCatalogSource(file) {
76
79
  return Effect.gen(function* () {
77
- const source = yield* Effect.try({
78
- try: () => readFileSync(file, "utf8"),
79
- catch: () => new UpgradeError({ message: `Cannot read ${file}` })
80
- });
80
+ const source = yield* (yield* FileSystem.FileSystem).readFileString(file).pipe(Effect.mapError(() => new UpgradeError({ message: `Cannot read ${file}` })));
81
81
  const { entries, skipped } = yield* Effect.try({
82
82
  try: () => discoverCatalogEntries(source, file),
83
- catch: (e) => new UpgradeError({ message: String(e) })
83
+ catch: (e) => new UpgradeError({ message: Fmt.describe(e) })
84
84
  });
85
85
  return {
86
86
  source,
@@ -165,16 +165,15 @@ function resolveGatedVersions(entries, resolver, gate, now, onProgress, workspac
165
165
  * @internal
166
166
  */
167
167
  function resolveProgress(report) {
168
- const noun = (n) => `${n} package${n === 1 ? "" : "s"}`;
169
168
  const finished = (total) => report({
170
169
  _tag: "Finished",
171
- label: `Resolved ${noun(total)}`
170
+ label: `Resolved ${Fmt.plural(total, "package")}`
172
171
  });
173
172
  return (resolved, total) => {
174
173
  if (resolved === 0) {
175
174
  const begin = report({
176
175
  _tag: "Phase",
177
- label: `Resolving ${noun(total)}`,
176
+ label: `Resolving ${Fmt.plural(total, "package")}`,
178
177
  total
179
178
  });
180
179
  return total === 0 ? Effect.andThen(begin, finished(total)) : begin;
@@ -279,14 +278,11 @@ function runUpgrade(opts) {
279
278
  kind: "peer-strategy"
280
279
  }));
281
280
  const { accepted, rejected } = yield* validateEdits(edits, versionsByPkg.raw);
282
- if (rejected.length > 0) return yield* Effect.fail(new UpgradeError({ message: `Refusing to write unsatisfiable range(s):\n${rejected.map((r) => ` ${r.reason}`).join("\n")}` }));
281
+ if (rejected.length > 0) return yield* Effect.fail(new UpgradeError({ message: `Refusing to write ${Fmt.plural(rejected.length, "unsatisfiable range")}:\n${rejected.map((r) => ` ${r.reason}`).join("\n")}` }));
283
282
  const allEdits = [...accepted, ...interopEdits];
284
283
  if (allEdits.length > 0 && !opts.dryRun) {
285
284
  const next = applyEdits(source, allEdits);
286
- yield* Effect.try({
287
- try: () => writeFileSync(opts.file, next, "utf8"),
288
- catch: () => new UpgradeError({ message: `Cannot write ${opts.file}` })
289
- });
285
+ yield* writeSource(opts.file, next);
290
286
  }
291
287
  return {
292
288
  updated: changedSpans.size,
@@ -316,11 +312,7 @@ function applyInteropAndDecisions(file, source, nonInteropEdits, interopEdits) {
316
312
  return Effect.gen(function* () {
317
313
  const edits = [...nonInteropEdits, ...interopEdits];
318
314
  if (edits.length === 0) return;
319
- const next = applyEdits(source, edits);
320
- yield* Effect.try({
321
- try: () => writeFileSync(file, next, "utf8"),
322
- catch: () => new UpgradeError({ message: `Cannot write ${file}` })
323
- });
315
+ yield* writeSource(file, applyEdits(source, edits));
324
316
  });
325
317
  }
326
318
  /**
@@ -331,7 +323,7 @@ function applyInteropAndDecisions(file, source, nonInteropEdits, interopEdits) {
331
323
  * @internal
332
324
  */
333
325
  function nothingToUpgradeMessage(totalItems) {
334
- return totalItems === 0 ? "Nothing to upgrade — no catalog packages found." : `Nothing to upgrade — ${totalItems} package(s) already up to date.`;
326
+ return totalItems === 0 ? "Nothing to upgrade — no catalog packages found." : `Nothing to upgrade — ${Fmt.plural(totalItems, "package")} already up to date.`;
335
327
  }
336
328
  /**
337
329
  * The message for packages the registry could not resolve. Almost always a
@@ -342,7 +334,7 @@ function nothingToUpgradeMessage(totalItems) {
342
334
  */
343
335
  function unresolvedMessage(unresolved) {
344
336
  const list = unresolved.map((p) => ` ${p}`).join("\n");
345
- return `Could not resolve ${unresolved.length} package(s) from the registry — check the name(s) for typos, or your registry auth:\n${list}`;
337
+ return `Could not resolve ${Fmt.plural(unresolved.length, "package")} from the registry — check each name for a typo, or your registry auth:\n${list}`;
346
338
  }
347
339
  /** Project walk items to the non-interactive default decisions (latest-in-range, plus peer-only keeps). @internal */
348
340
  function projectDecisions(items, full) {
@@ -413,7 +405,7 @@ function checkOutcome(changed) {
413
405
  return {
414
406
  exitCode: 1,
415
407
  doc: [
416
- Doc.line(`Catalog drift detected in ${changed.length} package(s):`),
408
+ Doc.line(`Catalog drift detected in ${Fmt.plural(changed.length, "package")}:`),
417
409
  Doc.lines(changed.map((c) => ` ${c.name} (${c.source})`), { wrap: false }),
418
410
  Doc.line([
419
411
  "Run ",
@@ -434,12 +426,12 @@ function driftRowJson(row) {
434
426
  };
435
427
  }
436
428
  /**
437
- * The `--check --json` outcome. stdout carries exactly one single-line JSON
438
- * document (`json`, written with `Console.log`) in EVERY case — including the
439
- * resolution-failure family, which keeps its non-zero exit but must never
440
- * leave a bash gate with exit 1 and an empty stdout. A resolution failure is
441
- * also returned as `failure`, which the command fails with so the labelled
442
- * human report lands on stderr.
429
+ * The `--check --format json` outcome. stdout carries exactly one JSON
430
+ * document (`data`, printed by `CliFormat.print` on one line) in EVERY case —
431
+ * including the resolution-failure family, which keeps its non-zero exit but
432
+ * must never leave a bash gate with exit 1 and an empty stdout. A resolution
433
+ * failure is also returned as `failure`, which the command fails with so the
434
+ * labelled human report lands on stderr.
443
435
  *
444
436
  * @internal
445
437
  */
@@ -448,14 +440,14 @@ function checkJsonOutcome(result) {
448
440
  const { message, kind } = result.failure;
449
441
  return {
450
442
  exitCode: 1,
451
- json: JSON.stringify({
443
+ data: {
452
444
  command: "check",
453
445
  inSync: false,
454
446
  error: {
455
447
  kind: kind ?? "resolution",
456
448
  message
457
449
  }
458
- }),
450
+ },
459
451
  failure: new CheckFailedError({
460
452
  message,
461
453
  ...kind !== void 0 ? { kind } : {}
@@ -465,15 +457,15 @@ function checkJsonOutcome(result) {
465
457
  const changed = result.success.changed;
466
458
  return {
467
459
  exitCode: changed.length === 0 ? 0 : 1,
468
- json: JSON.stringify({
460
+ data: {
469
461
  command: "check",
470
462
  inSync: changed.length === 0,
471
463
  drift: changed.map(driftRowJson)
472
- })
464
+ }
473
465
  };
474
466
  }
475
467
  /**
476
- * The `--yes --json` / `--dry-run --json` outcome. `applied` reports whether
468
+ * The `--yes --format json` / `--dry-run --format json` outcome. `applied` reports whether
477
469
  * the run wrote at least one change — always false under dry-run AND on an
478
470
  * already-in-sync run, so a consumer keying on it never reads a no-op as a
479
471
  * real apply; `changed` uses the same row object as check's `drift`. A failure
@@ -487,19 +479,19 @@ function upgradeJsonOutcome(result, dryRun) {
487
479
  if (Result.isFailure(result)) {
488
480
  const { message, kind } = result.failure;
489
481
  return {
490
- json: JSON.stringify({
482
+ data: {
491
483
  command: "upgrade",
492
484
  applied: false,
493
485
  error: {
494
486
  kind: kind ?? "resolution",
495
487
  message
496
488
  }
497
- }),
489
+ },
498
490
  failure: result.failure
499
491
  };
500
492
  }
501
493
  const r = result.success;
502
- return { json: JSON.stringify({
494
+ return { data: {
503
495
  command: "upgrade",
504
496
  applied: !dryRun && r.updated > 0,
505
497
  updated: r.updated,
@@ -510,21 +502,21 @@ function upgradeJsonOutcome(result, dryRun) {
510
502
  ceiling: c.ceiling,
511
503
  blockedBy: c.blockedBy
512
504
  }))
513
- }) };
505
+ } };
514
506
  }
515
507
  /**
516
- * Reject `--json` outside the non-interactive modes it exists for. JSON mode
517
- * is a machine contract (a GitHub Action parsing stdout from bash); the
508
+ * Reject `--format json` outside the non-interactive modes it exists for. JSON
509
+ * mode is a machine contract (a GitHub Action parsing stdout from bash); the
518
510
  * interactive table and the preview views have no meaningful document to emit.
519
- * Returns the usage error to fail with (exit 64), or null when the combination
520
- * is valid.
511
+ * Returns the usage error to fail with (exit 64, see `EXIT_CODES`), or null
512
+ * when the combination is valid.
521
513
  *
522
514
  * @internal
523
515
  */
524
- function validateJsonMode(flags) {
525
- if (!flags.json) return null;
526
- if (flags.preview) return new UpgradeUsageError({ message: "--json cannot be combined with --preview; use --check, --yes, or --dry-run" });
527
- if (!flags.check && !flags.yes && !flags.dryRun) return new UpgradeUsageError({ message: "--json requires a non-interactive mode: combine it with --check, --yes, or --dry-run" });
516
+ function validateFormatMode(flags) {
517
+ if (flags.format !== "json") return null;
518
+ if (flags.preview) return new UpgradeUsageError({ message: "--format json cannot be combined with --preview; use --check, --yes, or --dry-run" });
519
+ if (!flags.check && !flags.yes && !flags.dryRun) return new UpgradeUsageError({ message: "--format json requires a non-interactive mode: combine it with --check, --yes, or --dry-run" });
528
520
  return null;
529
521
  }
530
522
  /**
@@ -607,7 +599,9 @@ const catalogOption = Flag.String("catalog").pipe(Flag.withDescription("Only upg
607
599
  const previewFlag = Flag.Boolean("preview").pipe(Flag.withDescription("Print the default picks as a summary and exit"), Flag.withDefault(false));
608
600
  const fullFlag = Flag.Boolean("full").pipe(Flag.withDescription("Include up-to-date packages in a printed summary"), Flag.withDefault(false));
609
601
  const checkFlag = Flag.Boolean("check").pipe(Flag.withDescription("Exit 1 when --yes would change anything (a drift gate; never writes)"), Flag.withDefault(false));
610
- const jsonFlag = Flag.Boolean("json").pipe(Flag.withDescription("Print one JSON document on stdout (with --check, --yes or --dry-run)"), Flag.withDefault(false));
602
+ const formatFlag = CliFormat.flag(["auto", "json"], { description: "Output format: auto (for whoever is reading) or json (one JSON document on stdout, with --check, --yes or --dry-run)" });
603
+ /** JSON mode writes one single-line document, so a bash gate can pipe it straight into jq. */
604
+ const ONE_LINE = { indent: 0 };
611
605
  /**
612
606
  * The "upgrade" command. The default path runs the interactive table;
613
607
  * --yes applies latest-in-range non-interactively; --dry-run runs the identical
@@ -624,10 +618,10 @@ const upgradeCommand = Command.make("upgrade", {
624
618
  preview: previewFlag,
625
619
  full: fullFlag,
626
620
  check: checkFlag,
627
- json: jsonFlag
628
- }, ({ file: fileOpt, yes, dryRun, catalog, preview, full, check, json }) => Effect.gen(function* () {
629
- const jsonRejection = validateJsonMode({
630
- json,
621
+ format: formatFlag
622
+ }, ({ file: fileOpt, yes, dryRun, catalog, preview, full, check, format }) => Effect.gen(function* () {
623
+ const jsonRejection = validateFormatMode({
624
+ format,
631
625
  check,
632
626
  yes,
633
627
  dryRun,
@@ -644,26 +638,28 @@ const upgradeCommand = Command.make("upgrade", {
644
638
  })), Effect.result);
645
639
  if (check) {
646
640
  const result = yield* nonInteractive(true);
647
- if (json) {
648
- const o = checkJsonOutcome(result);
649
- yield* Console.log(o.json);
650
- if (o.failure !== void 0) return yield* Effect.fail(o.failure);
651
- return yield* CliExit.set(o.exitCode);
652
- }
653
- if (Result.isFailure(result)) {
654
- const { message, kind } = result.failure;
655
- return yield* Effect.fail(new CheckFailedError({
656
- message,
657
- ...kind !== void 0 ? { kind } : {}
658
- }));
641
+ const json = checkJsonOutcome(result);
642
+ const failure = json.failure;
643
+ if (failure !== void 0) {
644
+ if (format === "json") yield* CliFormat.print("json", {
645
+ doc: () => failure[CliDoc](),
646
+ json: () => json.data
647
+ }, ONE_LINE);
648
+ return yield* Effect.fail(failure);
659
649
  }
660
- const outcome = checkOutcome(result.success.changed);
661
- yield* Doc.print(outcome.doc);
650
+ const outcome = checkOutcome(Result.isSuccess(result) ? result.success.changed : []);
651
+ yield* CliFormat.print(format, {
652
+ doc: () => outcome.doc,
653
+ json: () => json.data
654
+ }, ONE_LINE);
662
655
  return yield* CliExit.set(outcome.exitCode);
663
656
  }
664
- if (json) {
657
+ if (format === "json") {
665
658
  const o = upgradeJsonOutcome(yield* nonInteractive(false), dryRun);
666
- yield* Console.log(o.json);
659
+ yield* CliFormat.print("json", {
660
+ doc: () => [],
661
+ json: () => o.data
662
+ }, ONE_LINE);
667
663
  if (o.failure !== void 0) return yield* Effect.fail(o.failure);
668
664
  return;
669
665
  }
@@ -687,7 +683,7 @@ const upgradeCommand = Command.make("upgrade", {
687
683
  workspaceResolver,
688
684
  onProgress: resolveProgress(report)
689
685
  }));
690
- yield* dryRun ? CliMessage.info(`Dry run — no changes written. ${result.updated} package(s) would be updated; skipped ${result.skipped.length}.`) : CliMessage.success(`Updated ${result.updated} package(s); skipped ${result.skipped.length}.`);
686
+ yield* dryRun ? CliMessage.info(`Dry run — no changes written. ${Fmt.plural(result.updated, "package")} would be updated; skipped ${result.skipped.length}.`) : CliMessage.success(`Updated ${Fmt.plural(result.updated, "package")}; skipped ${result.skipped.length}.`);
691
687
  if (result.conflicts.length > 0) {
692
688
  const lines = result.conflicts.map((c) => ` ${c.pkg} (kept ${c.ceiling}) blocked by ${c.blockedBy}`);
693
689
  yield* Doc.print(warningDoc(["Interop conflicts (left at your pick):", ...lines].join("\n")), { stream: "stderr" });
@@ -759,8 +755,8 @@ const upgradeCommand = Command.make("upgrade", {
759
755
  yield* Doc.print(summaryDoc(decisions, { conflicts: allConflicts }, rejected));
760
756
  if (!dryRun) yield* applyInteropAndDecisions(file, source, accepted, interopEdits);
761
757
  const changed = countChangedDecisions(nonInteropDecisions.filter((d) => acceptedPkgs.has(d.item.entry.pkg))) + interopChanged;
762
- yield* dryRun ? CliMessage.info(`Dry run — no changes written. ${changed} change(s) would be applied.`) : CliMessage.success(`Applied ${changed} change(s).`);
758
+ yield* dryRun ? CliMessage.info(`Dry run — no changes written. ${Fmt.plural(changed, "change")} would be applied.`) : CliMessage.success(`Applied ${Fmt.plural(changed, "change")}.`);
763
759
  yield* warnUnresolved(versions.unresolved);
764
760
  })).pipe(Command.withDescription("Upgrade catalog versions in a config file"));
765
761
 
766
- export { CHECK_FAILED_LABEL, CheckFailedError, UpgradeError, UpgradeUsageError, applyInteropAndDecisions, checkJsonOutcome, checkOutcome, computeGate, countChangedDecisions, nothingToUpgradeMessage, projectDecisions, readCatalogSource, resolveGatedVersions, resolveProgress, resolveTargetFile, runUpgrade, runUpgradePreview, unresolvedMessage, upgradeCommand, upgradeJsonOutcome, validateJsonMode };
762
+ export { CHECK_FAILED_LABEL, CheckFailedError, UpgradeError, UpgradeUsageError, applyInteropAndDecisions, checkJsonOutcome, checkOutcome, computeGate, countChangedDecisions, nothingToUpgradeMessage, projectDecisions, readCatalogSource, resolveGatedVersions, resolveProgress, resolveTargetFile, runUpgrade, runUpgradePreview, unresolvedMessage, upgradeCommand, upgradeJsonOutcome, validateFormatMode };
package/cli/diff/build.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Predicate } from "effect";
1
+ import { Order, Predicate } from "effect";
2
2
 
3
3
  // src/cli/diff/build.ts
4
4
  const isObject = Predicate.isObject;
@@ -49,7 +49,7 @@ function diffArray(key, path, before, after, tag) {
49
49
  const here = [...path, key];
50
50
  const b = new Set(before.map(String));
51
51
  const a = new Set(after.map(String));
52
- const children = [.../* @__PURE__ */ new Set([...b, ...a])].sort((x, y) => x.localeCompare(y)).map((el) => {
52
+ const children = [.../* @__PURE__ */ new Set([...b, ...a])].sort(Order.String).map((el) => {
53
53
  const inB = b.has(el);
54
54
  const inA = a.has(el);
55
55
  const kind = inB && inA ? "unchanged" : inA ? "added" : "removed";
@@ -73,7 +73,7 @@ function diffArray(key, path, before, after, tag) {
73
73
  function diffValue(key, path, before, after, tag) {
74
74
  const here = [...path, key];
75
75
  if (isObject(before) && isObject(after)) {
76
- const children = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort((x, y) => x.localeCompare(y)).map((k) => {
76
+ const children = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort(Order.String).map((k) => {
77
77
  if (!(k in before)) return uniform(k, here, after[k], "added");
78
78
  if (!(k in after)) return uniform(k, here, before[k], "removed");
79
79
  return diffValue(k, here, before[k], after[k]);
@@ -104,7 +104,7 @@ function diffValue(key, path, before, after, tag) {
104
104
  * @internal
105
105
  */
106
106
  function buildDiff(before, after, meta) {
107
- const children = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort((x, y) => x.localeCompare(y)).map((k) => {
107
+ const children = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort(Order.String).map((k) => {
108
108
  const tag = meta.localKeys.has(k) ? "local" : meta.managedKeys.has(k) ? void 0 : "unmanaged";
109
109
  if (!(k in before)) return uniform(k, [], after[k], "added", tag);
110
110
  if (!(k in after)) return uniform(k, [], before[k], "removed", tag);
@@ -0,0 +1,16 @@
1
+ // src/cli/exit-codes.ts
2
+ /**
3
+ * Exit codes by failure tag, handed to `CliRuntime.main`'s `exitCodes`. A
4
+ * typed failure whose `_tag` is a key here exits with that code; every other
5
+ * failure falls back to 1. The kit owns the remaining codes: a parse-time
6
+ * usage error is 64 (`usageExitCode`) and a cancelled screen is 130.
7
+ *
8
+ * `UpgradeUsageError` is the command's own invalid flag combination (`--format
9
+ * json` outside `--check`/`--yes`/`--dry-run`), so it shares the parser's 64
10
+ * (BSD `EX_USAGE`) rather than a resolution failure's 1.
11
+ *
12
+ * @internal
13
+ */
14
+ const EXIT_CODES = { UpgradeUsageError: 64 };
15
+
16
+ export { EXIT_CODES };
@@ -1,5 +1,5 @@
1
1
  import { bareVersion, parseRange, parseVersion } from "../semver-util.js";
2
- import "./interop.js";
2
+ import { lowestVersion } from "./interop.js";
3
3
  import { Effect } from "effect";
4
4
 
5
5
  // src/cli/interop-live.ts
@@ -28,19 +28,7 @@ function computeGroupPeers(model, selected) {
28
28
  for (const m of model.members) {
29
29
  const v = selected.get(m);
30
30
  if (v === void 0) continue;
31
- const declared = declaredFloors.get(m);
32
- if (declared?.length) {
33
- let minStr = declared[0];
34
- let minSv = model.ver.get(minStr) ?? null;
35
- for (const f of declared.slice(1)) {
36
- const sv = model.ver.get(f) ?? null;
37
- if (sv && (minSv === null || sv.compare(minSv) < 0)) {
38
- minSv = sv;
39
- minStr = f;
40
- }
41
- }
42
- peer.set(m, `^${minStr}`);
43
- } else peer.set(m, `^${v}`);
31
+ peer.set(m, `^${lowestVersion(declaredFloors.get(m) ?? []) ?? v}`);
44
32
  }
45
33
  const conflict = /* @__PURE__ */ new Map();
46
34
  for (const m of model.members) {
package/cli/interop.js CHANGED
@@ -50,7 +50,13 @@ function deriveFloors(resolved, fetchPeer) {
50
50
  return out;
51
51
  });
52
52
  }
53
- /** The lowest parseable version string in `list`, or null when none parses. */
53
+ /**
54
+ * The lowest parseable version string in `list`, or null when none parses.
55
+ * Shared by the `--yes` resolver and the interactive table so both write the
56
+ * same peer floor.
57
+ *
58
+ * @internal
59
+ */
54
60
  function lowestVersion(list) {
55
61
  let best = null;
56
62
  for (const f of list) {
@@ -223,4 +229,4 @@ function buildInteropEdits(entries, result) {
223
229
  return edits;
224
230
  }
225
231
 
226
- export { buildInteropEdits, deriveFloors, interopEntryChanged, makePeerFetcher, resolveGroup, runInterop };
232
+ export { buildInteropEdits, deriveFloors, interopEntryChanged, lowestVersion, makePeerFetcher, resolveGroup, runInterop };
@@ -1,7 +1,7 @@
1
1
  import { parseWorkspace } from "./workspace-file.js";
2
2
  import { evaluatePluginConfig } from "./evaluate.js";
3
- import { Effect, Predicate } from "effect";
4
- import { existsSync, readFileSync } from "node:fs";
3
+ import { Effect, FileSystem, Predicate } from "effect";
4
+ import { Fmt } from "@effected/cli";
5
5
 
6
6
  // src/cli/load-config.ts
7
7
  /**
@@ -10,22 +10,26 @@ import { existsSync, readFileSync } from "node:fs";
10
10
  * parse the workspace file when present. `mkError` wraps each failure
11
11
  * message in the command's own error type.
12
12
  *
13
+ * IO goes through the platform `FileSystem`. Only a workspace file that is
14
+ * NOT FOUND reads as absent (`fs.exists` maps `NotFound` to `false`); an
15
+ * existence check that fails for any other reason (EACCES, ENOTDIR, EIO) is a
16
+ * `Cannot read or parse` failure, never a silent "create it fresh".
17
+ *
13
18
  * @internal
14
19
  */
15
20
  function loadConfigAndWorkspace(opts, mkError) {
16
21
  return Effect.gen(function* () {
17
- const configSource = yield* Effect.try({
18
- try: () => readFileSync(opts.configFile, "utf8"),
19
- catch: () => mkError(`Cannot read ${opts.configFile}`)
20
- });
22
+ const fs = yield* FileSystem.FileSystem;
23
+ const configSource = yield* fs.readFileString(opts.configFile).pipe(Effect.mapError(() => mkError(`Cannot read ${opts.configFile}`)));
21
24
  const { config, errors } = evaluatePluginConfig(configSource, opts.configFile);
22
25
  if (config === null) return yield* Effect.fail(mkError(`No PnpmConfigPlugin call found in ${opts.configFile}`));
23
26
  if (errors.length > 0) return yield* Effect.fail(mkError(`Non-literal config values: ${errors.join("; ")}`));
24
27
  const path = opts.workspacePath;
25
- const parsed = existsSync(path) ? yield* Effect.try({
26
- try: () => parseWorkspace(readFileSync(path, "utf8")),
27
- catch: (e) => mkError(`Cannot read or parse ${path}: ${String(e)}`)
28
- }) : {};
28
+ const cannotRead = (e) => mkError(`Cannot read or parse ${path}: ${Fmt.errorText(e)}`);
29
+ const parsed = (yield* fs.exists(path).pipe(Effect.mapError(cannotRead))) ? yield* fs.readFileString(path).pipe(Effect.mapError(cannotRead), Effect.flatMap((text) => Effect.try({
30
+ try: () => parseWorkspace(text),
31
+ catch: cannotRead
32
+ }))) : {};
29
33
  return {
30
34
  config,
31
35
  localCfg: Predicate.isObject(config.local) ? config.local : void 0,
@@ -1,4 +1,4 @@
1
- import { Predicate } from "effect";
1
+ import { Array as Array$1, Predicate } from "effect";
2
2
 
3
3
  // src/cli/local-merge.ts
4
4
  /** Default protocols whose existing-file override entries are preserved. @internal */
@@ -30,9 +30,7 @@ function combine(managed, value, strategy) {
30
30
  if (Array.isArray(managed) || Array.isArray(value)) {
31
31
  const m = Array.isArray(managed) ? managed : [];
32
32
  const v = Array.isArray(value) ? value : [];
33
- if (strategy === "union") return [.../* @__PURE__ */ new Set([...m, ...v])];
34
- const drop = new Set(v.map((x) => JSON.stringify(x)));
35
- return m.filter((x) => !drop.has(JSON.stringify(x)));
33
+ return strategy === "union" ? Array$1.dedupe(Array$1.appendAll(m, v)) : Array$1.difference(m, v);
36
34
  }
37
35
  const m = Predicate.isObject(managed) ? managed : {};
38
36
  const v = Predicate.isObject(value) ? value : {};
package/cli/main.js CHANGED
@@ -1,16 +1,19 @@
1
1
  import { WorkingDirectory } from "./cwd.js";
2
+ import { EXIT_CODES } from "./exit-codes.js";
2
3
  import { RegistryResolverLive } from "./resolve.js";
3
4
  import { rootCommand } from "./root.js";
4
5
  import { CLI_VERSION } from "./version.js";
5
6
  import { Effect } from "effect";
6
- import { NodeRuntime, NodeServices } from "@effect/platform-node";
7
7
  import { CliAudience, CliRuntime } from "@effected/cli";
8
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
8
9
 
9
10
  // src/cli/main.ts
10
11
  /**
11
12
  * Assemble and run the CLI. One of the three files allowed to read `process`:
12
- * the working directory, argv for the build-time log format, and stderr's
13
- * terminal state are read here and passed down as plain values.
13
+ * the working directory and argv for the build-time log format are read here
14
+ * and passed down as plain values. Stderr's terminal state is not: the
15
+ * platform's `Stdio` (in `NodeServices.layer`) answers it, which is where the
16
+ * kit reads it from when `env.stderrIsTerminal` is omitted.
14
17
  *
15
18
  * The registry resolver (`pnpm view`, spawned through the platform) is
16
19
  * provided here, at the edge, so a test drives the same command tree against
@@ -18,7 +21,8 @@ import { CliAudience, CliRuntime } from "@effected/cli";
18
21
  *
19
22
  * `CliRuntime.main` builds the audience, terminal and theme from `env`,
20
23
  * decides whether the run may prompt, reports every failure on stderr for the
21
- * run's audience, and maps the exit code (usage 64, cancel 130, otherwise 1).
24
+ * run's audience, and maps the exit code (usage 64, cancel 130, `EXIT_CODES`
25
+ * by tag, otherwise 1).
22
26
  *
23
27
  * @internal
24
28
  */
@@ -27,13 +31,13 @@ function main() {
27
31
  NodeRuntime.runMain(CliRuntime.main(program, {
28
32
  platform: NodeServices.layer,
29
33
  helpOnUsageError: "stderr",
34
+ exitCodes: EXIT_CODES,
30
35
  env: {
31
36
  audienceEnvVar: "ROLLDOWN_PNPM_CONFIG_AUDIENCE",
32
37
  log: {
33
38
  envVar: "ROLLDOWN_PNPM_CONFIG_LOG_LEVEL",
34
39
  argv: process.argv.slice(2)
35
- },
36
- stderrIsTerminal: Effect.sync(() => process.stderr.isTTY === true)
40
+ }
37
41
  }
38
42
  }));
39
43
  }
package/cli/resolve.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Context, Data, Effect, Layer, Predicate } from "effect";
2
+ import { Fmt } from "@effected/cli";
2
3
  import { ChildProcess, ChildProcessSpawner } from "effect/process";
3
4
 
4
5
  // src/cli/resolve.ts
@@ -85,7 +86,7 @@ const RegistryResolverLive = Layer.effect(RegistryResolver, Effect.gen(function*
85
86
  "--json"
86
87
  ])).pipe(Effect.map(stripPnpmNotices), Effect.mapError((e) => new ResolveError({
87
88
  pkg,
88
- message: String(e)
89
+ message: Fmt.errorText(e)
89
90
  })));
90
91
  return {
91
92
  versions: (pkg) => pnpmView(pkg, pkg, "versions").pipe(Effect.flatMap((out) => parseVersions(pkg, out))),
@@ -1,33 +1,35 @@
1
1
  import { discoverCatalogEntries } from "./discover.js";
2
- import { Effect } from "effect";
3
- import { readFileSync, readdirSync } from "node:fs";
2
+ import { Effect, FileSystem, Result } from "effect";
4
3
  import { join } from "node:path";
4
+ import { GlobPattern, GlobPatternOptions } from "@effected/glob";
5
+ import { descendSync } from "@effected/walker";
6
+ import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
5
7
 
6
8
  // src/cli/select-file.ts
9
+ /** Top-level `.ts` FILES of a directory, no dotfiles, sorted. */
10
+ const TS_FILES = Result.getOrThrow(GlobPattern.compileResult("*.ts", GlobPatternOptions.make({ dot: false })));
7
11
  /**
8
12
  * Scan a directory for top-level `.ts` files whose source contains a usable
9
13
  * `PnpmConfigPlugin(...)` catalog (discovery yields at least one entry). Read
10
- * or parse failures are skipped silently. Returns absolute-or-joined paths.
14
+ * or parse failures are skipped silently, and so is an unreadable directory.
15
+ * Returns joined paths in sorted order (`@effected/walker`'s `descendSync`),
16
+ * so a "multiple config files" report lists them the same way on every
17
+ * filesystem.
11
18
  *
12
19
  * @internal
13
20
  */
14
21
  function findConfigFiles(dir) {
15
- return Effect.sync(() => {
16
- let names;
17
- try {
18
- names = readdirSync(dir);
19
- } catch {
20
- return [];
21
- }
22
+ return Effect.gen(function* () {
23
+ const fs = yield* FileSystem.FileSystem;
24
+ const names = Result.getOrElse(descendSync(TS_FILES, {
25
+ cwd: dir,
26
+ ...nodeWalkerSyncOps
27
+ }), () => []);
22
28
  const matches = [];
23
29
  for (const name of names) {
24
- if (!name.endsWith(".ts") || name.endsWith(".d.ts")) continue;
30
+ if (name.endsWith(".d.ts")) continue;
25
31
  const path = join(dir, name);
26
- try {
27
- const source = readFileSync(path, "utf8");
28
- const { entries } = discoverCatalogEntries(source, path);
29
- if (entries.length > 0) matches.push(path);
30
- } catch {}
32
+ if (yield* fs.readFileString(path).pipe(Effect.flatMap((source) => Effect.try(() => discoverCatalogEntries(source, path).entries.length > 0)), Effect.orElseSucceed(() => false))) matches.push(path);
31
33
  }
32
34
  return matches;
33
35
  });
@@ -1,6 +1,7 @@
1
1
  import { canonicalize } from "./workspace-file.js";
2
2
  import { row, tone } from "./render/tone.js";
3
3
  import { scalarText } from "./diff/render.js";
4
+ import { Predicate } from "effect";
4
5
  import { Doc } from "@effected/cli";
5
6
 
6
7
  // src/cli/simulated-view.ts
@@ -42,7 +43,7 @@ function annotation(entry) {
42
43
  /** Flatten one key/value into YAML-shaped plain lines; `ann` annotates the head. */
43
44
  function flatten(key, value, depth, ann) {
44
45
  if (Array.isArray(value)) return [row(" ", depth, [tone(`${key}:`, "plain"), ...ann]), ...value.map((el) => row(" ", depth + 1, [tone(`- ${scalarText(el)}`, "plain")]))];
45
- if (value !== null && typeof value === "object") return [row(" ", depth, [tone(`${key}:`, "plain"), ...ann]), ...Object.entries(value).flatMap(([k, v]) => flatten(k, v, depth + 1, []))];
46
+ if (Predicate.isObject(value)) return [row(" ", depth, [tone(`${key}:`, "plain"), ...ann]), ...Object.entries(value).flatMap(([k, v]) => flatten(k, v, depth + 1, []))];
46
47
  return [row(" ", depth, [tone(`${key}: ${scalarText(value)}`, "plain"), ...ann])];
47
48
  }
48
49
  /**
package/cli/summary.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { row, tone } from "./render/tone.js";
2
2
  import { cellTone, displayCandidates, peerFor, tableLayout } from "./walk-reducer.js";
3
- import { Doc } from "@effected/cli";
3
+ import { Doc, Fmt } from "@effected/cli";
4
4
 
5
5
  // src/cli/summary.ts
6
6
  /**
@@ -27,7 +27,7 @@ function summaryDoc(decisions, interop, rejected) {
27
27
  lines.push(row(" ", 0, [tone(`── catalog: ${entry.catalog} ──`, "unchanged")]));
28
28
  }
29
29
  const cells = displayCandidates(item);
30
- const content = [tone(entry.pkg.padEnd(pkgWidth + 2), "plain")];
30
+ const content = [tone(Fmt.pad(entry.pkg, pkgWidth + 2), "plain")];
31
31
  for (const c of cells) {
32
32
  const selected = c.kind === chosen.kind;
33
33
  content.push(tone(cellText(c, selected), cellTone(c, selected) ?? "unchanged"));
@@ -1,6 +1,6 @@
1
1
  import { TONES } from "../render/tone.js";
2
2
  import { computeGroupPeers } from "../interop-live.js";
3
- import { cellTone, displayCandidates, initTable, peerFor, tableDecisions, tableLayout, tableStep, truncateEnd } from "../walk-reducer.js";
3
+ import { cellTone, displayCandidates, initTable, peerFor, tableDecisions, tableLayout, tableStep } from "../walk-reducer.js";
4
4
  import { Fmt } from "@effected/cli";
5
5
  import { KeyHelp, KeyTable, Styled, useKeys, useTerminalSize } from "@effected/cli/ui";
6
6
  import { Box, Text } from "ink";
@@ -104,7 +104,7 @@ function makeWalkScreen(props) {
104
104
  const gp = interopPeers.get(item.entry.catalog);
105
105
  const peerText = gp?.peer.get(item.entry.pkg) ?? (chosen === void 0 ? "—" : peerFor(item, chosen));
106
106
  const conflict = gp?.conflict.get(item.entry.pkg);
107
- const room = Math.max(8, columns - fixedPrefix - peerText.length - 3);
107
+ const room = Math.max(8, columns - fixedPrefix - Fmt.width(peerText) - 3);
108
108
  rows.push(/* @__PURE__ */ jsxs(Text, {
109
109
  wrap: "truncate-end",
110
110
  children: [
@@ -114,7 +114,7 @@ function makeWalkScreen(props) {
114
114
  }) : " ",
115
115
  /* @__PURE__ */ jsx(Text, {
116
116
  bold: onCursor,
117
- children: Fmt.sanitize(item.entry.pkg).padEnd(pkgWidth + 2)
117
+ children: Fmt.pad(Fmt.sanitize(item.entry.pkg), pkgWidth + 2)
118
118
  }),
119
119
  candidates.map((c, ci) => {
120
120
  const t = cellTone(c, ci === pick);
@@ -131,11 +131,11 @@ function makeWalkScreen(props) {
131
131
  }),
132
132
  conflict ? /* @__PURE__ */ jsx(Styled, {
133
133
  token: "failure",
134
- children: truncateEnd(` ⚠ needs ${Fmt.sanitize(conflict)}`, room)
134
+ children: Fmt.truncate(` ⚠ needs ${Fmt.sanitize(conflict)}`, room)
135
135
  }) : null,
136
136
  item.peerWarning ? /* @__PURE__ */ jsx(Styled, {
137
137
  token: "failure",
138
- children: truncateEnd(` ⚠ ${Fmt.sanitize(item.peerWarning.message)}`, room)
138
+ children: Fmt.truncate(` ⚠ ${Fmt.sanitize(item.peerWarning.message)}`, room)
139
139
  }) : null
140
140
  ]
141
141
  }, `${item.entry.catalog}/${item.entry.pkg}`));
package/cli/version.js CHANGED
@@ -6,6 +6,6 @@
6
6
  *
7
7
  * @internal
8
8
  */
9
- const CLI_VERSION = "2.1.7";
9
+ const CLI_VERSION = "2.2.1";
10
10
 
11
11
  export { CLI_VERSION };
@@ -1,3 +1,5 @@
1
+ import { Fmt } from "@effected/cli";
2
+
1
3
  // src/cli/walk-reducer.ts
2
4
  const ORDER = {
3
5
  keep: 0,
@@ -17,35 +19,25 @@ const BUBBLE_WIDTH = 2;
17
19
  * Compute the table geometry once for a set of rows. The range and the major
18
20
  * suffix are padded TOGETHER so a major annotation never shifts a later
19
21
  * column, and every row emits `maxCells` cells so the peer separator lands
20
- * in the same column regardless of how many candidates a row has.
22
+ * in the same column regardless of how many candidates a row has. Widths are
23
+ * terminal columns (`Fmt.width`) and padding is `Fmt.pad`, so a wide character
24
+ * in a name or range keeps every later column aligned.
21
25
  *
22
26
  * @internal
23
27
  */
24
28
  function tableLayout(items) {
25
29
  const cells = items.map(displayCandidates);
26
- const pkgWidth = items.length ? Math.max(...items.map((i) => i.entry.pkg.length)) : 0;
27
- const cellWidth = items.length ? Math.max(...cells.flatMap((cs) => cs.map((c) => c.range.length + (c.isMajor ? 8 : 0)))) : 0;
30
+ const pkgWidth = items.length ? Math.max(...items.map((i) => Fmt.width(i.entry.pkg))) : 0;
31
+ const cellWidth = items.length ? Math.max(...cells.flatMap((cs) => cs.map((c) => Fmt.width(`${c.range}${c.isMajor ? MAJOR_SUFFIX : ""}`)))) : 0;
28
32
  return {
29
33
  pkgWidth,
30
34
  cellWidth,
31
35
  maxCells: items.length ? Math.max(...cells.map((cs) => cs.length)) : 0,
32
36
  blankCell: `${" ".repeat(BUBBLE_WIDTH + cellWidth)} `,
33
- cellText: (c, selected) => `${selected ? "●" : "○"} ${`${c.range}${c.isMajor ? MAJOR_SUFFIX : ""}`.padEnd(cellWidth)} `
37
+ cellText: (c, selected) => `${selected ? "●" : "○"} ${Fmt.pad(`${c.range}${c.isMajor ? MAJOR_SUFFIX : ""}`, cellWidth)} `
34
38
  };
35
39
  }
36
40
  /**
37
- * Truncate `s` to at most `max` display columns, appending `…` when clipped.
38
- * Keeps a long peer-conflict annotation from wrapping and breaking the table's
39
- * column alignment. A `max` of 1 or less yields just the ellipsis.
40
- *
41
- * @internal
42
- */
43
- function truncateEnd(s, max) {
44
- if (s.length <= max) return s;
45
- if (max <= 1) return "…";
46
- return `${s.slice(0, max - 1)}…`;
47
- }
48
- /**
49
41
  * The row's options in display order: keep first (always index 0, always the
50
42
  * default), then the in-range bump, then the latest-overall bump. `planEntry`
51
43
  * emits them in the opposite order, with keep last.
@@ -146,4 +138,4 @@ function cellTone(candidate, selected) {
146
138
  return candidate.isMajor ? "changed" : "added";
147
139
  }
148
140
 
149
- export { MAJOR_SUFFIX, cellTone, displayCandidates, initTable, peerFor, tableDecisions, tableLayout, tableStep, truncateEnd };
141
+ export { MAJOR_SUFFIX, cellTone, displayCandidates, initTable, peerFor, tableDecisions, tableLayout, tableStep };
@@ -1,6 +1,6 @@
1
- import { Result } from "effect";
2
- import { existsSync } from "node:fs";
3
- import { dirname, join } from "node:path";
1
+ import { Order, Predicate, Result } from "effect";
2
+ import { locateWorkspaceRootSync } from "@effected/workspaces";
3
+ import { nodeSyncOps } from "@effected/workspaces/node-sync";
4
4
  import { Yaml, YamlStringifyOptions } from "@effected/yaml";
5
5
 
6
6
  // src/cli/workspace-file.ts
@@ -23,31 +23,28 @@ function allPrimitive(arr) {
23
23
  function canonicalize(value) {
24
24
  if (Array.isArray(value)) {
25
25
  const mapped = value.map(canonicalize);
26
- return allPrimitive(mapped) ? [...mapped].sort((a, b) => String(a).localeCompare(String(b))) : mapped;
26
+ return allPrimitive(mapped) ? [...mapped].sort((a, b) => Order.String(String(a), String(b))) : mapped;
27
27
  }
28
- if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, canonicalize(v)]));
28
+ if (Predicate.isObject(value)) return Object.fromEntries(Object.entries(value).sort(([a], [b]) => Order.String(a, b)).map(([k, v]) => [k, canonicalize(v)]));
29
29
  return value;
30
30
  }
31
31
  /**
32
32
  * Walk up from `startDir` to the nearest directory containing a
33
- * pnpm-workspace.yaml; returns the file path or null.
33
+ * pnpm-workspace.yaml; returns the file path or null. A `package.json`
34
+ * `workspaces` field is not a marker here: this tool targets pnpm only.
34
35
  *
35
36
  * @internal
36
37
  */
37
38
  function findWorkspaceFile(startDir) {
38
- let dir = startDir;
39
- for (;;) {
40
- const candidate = join(dir, FILENAME);
41
- if (existsSync(candidate)) return candidate;
42
- const parent = dirname(dir);
43
- if (parent === dir) return null;
44
- dir = parent;
45
- }
39
+ return locateWorkspaceRootSync(startDir, {
40
+ ...nodeSyncOps,
41
+ markers: [FILENAME]
42
+ })?.path ?? null;
46
43
  }
47
44
  /** Parse pnpm-workspace.yaml source; empty/whitespace yields an empty object. @internal */
48
45
  function parseWorkspace(source) {
49
46
  const parsed = Result.getOrThrow(Yaml.parseResult(source));
50
- return parsed && typeof parsed === "object" ? parsed : {};
47
+ return Predicate.isObject(parsed) ? parsed : {};
51
48
  }
52
49
  /** Render a workspace object: deterministic key sort + yaml.stringify. @internal */
53
50
  function renderWorkspace(obj) {
@@ -1,10 +1,12 @@
1
1
  import { parseVersion } from "../semver-util.js";
2
- import { findWorkspaceFile } from "./workspace-file.js";
3
2
  import { ResolveError } from "./resolve.js";
4
- import { Effect, Layer } from "effect";
5
- import { existsSync, readFileSync, readdirSync } from "node:fs";
6
- import { dirname, join, resolve } from "node:path";
7
- import { getWorkspacePackagesSync } from "@effected/workspaces";
3
+ import { Effect, Layer, Result } from "effect";
4
+ import { join, resolve } from "node:path";
5
+ import { GlobPattern, GlobPatternOptions } from "@effected/glob";
6
+ import { descendSync } from "@effected/walker";
7
+ import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
8
+ import { readFileSync } from "node:fs";
9
+ import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/workspaces";
8
10
  import { nodeSyncOps } from "@effected/workspaces/node-sync";
9
11
 
10
12
  // src/cli/workspace-resolve.ts
@@ -13,6 +15,8 @@ const BUMP_ORDER = {
13
15
  minor: 1,
14
16
  major: 2
15
17
  };
18
+ /** The changeset files of `.changeset/`: top-level `.md` FILES, no dotfiles, sorted. */
19
+ const CHANGESET_FILES = Result.getOrThrow(GlobPattern.compileResult("*.md", GlobPatternOptions.make({ dot: false })));
16
20
  /** Matches one changeset frontmatter line: `"@scope/name": minor` (quotes optional). */
17
21
  const FRONTMATTER_LINE_RE = /^\s*["']?([^"':\s]+)["']?\s*:\s*(major|minor|patch)\s*$/;
18
22
  /**
@@ -62,13 +66,10 @@ function readManifests(rootDir) {
62
66
  function readPendingBumps(rootDir) {
63
67
  const out = /* @__PURE__ */ new Map();
64
68
  const dir = join(rootDir, ".changeset");
65
- if (!existsSync(dir)) return out;
66
- let names;
67
- try {
68
- names = readdirSync(dir).filter((n) => n.endsWith(".md") && n !== "README.md");
69
- } catch {
70
- return out;
71
- }
69
+ const names = Result.getOrElse(descendSync(CHANGESET_FILES, {
70
+ cwd: dir,
71
+ ...nodeWalkerSyncOps
72
+ }), () => []).filter((n) => n !== "README.md");
72
73
  for (const name of names) {
73
74
  let text;
74
75
  try {
@@ -157,8 +158,10 @@ function makeWorkspaceResolver(rootDir) {
157
158
  * @internal
158
159
  */
159
160
  function findWorkspaceRoot(startDir) {
160
- const file = findWorkspaceFile(resolve(startDir));
161
- return file === null ? resolve(startDir) : dirname(file);
161
+ return findWorkspaceRootSync(startDir, {
162
+ ...nodeSyncOps,
163
+ markers: ["pnpm-workspace.yaml"]
164
+ }) ?? resolve(startDir);
162
165
  }
163
166
 
164
167
  export { findWorkspaceRoot, makeWorkspaceResolver, readManifests, readWorkspaceVersions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rolldown-pnpm-config",
3
- "version": "2.1.7",
3
+ "version": "2.2.1",
4
4
  "private": false,
5
5
  "description": "A dogfooding example of our plugin",
6
6
  "repository": {
@@ -35,18 +35,18 @@
35
35
  "rolldown-pnpm-config": "bin/rolldown-pnpm-config.js"
36
36
  },
37
37
  "dependencies": {
38
- "@effect/platform-node": "^4.0.6",
39
- "@effected/cli": "^0.22.2",
40
- "@effected/env": "^0.5.0",
41
- "@effected/glob": "^0.14.0",
42
- "@effected/npm": "^0.27.0",
43
- "@effected/semver": "^0.18.0",
44
- "@effected/walker": "^0.20.1",
45
- "@effected/workspaces": "^0.39.0",
46
- "@effected/yaml": "^0.25.1",
47
- "effect": "^4.0.6",
38
+ "@effect/platform-node": "^4.0.7",
39
+ "@effected/cli": "^0.24.0",
40
+ "@effected/env": "^0.7.0",
41
+ "@effected/glob": "^0.16.0",
42
+ "@effected/npm": "^0.28.0",
43
+ "@effected/semver": "^0.19.0",
44
+ "@effected/walker": "^0.22.0",
45
+ "@effected/workspaces": "^0.40.0",
46
+ "@effected/yaml": "^0.26.0",
47
+ "effect": "^4.0.7",
48
48
  "ink": "^8.0.0",
49
- "oxc-parser": "^0.155.0",
49
+ "oxc-parser": "^0.156.0",
50
50
  "react": "^19.3.1"
51
51
  },
52
52
  "peerDependencies": {
@@ -1,14 +1,26 @@
1
1
  import { patchKeyFromFileName } from "./keys.js";
2
2
  import { distributedPatchPath, distributedRel } from "./paths.js";
3
- import { existsSync, readdirSync } from "node:fs";
3
+ import { Result } from "effect";
4
4
  import { isAbsolute, join } from "node:path";
5
+ import { GlobPattern, GlobPatternOptions } from "@effected/glob";
6
+ import { descendSync } from "@effected/walker";
7
+ import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
5
8
 
6
9
  // src/patches/discover.ts
7
10
  /**
11
+ * The patch files of one convention folder: top-level `.patch` FILES only
12
+ * (a directory named `x.patch` is not a patch), no dotfiles, sorted.
13
+ */
14
+ const PATCH_FILES = Result.getOrThrow(GlobPattern.compileResult("*.patch", GlobPatternOptions.make({ dot: false })));
15
+ /**
8
16
  * Discover owned patches in the two convention folders adjacent to the build
9
17
  * file: `public/patches/` (distributed, rewritten) and `patches/` (local-only).
10
18
  * `localPatchesDir` overrides the distributed source root only. Read-only.
11
19
  *
20
+ * Each folder is listed with `@effected/walker`'s `descendSync`: an absent
21
+ * folder yields nothing, and an unreadable one throws its `DescendError`, as
22
+ * the bare `readdirSync` this replaced threw its `EACCES`.
23
+ *
12
24
  * @internal
13
25
  */
14
26
  function discoverPatches(opts) {
@@ -19,8 +31,11 @@ function discoverPatches(opts) {
19
31
  collect(localOnlyRoot, false);
20
32
  return out;
21
33
  function collect(dir, distributed) {
22
- if (!existsSync(dir)) return;
23
- for (const fileName of readdirSync(dir).sort()) {
34
+ const files = Result.getOrThrow(descendSync(PATCH_FILES, {
35
+ cwd: dir,
36
+ ...nodeWalkerSyncOps
37
+ }));
38
+ for (const fileName of files) {
24
39
  const key = patchKeyFromFileName(fileName);
25
40
  if (key === null) continue;
26
41
  const absPath = join(dir, fileName);
@@ -1,5 +1,6 @@
1
1
  import { rangeOf } from "../catalogs.js";
2
2
  import { bareVersion } from "../semver-util.js";
3
+ import { Predicate } from "effect";
3
4
 
4
5
  // src/plugin/allowed-versions.ts
5
6
  /** An exact version: no range operator, no wildcard — `X.Y.Z` with an optional prerelease. */
@@ -43,7 +44,7 @@ function deriveAllowedVersions(catalogs, directives) {
43
44
  * @internal
44
45
  */
45
46
  function resolvePeerDependencyRules(value, catalogs) {
46
- if (value === null || typeof value !== "object" || !("allowedVersionsFromCatalogs" in value)) return value;
47
+ if (!Predicate.isObject(value) || !("allowedVersionsFromCatalogs" in value)) return value;
47
48
  const { allowedVersionsFromCatalogs, ...rest } = value;
48
49
  const merged = {
49
50
  ...deriveAllowedVersions(catalogs, allowedVersionsFromCatalogs),
package/plugin/freeze.js CHANGED
@@ -3,7 +3,8 @@ import { DESCRIPTORS, deriveSchemas } from "../descriptors/index.js";
3
3
  import { FIELD_REGISTRY } from "../registry.js";
4
4
  import { resolvePeerDependencyRules } from "./allowed-versions.js";
5
5
  import { resolveOverrides } from "./overrides-from-catalogs.js";
6
- import { Data, Effect, Schema } from "effect";
6
+ import { Data, Effect, Predicate, Schema } from "effect";
7
+ import { Fmt } from "@effected/cli";
7
8
 
8
9
  // src/plugin/freeze.ts
9
10
  /**
@@ -32,7 +33,7 @@ const WRAPPED_KEYS = /* @__PURE__ */ new Set([
32
33
  * @internal
33
34
  */
34
35
  function isWrappedField(input) {
35
- if (input === null || typeof input !== "object") return false;
36
+ if (!Predicate.isObject(input)) return false;
36
37
  const keys = Object.keys(input);
37
38
  return keys.includes("value") && keys.every((k) => WRAPPED_KEYS.has(k));
38
39
  }
@@ -57,13 +58,13 @@ function freeze(config) {
57
58
  return Effect.gen(function* () {
58
59
  const base = {};
59
60
  const manifest = {};
60
- base.catalogs = yield* Schema.decodeUnknownEffect(CatalogsSchema)(normalizeCatalogs(config.catalogs)).pipe(Effect.mapError((error) => new ConfigError({ message: `Invalid catalogs: ${String(error)}` })));
61
+ base.catalogs = yield* Schema.decodeUnknownEffect(CatalogsSchema)(normalizeCatalogs(config.catalogs)).pipe(Effect.mapError((error) => new ConfigError({ message: `Invalid catalogs: ${Fmt.describe(error)}` })));
61
62
  manifest.catalogs = {
62
63
  strategy: "catalogs",
63
64
  enforcement: "warn"
64
65
  };
65
66
  if (typeof config.name !== "string" || config.name.trim() === "") return yield* Effect.fail(new ConfigError({ message: "Config `name` is required and must be a non-empty string" }));
66
- const directiveError = (error) => new ConfigError({ message: error instanceof Error ? error.message : String(error) });
67
+ const directiveError = (error) => new ConfigError({ message: Fmt.describe(error) });
67
68
  for (const [field, reg] of Object.entries(FIELD_REGISTRY)) {
68
69
  if (field === "catalogs") continue;
69
70
  let raw = config[field];
@@ -78,7 +79,7 @@ function freeze(config) {
78
79
  try: () => resolveOverrides(decl.value, config.overridesFromCatalogs, config.catalogs),
79
80
  catch: directiveError
80
81
  }) : decl.value;
81
- base[field] = schema ? yield* Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError((error) => new ConfigError({ message: `Invalid ${field}: ${String(error)}` }))) : value;
82
+ base[field] = schema ? yield* Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError((error) => new ConfigError({ message: `Invalid ${field}: ${Fmt.describe(error)}` }))) : value;
82
83
  manifest[field] = {
83
84
  strategy: reg.strategy,
84
85
  enforcement: decl.enforcement ?? reg.enforcement,
@@ -1,5 +1,6 @@
1
1
  import { rangeOf } from "../catalogs.js";
2
2
  import { bareVersion } from "../semver-util.js";
3
+ import { Predicate } from "effect";
3
4
 
4
5
  // src/plugin/overrides-from-catalogs.ts
5
6
  /** A plain release version: `X.Y.Z`, no prerelease, no build metadata. */
@@ -58,7 +59,7 @@ function deriveOverrides(catalogs, directives) {
58
59
  function resolveOverrides(authored, directives, catalogs) {
59
60
  if (directives === void 0) return authored;
60
61
  const derived = deriveOverrides(catalogs, directives);
61
- const manual = authored !== null && typeof authored === "object" ? authored : {};
62
+ const manual = Predicate.isObject(authored) ? authored : {};
62
63
  return {
63
64
  ...derived,
64
65
  ...manual
@@ -1,8 +1,10 @@
1
+ import { Order, Predicate } from "effect";
2
+
1
3
  // src/plugin/serialize.ts
2
4
  /** Recursively sort object keys for deterministic output; arrays keep order. */
3
5
  function sortKeys(value) {
4
6
  if (Array.isArray(value)) return value.map(sortKeys);
5
- if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, sortKeys(v)]));
7
+ if (Predicate.isObject(value)) return Object.fromEntries(Object.entries(value).sort(([a], [b]) => Order.String(a, b)).map(([k, v]) => [k, sortKeys(v)]));
6
8
  return value;
7
9
  }
8
10
  /** Source for the `catalogs` virtual module: a sorted Map literal (plain-JS branch). */
package/runtime/ctx.js CHANGED
@@ -1,5 +1,5 @@
1
- import { readFileSync } from "node:fs";
2
1
  import { join } from "node:path";
2
+ import { readFileSync } from "node:fs";
3
3
 
4
4
  // src/runtime/ctx.ts
5
5
  /**
@@ -1,9 +1,11 @@
1
1
  // src/runtime/strategies/arrays.ts
2
+ /** Code-unit order: locale-independent, so the render is the same on every machine. */
3
+ const byCodeUnit = (a, b) => a < b ? -1 : a > b ? 1 : 0;
2
4
  /** Union `managed` with `local` (when present), deduped and sorted for a stable render. @internal */
3
5
  function unionSort(managed, local) {
4
6
  const set = new Set(managed);
5
7
  for (const item of local ?? []) set.add(item);
6
- return [...set].sort((a, b) => a.localeCompare(b));
8
+ return [...set].sort(byCodeUnit);
7
9
  }
8
10
  /**
9
11
  * Union + sort string arrays; child entries are added to the managed set. Quiet.
@@ -24,7 +26,7 @@ const arrayRecordUnion = (base, local) => {
24
26
  const child = local ?? {};
25
27
  const keys = /* @__PURE__ */ new Set([...Object.keys(managed), ...Object.keys(child)]);
26
28
  const result = {};
27
- for (const key of [...keys].sort((a, b) => a.localeCompare(b))) {
29
+ for (const key of [...keys].sort(byCodeUnit)) {
28
30
  const merged = unionSort(managed[key] ?? [], child[key]);
29
31
  if (merged.length > 0) result[key] = merged;
30
32
  }
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.59.4"
8
+ "packageVersion": "7.60.0"
9
9
  }
10
10
  ]
11
11
  }