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 +1 -1
- package/cli/commands/export.js +3 -6
- package/cli/commands/upgrade.js +69 -73
- package/cli/diff/build.js +4 -4
- package/cli/exit-codes.js +16 -0
- package/cli/interop-live.js +2 -14
- package/cli/interop.js +8 -2
- package/cli/load-config.js +14 -10
- package/cli/local-merge.js +2 -4
- package/cli/main.js +10 -6
- package/cli/resolve.js +2 -1
- package/cli/select-file.js +18 -16
- package/cli/simulated-view.js +2 -1
- package/cli/summary.js +2 -2
- package/cli/ui/walk-screen.js +5 -5
- package/cli/version.js +1 -1
- package/cli/walk-reducer.js +9 -17
- package/cli/workspace-file.js +12 -15
- package/cli/workspace-resolve.js +17 -14
- package/package.json +12 -12
- package/patches/discover.js +18 -3
- package/plugin/allowed-versions.js +2 -1
- package/plugin/freeze.js +6 -5
- package/plugin/overrides-from-catalogs.js +2 -1
- package/plugin/serialize.js +3 -1
- package/runtime/ctx.js +1 -1
- package/runtime/strategies/arrays.js +4 -2
- package/tsdoc-metadata.json +1 -1
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>
|
|
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.
|
package/cli/commands/export.js
CHANGED
|
@@ -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.
|
|
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,
|
package/cli/commands/upgrade.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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:
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
438
|
-
* document (`
|
|
439
|
-
* resolution-failure family, which keeps its non-zero exit but
|
|
440
|
-
* leave a bash gate with exit 1 and an empty stdout. A resolution
|
|
441
|
-
* also returned as `failure`, which the command fails with so the
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
525
|
-
if (
|
|
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
|
|
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
|
-
|
|
628
|
-
}, ({ file: fileOpt, yes, dryRun, catalog, preview, full, check,
|
|
629
|
-
const jsonRejection =
|
|
630
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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*
|
|
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*
|
|
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
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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 };
|
package/cli/interop-live.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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 };
|
package/cli/load-config.js
CHANGED
|
@@ -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 {
|
|
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
|
|
18
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
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,
|
package/cli/local-merge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
13
|
-
*
|
|
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,
|
|
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:
|
|
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))),
|
package/cli/select-file.js
CHANGED
|
@@ -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
|
|
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.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 (
|
|
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
|
});
|
package/cli/simulated-view.js
CHANGED
|
@@ -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
|
|
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
|
|
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"));
|
package/cli/ui/walk-screen.js
CHANGED
|
@@ -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
|
|
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
|
|
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)
|
|
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:
|
|
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:
|
|
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
package/cli/walk-reducer.js
CHANGED
|
@@ -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
|
|
27
|
-
const cellWidth = items.length ? Math.max(...cells.flatMap((cs) => cs.map((c) => c.range
|
|
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 : ""}
|
|
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
|
|
141
|
+
export { MAJOR_SUFFIX, cellTone, displayCandidates, initTable, peerFor, tableDecisions, tableLayout, tableStep };
|
package/cli/workspace-file.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Result } from "effect";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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)
|
|
26
|
+
return allPrimitive(mapped) ? [...mapped].sort((a, b) => Order.String(String(a), String(b))) : mapped;
|
|
27
27
|
}
|
|
28
|
-
if (value
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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) {
|
package/cli/workspace-resolve.js
CHANGED
|
@@ -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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
|
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.
|
|
39
|
-
"@effected/cli": "^0.
|
|
40
|
-
"@effected/env": "^0.
|
|
41
|
-
"@effected/glob": "^0.
|
|
42
|
-
"@effected/npm": "^0.
|
|
43
|
-
"@effected/semver": "^0.
|
|
44
|
-
"@effected/walker": "^0.
|
|
45
|
-
"@effected/workspaces": "^0.
|
|
46
|
-
"@effected/yaml": "^0.
|
|
47
|
-
"effect": "^4.0.
|
|
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.
|
|
49
|
+
"oxc-parser": "^0.156.0",
|
|
50
50
|
"react": "^19.3.1"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
package/patches/discover.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { patchKeyFromFileName } from "./keys.js";
|
|
2
2
|
import { distributedPatchPath, distributedRel } from "./paths.js";
|
|
3
|
-
import {
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
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: ${
|
|
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:
|
|
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}: ${
|
|
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
|
|
62
|
+
const manual = Predicate.isObject(authored) ? authored : {};
|
|
62
63
|
return {
|
|
63
64
|
...derived,
|
|
64
65
|
...manual
|
package/plugin/serialize.js
CHANGED
|
@@ -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
|
|
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,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(
|
|
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(
|
|
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
|
}
|