rolldown-pnpm-config 2.1.7 → 2.2.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/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.
@@ -15,9 +15,9 @@ import { failureDoc } from "../render/report.js";
15
15
  import { findConfigFiles, pickConfigCandidate } from "../select-file.js";
16
16
  import { overlayWorkspace } from "../workspace-overlay.js";
17
17
  import { Data, Effect, Option, Predicate } from "effect";
18
- import { existsSync, writeFileSync } from "node:fs";
19
18
  import { dirname, relative } from "node:path";
20
19
  import { CliDoc, CliMessage } from "@effected/cli";
20
+ import { existsSync, writeFileSync } from "node:fs";
21
21
  import { Argument, Command, Flag } from "effect/cli";
22
22
 
23
23
  // src/cli/commands/export.ts
@@ -18,10 +18,10 @@ 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, 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";
24
+ import { readFileSync, writeFileSync } from "node:fs";
25
25
  import { Argument, Command, Flag } from "effect/cli";
26
26
  import { CliUi } from "@effected/cli/ui";
27
27
  import { ReleaseAgeGate } from "@effected/npm";
@@ -40,12 +40,12 @@ var UpgradeError = class extends Data.TaggedError("UpgradeError") {
40
40
  };
41
41
  /**
42
42
  * An invalid flag combination: a usage error, so it exits 64 (BSD
43
- * `EX_USAGE`) rather than the 1 a resolution failure exits with.
43
+ * `EX_USAGE`) rather than the 1 a resolution failure exits with. The code is
44
+ * mapped by tag in `EXIT_CODES`, which `main` hands to `CliRuntime.main`.
44
45
  *
45
46
  * @internal
46
47
  */
47
48
  var UpgradeUsageError = class extends Data.TaggedError("UpgradeUsageError") {
48
- [Runtime.errorExitCode] = 64;
49
49
  /** The failure report `CliRuntime.main` prints: the message, without the class name. */
50
50
  [CliDoc]() {
51
51
  return failureDoc(this.message);
@@ -80,7 +80,7 @@ function readCatalogSource(file) {
80
80
  });
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,
@@ -279,7 +279,7 @@ function runUpgrade(opts) {
279
279
  kind: "peer-strategy"
280
280
  }));
281
281
  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")}` }));
282
+ 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
283
  const allEdits = [...accepted, ...interopEdits];
284
284
  if (allEdits.length > 0 && !opts.dryRun) {
285
285
  const next = applyEdits(source, allEdits);
@@ -331,7 +331,7 @@ function applyInteropAndDecisions(file, source, nonInteropEdits, interopEdits) {
331
331
  * @internal
332
332
  */
333
333
  function nothingToUpgradeMessage(totalItems) {
334
- return totalItems === 0 ? "Nothing to upgrade — no catalog packages found." : `Nothing to upgrade — ${totalItems} package(s) already up to date.`;
334
+ return totalItems === 0 ? "Nothing to upgrade — no catalog packages found." : `Nothing to upgrade — ${Fmt.plural(totalItems, "package")} already up to date.`;
335
335
  }
336
336
  /**
337
337
  * The message for packages the registry could not resolve. Almost always a
@@ -342,7 +342,7 @@ function nothingToUpgradeMessage(totalItems) {
342
342
  */
343
343
  function unresolvedMessage(unresolved) {
344
344
  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}`;
345
+ 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
346
  }
347
347
  /** Project walk items to the non-interactive default decisions (latest-in-range, plus peer-only keeps). @internal */
348
348
  function projectDecisions(items, full) {
@@ -413,7 +413,7 @@ function checkOutcome(changed) {
413
413
  return {
414
414
  exitCode: 1,
415
415
  doc: [
416
- Doc.line(`Catalog drift detected in ${changed.length} package(s):`),
416
+ Doc.line(`Catalog drift detected in ${Fmt.plural(changed.length, "package")}:`),
417
417
  Doc.lines(changed.map((c) => ` ${c.name} (${c.source})`), { wrap: false }),
418
418
  Doc.line([
419
419
  "Run ",
@@ -434,12 +434,12 @@ function driftRowJson(row) {
434
434
  };
435
435
  }
436
436
  /**
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.
437
+ * The `--check --format json` outcome. stdout carries exactly one JSON
438
+ * document (`data`, printed by `CliFormat.print` on one line) in EVERY case —
439
+ * including the resolution-failure family, which keeps its non-zero exit but
440
+ * must never leave a bash gate with exit 1 and an empty stdout. A resolution
441
+ * failure is also returned as `failure`, which the command fails with so the
442
+ * labelled human report lands on stderr.
443
443
  *
444
444
  * @internal
445
445
  */
@@ -448,14 +448,14 @@ function checkJsonOutcome(result) {
448
448
  const { message, kind } = result.failure;
449
449
  return {
450
450
  exitCode: 1,
451
- json: JSON.stringify({
451
+ data: {
452
452
  command: "check",
453
453
  inSync: false,
454
454
  error: {
455
455
  kind: kind ?? "resolution",
456
456
  message
457
457
  }
458
- }),
458
+ },
459
459
  failure: new CheckFailedError({
460
460
  message,
461
461
  ...kind !== void 0 ? { kind } : {}
@@ -465,15 +465,15 @@ function checkJsonOutcome(result) {
465
465
  const changed = result.success.changed;
466
466
  return {
467
467
  exitCode: changed.length === 0 ? 0 : 1,
468
- json: JSON.stringify({
468
+ data: {
469
469
  command: "check",
470
470
  inSync: changed.length === 0,
471
471
  drift: changed.map(driftRowJson)
472
- })
472
+ }
473
473
  };
474
474
  }
475
475
  /**
476
- * The `--yes --json` / `--dry-run --json` outcome. `applied` reports whether
476
+ * The `--yes --format json` / `--dry-run --format json` outcome. `applied` reports whether
477
477
  * the run wrote at least one change — always false under dry-run AND on an
478
478
  * already-in-sync run, so a consumer keying on it never reads a no-op as a
479
479
  * real apply; `changed` uses the same row object as check's `drift`. A failure
@@ -487,19 +487,19 @@ function upgradeJsonOutcome(result, dryRun) {
487
487
  if (Result.isFailure(result)) {
488
488
  const { message, kind } = result.failure;
489
489
  return {
490
- json: JSON.stringify({
490
+ data: {
491
491
  command: "upgrade",
492
492
  applied: false,
493
493
  error: {
494
494
  kind: kind ?? "resolution",
495
495
  message
496
496
  }
497
- }),
497
+ },
498
498
  failure: result.failure
499
499
  };
500
500
  }
501
501
  const r = result.success;
502
- return { json: JSON.stringify({
502
+ return { data: {
503
503
  command: "upgrade",
504
504
  applied: !dryRun && r.updated > 0,
505
505
  updated: r.updated,
@@ -510,21 +510,21 @@ function upgradeJsonOutcome(result, dryRun) {
510
510
  ceiling: c.ceiling,
511
511
  blockedBy: c.blockedBy
512
512
  }))
513
- }) };
513
+ } };
514
514
  }
515
515
  /**
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
516
+ * Reject `--format json` outside the non-interactive modes it exists for. JSON
517
+ * mode is a machine contract (a GitHub Action parsing stdout from bash); the
518
518
  * 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.
519
+ * Returns the usage error to fail with (exit 64, see `EXIT_CODES`), or null
520
+ * when the combination is valid.
521
521
  *
522
522
  * @internal
523
523
  */
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" });
524
+ function validateFormatMode(flags) {
525
+ if (flags.format !== "json") return null;
526
+ if (flags.preview) return new UpgradeUsageError({ message: "--format json cannot be combined with --preview; use --check, --yes, or --dry-run" });
527
+ 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
528
  return null;
529
529
  }
530
530
  /**
@@ -607,7 +607,9 @@ const catalogOption = Flag.String("catalog").pipe(Flag.withDescription("Only upg
607
607
  const previewFlag = Flag.Boolean("preview").pipe(Flag.withDescription("Print the default picks as a summary and exit"), Flag.withDefault(false));
608
608
  const fullFlag = Flag.Boolean("full").pipe(Flag.withDescription("Include up-to-date packages in a printed summary"), Flag.withDefault(false));
609
609
  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));
610
+ 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)" });
611
+ /** JSON mode writes one single-line document, so a bash gate can pipe it straight into jq. */
612
+ const ONE_LINE = { indent: 0 };
611
613
  /**
612
614
  * The "upgrade" command. The default path runs the interactive table;
613
615
  * --yes applies latest-in-range non-interactively; --dry-run runs the identical
@@ -624,10 +626,10 @@ const upgradeCommand = Command.make("upgrade", {
624
626
  preview: previewFlag,
625
627
  full: fullFlag,
626
628
  check: checkFlag,
627
- json: jsonFlag
628
- }, ({ file: fileOpt, yes, dryRun, catalog, preview, full, check, json }) => Effect.gen(function* () {
629
- const jsonRejection = validateJsonMode({
630
- json,
629
+ format: formatFlag
630
+ }, ({ file: fileOpt, yes, dryRun, catalog, preview, full, check, format }) => Effect.gen(function* () {
631
+ const jsonRejection = validateFormatMode({
632
+ format,
631
633
  check,
632
634
  yes,
633
635
  dryRun,
@@ -644,26 +646,28 @@ const upgradeCommand = Command.make("upgrade", {
644
646
  })), Effect.result);
645
647
  if (check) {
646
648
  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
- }));
649
+ const json = checkJsonOutcome(result);
650
+ const failure = json.failure;
651
+ if (failure !== void 0) {
652
+ if (format === "json") yield* CliFormat.print("json", {
653
+ doc: () => failure[CliDoc](),
654
+ json: () => json.data
655
+ }, ONE_LINE);
656
+ return yield* Effect.fail(failure);
659
657
  }
660
- const outcome = checkOutcome(result.success.changed);
661
- yield* Doc.print(outcome.doc);
658
+ const outcome = checkOutcome(Result.isSuccess(result) ? result.success.changed : []);
659
+ yield* CliFormat.print(format, {
660
+ doc: () => outcome.doc,
661
+ json: () => json.data
662
+ }, ONE_LINE);
662
663
  return yield* CliExit.set(outcome.exitCode);
663
664
  }
664
- if (json) {
665
+ if (format === "json") {
665
666
  const o = upgradeJsonOutcome(yield* nonInteractive(false), dryRun);
666
- yield* Console.log(o.json);
667
+ yield* CliFormat.print("json", {
668
+ doc: () => [],
669
+ json: () => o.data
670
+ }, ONE_LINE);
667
671
  if (o.failure !== void 0) return yield* Effect.fail(o.failure);
668
672
  return;
669
673
  }
@@ -687,7 +691,7 @@ const upgradeCommand = Command.make("upgrade", {
687
691
  workspaceResolver,
688
692
  onProgress: resolveProgress(report)
689
693
  }));
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}.`);
694
+ 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
695
  if (result.conflicts.length > 0) {
692
696
  const lines = result.conflicts.map((c) => ` ${c.pkg} (kept ${c.ceiling}) blocked by ${c.blockedBy}`);
693
697
  yield* Doc.print(warningDoc(["Interop conflicts (left at your pick):", ...lines].join("\n")), { stream: "stderr" });
@@ -759,8 +763,8 @@ const upgradeCommand = Command.make("upgrade", {
759
763
  yield* Doc.print(summaryDoc(decisions, { conflicts: allConflicts }, rejected));
760
764
  if (!dryRun) yield* applyInteropAndDecisions(file, source, accepted, interopEdits);
761
765
  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).`);
766
+ yield* dryRun ? CliMessage.info(`Dry run — no changes written. ${Fmt.plural(changed, "change")} would be applied.`) : CliMessage.success(`Applied ${Fmt.plural(changed, "change")}.`);
763
767
  yield* warnUnresolved(versions.unresolved);
764
768
  })).pipe(Command.withDescription("Upgrade catalog versions in a config file"));
765
769
 
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 };
770
+ export { CHECK_FAILED_LABEL, CheckFailedError, UpgradeError, UpgradeUsageError, applyInteropAndDecisions, checkJsonOutcome, checkOutcome, computeGate, countChangedDecisions, nothingToUpgradeMessage, projectDecisions, readCatalogSource, resolveGatedVersions, resolveProgress, resolveTargetFile, runUpgrade, runUpgradePreview, unresolvedMessage, upgradeCommand, upgradeJsonOutcome, validateFormatMode };
@@ -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,6 +1,7 @@
1
1
  import { parseWorkspace } from "./workspace-file.js";
2
2
  import { evaluatePluginConfig } from "./evaluate.js";
3
3
  import { Effect, Predicate } from "effect";
4
+ import { Fmt } from "@effected/cli";
4
5
  import { existsSync, readFileSync } from "node:fs";
5
6
 
6
7
  // src/cli/load-config.ts
@@ -24,7 +25,7 @@ function loadConfigAndWorkspace(opts, mkError) {
24
25
  const path = opts.workspacePath;
25
26
  const parsed = existsSync(path) ? yield* Effect.try({
26
27
  try: () => parseWorkspace(readFileSync(path, "utf8")),
27
- catch: (e) => mkError(`Cannot read or parse ${path}: ${String(e)}`)
28
+ catch: (e) => mkError(`Cannot read or parse ${path}: ${Fmt.causeMessage(e) ?? Fmt.describe(e)}`)
28
29
  }) : {};
29
30
  return {
30
31
  config,
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.causeMessage(e) ?? Fmt.describe(e)
89
90
  })));
90
91
  return {
91
92
  versions: (pkg) => pnpmView(pkg, pkg, "versions").pipe(Effect.flatMap((out) => parseVersions(pkg, out))),
@@ -1,27 +1,33 @@
1
1
  import { discoverCatalogEntries } from "./discover.js";
2
- import { Effect } from "effect";
3
- import { readFileSync, readdirSync } from "node:fs";
2
+ import { Effect, 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";
7
+ import { readFileSync } from "node:fs";
5
8
 
6
9
  // src/cli/select-file.ts
10
+ /** Top-level `.ts` FILES of a directory, no dotfiles, sorted. */
11
+ const TS_FILES = Result.getOrThrow(GlobPattern.compileResult("*.ts", GlobPatternOptions.make({ dot: false })));
7
12
  /**
8
13
  * Scan a directory for top-level `.ts` files whose source contains a usable
9
14
  * `PnpmConfigPlugin(...)` catalog (discovery yields at least one entry). Read
10
- * or parse failures are skipped silently. Returns absolute-or-joined paths.
15
+ * or parse failures are skipped silently, and so is an unreadable directory.
16
+ * Returns joined paths in sorted order (`@effected/walker`'s `descendSync`),
17
+ * so a "multiple config files" report lists them the same way on every
18
+ * filesystem.
11
19
  *
12
20
  * @internal
13
21
  */
14
22
  function findConfigFiles(dir) {
15
23
  return Effect.sync(() => {
16
- let names;
17
- try {
18
- names = readdirSync(dir);
19
- } catch {
20
- return [];
21
- }
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
32
  try {
27
33
  const source = readFileSync(path, "utf8");
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.0";
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
1
  import { Result } from "effect";
2
- import { existsSync } from "node:fs";
3
2
  import { dirname, join } from "node:path";
3
+ import { existsSync } from "node:fs";
4
4
  import { Yaml, YamlStringifyOptions } from "@effected/yaml";
5
5
 
6
6
  // src/cli/workspace-file.ts
@@ -1,9 +1,12 @@
1
1
  import { parseVersion } from "../semver-util.js";
2
2
  import { findWorkspaceFile } from "./workspace-file.js";
3
3
  import { ResolveError } from "./resolve.js";
4
- import { Effect, Layer } from "effect";
5
- import { existsSync, readFileSync, readdirSync } from "node:fs";
4
+ import { Effect, Layer, Result } from "effect";
6
5
  import { dirname, join, resolve } from "node:path";
6
+ import { GlobPattern, GlobPatternOptions } from "@effected/glob";
7
+ import { descendSync } from "@effected/walker";
8
+ import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
9
+ import { readFileSync } from "node:fs";
7
10
  import { getWorkspacePackagesSync } from "@effected/workspaces";
8
11
  import { nodeSyncOps } from "@effected/workspaces/node-sync";
9
12
 
@@ -13,6 +16,8 @@ const BUMP_ORDER = {
13
16
  minor: 1,
14
17
  major: 2
15
18
  };
19
+ /** The changeset files of `.changeset/`: top-level `.md` FILES, no dotfiles, sorted. */
20
+ const CHANGESET_FILES = Result.getOrThrow(GlobPattern.compileResult("*.md", GlobPatternOptions.make({ dot: false })));
16
21
  /** Matches one changeset frontmatter line: `"@scope/name": minor` (quotes optional). */
17
22
  const FRONTMATTER_LINE_RE = /^\s*["']?([^"':\s]+)["']?\s*:\s*(major|minor|patch)\s*$/;
18
23
  /**
@@ -62,13 +67,10 @@ function readManifests(rootDir) {
62
67
  function readPendingBumps(rootDir) {
63
68
  const out = /* @__PURE__ */ new Map();
64
69
  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
- }
70
+ const names = Result.getOrElse(descendSync(CHANGESET_FILES, {
71
+ cwd: dir,
72
+ ...nodeWalkerSyncOps
73
+ }), () => []).filter((n) => n !== "README.md");
72
74
  for (const name of names) {
73
75
  let text;
74
76
  try {
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.0",
4
4
  "private": false,
5
5
  "description": "A dogfooding example of our plugin",
6
6
  "repository": {
@@ -36,17 +36,17 @@
36
36
  },
37
37
  "dependencies": {
38
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",
39
+ "@effected/cli": "^0.23.0",
40
+ "@effected/env": "^0.6.0",
41
+ "@effected/glob": "^0.15.0",
42
+ "@effected/npm": "^0.27.1",
43
43
  "@effected/semver": "^0.18.0",
44
- "@effected/walker": "^0.20.1",
45
- "@effected/workspaces": "^0.39.0",
44
+ "@effected/walker": "^0.21.0",
45
+ "@effected/workspaces": "^0.39.1",
46
46
  "@effected/yaml": "^0.25.1",
47
47
  "effect": "^4.0.6",
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);
package/plugin/freeze.js CHANGED
@@ -4,6 +4,7 @@ import { FIELD_REGISTRY } from "../registry.js";
4
4
  import { resolvePeerDependencyRules } from "./allowed-versions.js";
5
5
  import { resolveOverrides } from "./overrides-from-catalogs.js";
6
6
  import { Data, Effect, Schema } from "effect";
7
+ import { Fmt } from "@effected/cli";
7
8
 
8
9
  // src/plugin/freeze.ts
9
10
  /**
@@ -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,
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
  /**