rolldown-pnpm-config 0.1.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/LICENSE +21 -0
- package/README.md +127 -0
- package/bin/rolldown-pnpm-config.js +21 -0
- package/catalogs.js +27 -0
- package/cli/commands/export.js +118 -0
- package/cli/commands/preview.js +73 -0
- package/cli/commands/upgrade.js +437 -0
- package/cli/diff/build.js +122 -0
- package/cli/diff/render.js +103 -0
- package/cli/discover.js +128 -0
- package/cli/drift.js +22 -0
- package/cli/edits.js +41 -0
- package/cli/effective.js +42 -0
- package/cli/evaluate.js +91 -0
- package/cli/interop.js +285 -0
- package/cli/local-merge.js +75 -0
- package/cli/peer-range.js +34 -0
- package/cli/plan.js +66 -0
- package/cli/preview-views.js +34 -0
- package/cli/release-age.js +74 -0
- package/cli/resolve.js +109 -0
- package/cli/rewrite.js +22 -0
- package/cli/select-file.js +64 -0
- package/cli/summary.js +137 -0
- package/cli/ui/Preview.js +60 -0
- package/cli/ui/Walk.js +55 -0
- package/cli/ui/ansi.js +20 -0
- package/cli/ui/env.js +20 -0
- package/cli/ui/run-preview.js +23 -0
- package/cli/ui/run-walk.js +29 -0
- package/cli/ui/styled.js +27 -0
- package/cli/walk-plan.js +35 -0
- package/cli/walk-reducer.js +61 -0
- package/cli/workspace-file.js +58 -0
- package/cli/workspace-overlay.js +21 -0
- package/descriptors/build.js +248 -0
- package/descriptors/hoisting.js +175 -0
- package/descriptors/index.js +38 -0
- package/descriptors/lockfile.js +117 -0
- package/descriptors/misc.js +144 -0
- package/descriptors/network.js +108 -0
- package/descriptors/resolution.js +250 -0
- package/descriptors/runtime-cfg.js +90 -0
- package/descriptors/schemas.js +26 -0
- package/descriptors/workspace.js +116 -0
- package/index.d.ts +363 -0
- package/index.js +3 -0
- package/package.json +60 -0
- package/plugin/freeze.js +79 -0
- package/plugin/index.js +48 -0
- package/plugin/serialize.js +26 -0
- package/registry.js +8 -0
- package/runtime/ctx.js +39 -0
- package/runtime/enforcement.js +36 -0
- package/runtime/strategies/arrays.js +37 -0
- package/runtime/strategies/catalogs.js +36 -0
- package/runtime/strategies/maps.js +46 -0
- package/runtime/strategies/overrides.js +57 -0
- package/runtime/strategies/scalar.js +57 -0
- package/runtime/strategies/table.js +27 -0
- package/runtime/warnings.js +61 -0
- package/runtime.d.ts +111 -0
- package/runtime.js +54 -0
- package/tsdoc-metadata.json +11 -0
- package/virtual.d.ts +18 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Data, Effect } from "effect";
|
|
2
|
+
import { SemVer } from "semver-effect";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/peer-range.ts
|
|
5
|
+
/**
|
|
6
|
+
* Typed failure raised when a peer range cannot be derived from a range string.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
var PeerRangeError = class extends Data.TaggedError("PeerRangeError") {};
|
|
11
|
+
/** Splits a simple range into its operator prefix and version (e.g. `^6.5.1`). */
|
|
12
|
+
const PREFIX_RE = /^(\^|~|)(\d.*)$/;
|
|
13
|
+
/**
|
|
14
|
+
* Recompute a materialized peer range from a package range and a strategy.
|
|
15
|
+
* "lock" pins to the exact version; "lock-minor" floors the patch to .0.
|
|
16
|
+
* The operator (^/~/exact) is preserved.
|
|
17
|
+
*
|
|
18
|
+
* Note: expects a release (non-prerelease) version string; a prerelease tag
|
|
19
|
+
* would be dropped by the major.minor.patch reconstruction.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
function derivePeerRange(range, strategy) {
|
|
24
|
+
return Effect.gen(function* () {
|
|
25
|
+
const match = PREFIX_RE.exec(range);
|
|
26
|
+
if (!match) return yield* Effect.fail(new PeerRangeError({ message: `Cannot derive peer range from "${range}"` }));
|
|
27
|
+
const [, prefix, version] = match;
|
|
28
|
+
const parsed = yield* SemVer.parse(version).pipe(Effect.mapError(() => new PeerRangeError({ message: `Invalid version in range "${range}"` })));
|
|
29
|
+
return strategy === "lock" ? `${prefix}${parsed.major}.${parsed.minor}.${parsed.patch}` : `${prefix}${parsed.major}.${parsed.minor}.0`;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { PeerRangeError, derivePeerRange };
|
package/cli/plan.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { derivePeerRange } from "./peer-range.js";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { Range, SemVer } from "semver-effect";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/plan.ts
|
|
6
|
+
/** Parse a version, returning null instead of failing (filters junk tags). */
|
|
7
|
+
const parseOrNull = (v) => SemVer.parse(v).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
8
|
+
/**
|
|
9
|
+
* Compute the candidate versions for one catalog entry against the list of
|
|
10
|
+
* published versions. Order: latest in-range (when newer than current), latest
|
|
11
|
+
* overall stable (when newer than the in-range pick), then keep. Prereleases
|
|
12
|
+
* are excluded. When the entry carries a strategy, each non-keep candidate gets
|
|
13
|
+
* a recomputed `peerRange`.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
function planEntry(entry, versions) {
|
|
18
|
+
return Effect.gen(function* () {
|
|
19
|
+
const range = yield* Range.parse(entry.currentRange).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
20
|
+
const parsed = [];
|
|
21
|
+
for (const v of versions) {
|
|
22
|
+
const sv = yield* parseOrNull(v);
|
|
23
|
+
if (sv?.isStable) parsed.push(sv);
|
|
24
|
+
}
|
|
25
|
+
parsed.sort((a, b) => a.compare(b));
|
|
26
|
+
const maxOf = (list) => list.length ? list[list.length - 1] : null;
|
|
27
|
+
const current = yield* parseOrNull(entry.currentRange.replace(/^[\^~]/, ""));
|
|
28
|
+
const currentMajor = current?.major ?? 0;
|
|
29
|
+
const inRangeMax = range ? maxOf(parsed.filter((v) => range.test(v))) : null;
|
|
30
|
+
const overallMax = maxOf(parsed);
|
|
31
|
+
const withPeer = (version) => entry.strategy && entry.strategy !== "interop" ? derivePeerRange(`${entry.operator}${version}`, entry.strategy) : Effect.succeed(void 0);
|
|
32
|
+
const candidates = [];
|
|
33
|
+
if (inRangeMax && (current === null || inRangeMax.gt(current))) {
|
|
34
|
+
const version = inRangeMax.toString();
|
|
35
|
+
const peerRange = yield* withPeer(version);
|
|
36
|
+
candidates.push({
|
|
37
|
+
kind: "in-range",
|
|
38
|
+
range: `${entry.operator}${version}`,
|
|
39
|
+
version,
|
|
40
|
+
isMajor: inRangeMax.major > currentMajor,
|
|
41
|
+
...peerRange ? { peerRange } : {}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (overallMax && (current === null || overallMax.gt(current)) && (!inRangeMax || overallMax.gt(inRangeMax))) {
|
|
45
|
+
const version = overallMax.toString();
|
|
46
|
+
const peerRange = yield* withPeer(version);
|
|
47
|
+
candidates.push({
|
|
48
|
+
kind: "latest",
|
|
49
|
+
range: `${entry.operator}${version}`,
|
|
50
|
+
version,
|
|
51
|
+
isMajor: overallMax.major > currentMajor,
|
|
52
|
+
...peerRange ? { peerRange } : {}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
candidates.push({
|
|
56
|
+
kind: "keep",
|
|
57
|
+
range: entry.currentRange,
|
|
58
|
+
version: entry.currentRange.replace(/^[\^~]/, ""),
|
|
59
|
+
isMajor: false
|
|
60
|
+
});
|
|
61
|
+
return candidates;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
export { planEntry };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { buildDiff } from "./diff/build.js";
|
|
2
|
+
import { renderExportDiff } from "./diff/render.js";
|
|
3
|
+
import { effectiveManaged, vanillaManaged } from "./effective.js";
|
|
4
|
+
import { canonicalize } from "./workspace-file.js";
|
|
5
|
+
import { overlayWorkspace } from "./workspace-overlay.js";
|
|
6
|
+
import { WORKSPACE_FIELDS } from "./commands/export.js";
|
|
7
|
+
|
|
8
|
+
//#region src/cli/preview-views.ts
|
|
9
|
+
/**
|
|
10
|
+
* Build the three preview views as styled lines: Changes (parsed→merged, with
|
|
11
|
+
* local + preserve + excludeByRepo), Full (same tree, full verbosity), and
|
|
12
|
+
* Simulated (parsed→vanilla fresh-consumer output, no local/overlay).
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
function buildPreviewViews(input) {
|
|
17
|
+
const meta = {
|
|
18
|
+
localKeys: new Set(input.local ? Object.keys(input.local) : []),
|
|
19
|
+
managedKeys: WORKSPACE_FIELDS
|
|
20
|
+
};
|
|
21
|
+
const merged = overlayWorkspace(effectiveManaged(input.managed, input.local, input.parsed, input.manifest, input.rootName), input.parsed);
|
|
22
|
+
const vanilla = vanillaManaged(input.managed, input.manifest, input.rootName);
|
|
23
|
+
const before = canonicalize(input.parsed);
|
|
24
|
+
const changesTree = buildDiff(before, canonicalize(merged), meta);
|
|
25
|
+
const simulatedTree = buildDiff(before, canonicalize(vanilla), meta);
|
|
26
|
+
return {
|
|
27
|
+
changes: renderExportDiff(changesTree, { full: false }),
|
|
28
|
+
full: renderExportDiff(changesTree, { full: true }),
|
|
29
|
+
simulated: renderExportDiff(simulatedTree, { full: false })
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { buildPreviewViews };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//#region src/cli/release-age.ts
|
|
2
|
+
/** Combine two sources: strictest age (max), widest exempt set (union). @internal */
|
|
3
|
+
function combineReleaseAge(a, b) {
|
|
4
|
+
const ages = [a?.ageMinutes, b?.ageMinutes].filter((n) => typeof n === "number");
|
|
5
|
+
return {
|
|
6
|
+
ageMinutes: ages.length ? Math.max(0, ...ages) : 0,
|
|
7
|
+
exclude: [.../* @__PURE__ */ new Set([...a?.exclude ?? [], ...b?.exclude ?? []])]
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** Match a package name against exact names or `*`-globs (e.g. `@effect/*`). @internal */
|
|
11
|
+
function matchesExclude(pkg, patterns) {
|
|
12
|
+
for (const pat of patterns) {
|
|
13
|
+
if (pat === pkg) return true;
|
|
14
|
+
if (pat.includes("*")) {
|
|
15
|
+
if (new RegExp(`^${pat.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`).test(pkg)) return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Drop versions younger than the gate's cutoff (and versions with no publish
|
|
22
|
+
* timestamp). No-op when the age is 0 or the package is exempt.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
function filterByReleaseAge(versions, times, gate, pkg, now) {
|
|
27
|
+
if (gate.ageMinutes <= 0 || matchesExclude(pkg, gate.exclude)) return [...versions];
|
|
28
|
+
const cutoff = now - gate.ageMinutes * 6e4;
|
|
29
|
+
return versions.filter((v) => {
|
|
30
|
+
const t = times[v];
|
|
31
|
+
if (t === void 0) return false;
|
|
32
|
+
const published = Date.parse(t);
|
|
33
|
+
return Number.isFinite(published) && published <= cutoff;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** Unwrap a managed field that may be a bare value or a `{ value, enforcement }` FieldInput. */
|
|
37
|
+
function fieldValue(raw) {
|
|
38
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw) && "value" in raw) return raw.value;
|
|
39
|
+
return raw;
|
|
40
|
+
}
|
|
41
|
+
/** Read the release-age gate declared in a statically-evaluated PnpmConfigPlugin config. @internal */
|
|
42
|
+
function readConfigReleaseAge(config) {
|
|
43
|
+
if (!config) return null;
|
|
44
|
+
const age = fieldValue(config.minimumReleaseAge);
|
|
45
|
+
const exc = fieldValue(config.minimumReleaseAgeExclude);
|
|
46
|
+
const out = {};
|
|
47
|
+
if (typeof age === "number" && Number.isFinite(age)) out.ageMinutes = age;
|
|
48
|
+
if (Array.isArray(exc)) out.exclude = exc.filter((x) => typeof x === "string");
|
|
49
|
+
return out.ageMinutes === void 0 && out.exclude === void 0 ? null : out;
|
|
50
|
+
}
|
|
51
|
+
/** Parse `pnpm config get minimumReleaseAge[Exclude]` stdout into a PartialGate. @internal */
|
|
52
|
+
function parsePnpmGate(age, exclude) {
|
|
53
|
+
const out = {};
|
|
54
|
+
const trimmedAge = age?.trim();
|
|
55
|
+
if (trimmedAge && trimmedAge !== "undefined") {
|
|
56
|
+
const n = Number.parseInt(trimmedAge, 10);
|
|
57
|
+
if (Number.isFinite(n)) out.ageMinutes = n;
|
|
58
|
+
}
|
|
59
|
+
const trimmedExc = exclude?.trim();
|
|
60
|
+
if (trimmedExc && trimmedExc !== "undefined") {
|
|
61
|
+
let list = [];
|
|
62
|
+
try {
|
|
63
|
+
const json = JSON.parse(trimmedExc);
|
|
64
|
+
list = Array.isArray(json) ? json.map(String) : [String(json)];
|
|
65
|
+
} catch {
|
|
66
|
+
list = trimmedExc.split(/[\s,]+/).filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
if (list.length) out.exclude = list;
|
|
69
|
+
}
|
|
70
|
+
return out.ageMinutes === void 0 && out.exclude === void 0 ? null : out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
export { combineReleaseAge, filterByReleaseAge, matchesExclude, parsePnpmGate, readConfigReleaseAge };
|
package/cli/resolve.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Context, Data, Effect, Layer } from "effect";
|
|
2
|
+
import { Command, CommandExecutor } from "@effect/platform";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/resolve.ts
|
|
5
|
+
/**
|
|
6
|
+
* Typed failure raised when a package's versions cannot be resolved.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
var ResolveError = class extends Data.TaggedError("ResolveError") {};
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the published versions of a package from the registry. The Live
|
|
13
|
+
* implementation shells out to `pnpm view`, reusing the user's .npmrc, scoped
|
|
14
|
+
* registries, and auth tokens.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
var RegistryResolver = class extends Context.Tag("RegistryResolver")() {};
|
|
19
|
+
/** Parse `pnpm view ... versions --json` stdout: a JSON array, or a single JSON string. */
|
|
20
|
+
function parseVersions(pkg, stdout) {
|
|
21
|
+
return Effect.try({
|
|
22
|
+
try: () => {
|
|
23
|
+
const json = JSON.parse(stdout);
|
|
24
|
+
if (Array.isArray(json)) return json.map(String);
|
|
25
|
+
if (typeof json === "string") return [json];
|
|
26
|
+
throw new Error("unexpected shape");
|
|
27
|
+
},
|
|
28
|
+
catch: () => new ResolveError({
|
|
29
|
+
pkg,
|
|
30
|
+
message: `Could not parse versions for ${pkg}`
|
|
31
|
+
})
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Parse `pnpm view <pkg> time --json` stdout: an object of version → ISO date. @internal */
|
|
35
|
+
function parseTimes(pkg, stdout) {
|
|
36
|
+
return Effect.try({
|
|
37
|
+
try: () => {
|
|
38
|
+
const json = JSON.parse(stdout);
|
|
39
|
+
if (json && typeof json === "object" && !Array.isArray(json)) {
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [k, v] of Object.entries(json)) out[k] = String(v);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
throw new Error("unexpected shape");
|
|
45
|
+
},
|
|
46
|
+
catch: () => new ResolveError({
|
|
47
|
+
pkg,
|
|
48
|
+
message: `Could not parse times for ${pkg}`
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Parse `pnpm view <pkg>@<version> peerDependencies --json`; empty stdout → {}. @internal */
|
|
53
|
+
function parsePeerDeps(pkg, stdout) {
|
|
54
|
+
const trimmed = stdout.trim();
|
|
55
|
+
if (trimmed === "") return Effect.succeed({});
|
|
56
|
+
return Effect.try({
|
|
57
|
+
try: () => {
|
|
58
|
+
const json = JSON.parse(trimmed);
|
|
59
|
+
if (json && typeof json === "object" && !Array.isArray(json)) {
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const [k, v] of Object.entries(json)) out[k] = String(v);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
throw new Error("unexpected shape");
|
|
65
|
+
},
|
|
66
|
+
catch: () => new ResolveError({
|
|
67
|
+
pkg,
|
|
68
|
+
message: `Could not parse peerDependencies for ${pkg}`
|
|
69
|
+
})
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Live RegistryResolver backed by `pnpm view <pkg> versions --json`.
|
|
74
|
+
*
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
const RegistryResolverLive = Layer.effect(RegistryResolver, Effect.gen(function* () {
|
|
78
|
+
const executor = yield* CommandExecutor.CommandExecutor;
|
|
79
|
+
return {
|
|
80
|
+
versions: (pkg) => Effect.gen(function* () {
|
|
81
|
+
const cmd = Command.make("pnpm", "view", pkg, "versions", "--json");
|
|
82
|
+
return yield* parseVersions(pkg, yield* executor.string(cmd).pipe(Effect.mapError((e) => new ResolveError({
|
|
83
|
+
pkg,
|
|
84
|
+
message: String(e)
|
|
85
|
+
}))));
|
|
86
|
+
}),
|
|
87
|
+
times: (pkg) => Effect.gen(function* () {
|
|
88
|
+
const cmd = Command.make("pnpm", "view", pkg, "time", "--json");
|
|
89
|
+
return yield* parseTimes(pkg, yield* executor.string(cmd).pipe(Effect.mapError((e) => new ResolveError({
|
|
90
|
+
pkg,
|
|
91
|
+
message: String(e)
|
|
92
|
+
}))));
|
|
93
|
+
}),
|
|
94
|
+
peerDependencies: (pkg, version) => Effect.gen(function* () {
|
|
95
|
+
const cmd = Command.make("pnpm", "view", `${pkg}@${version}`, "peerDependencies", "--json");
|
|
96
|
+
return yield* parsePeerDeps(pkg, yield* executor.string(cmd).pipe(Effect.mapError((e) => new ResolveError({
|
|
97
|
+
pkg,
|
|
98
|
+
message: String(e)
|
|
99
|
+
}))));
|
|
100
|
+
}),
|
|
101
|
+
pnpmConfig: (key) => Effect.gen(function* () {
|
|
102
|
+
const cmd = Command.make("pnpm", "config", "get", key);
|
|
103
|
+
return yield* executor.string(cmd).pipe(Effect.map((s) => s.trim()), Effect.catchAll(() => Effect.succeed(null)));
|
|
104
|
+
})
|
|
105
|
+
};
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
export { RegistryResolver, RegistryResolverLive, ResolveError, parsePeerDeps, parseTimes, parseVersions };
|
package/cli/rewrite.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/cli/rewrite.ts
|
|
2
|
+
/**
|
|
3
|
+
* Apply span replacements to a source string. Edits are applied in descending
|
|
4
|
+
* start order so each edit's offsets remain valid as later text shifts. Throws
|
|
5
|
+
* a RangeError if any two edits overlap.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
function applyEdits(source, edits) {
|
|
10
|
+
const sorted = [...edits].sort((a, b) => b.span[0] - a.span[0]);
|
|
11
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
12
|
+
const prev = sorted[i - 1];
|
|
13
|
+
const cur = sorted[i];
|
|
14
|
+
if (cur.span[1] > prev.span[0]) throw new RangeError(`Overlapping edits at [${cur.span[0]}, ${cur.span[1]}) and [${prev.span[0]}, ${prev.span[1]})`);
|
|
15
|
+
}
|
|
16
|
+
let out = source;
|
|
17
|
+
for (const edit of sorted) out = out.slice(0, edit.span[0]) + edit.text + out.slice(edit.span[1]);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { applyEdits };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { discoverCatalogEntries } from "./discover.js";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/cli/select-file.ts
|
|
7
|
+
/**
|
|
8
|
+
* Scan a directory for top-level `.ts` files whose source contains a usable
|
|
9
|
+
* `PnpmConfigPlugin(...)` catalog (discovery yields at least one entry). Read
|
|
10
|
+
* or parse failures are skipped silently. Returns absolute-or-joined paths.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
function findConfigFiles(dir) {
|
|
15
|
+
return Effect.sync(() => {
|
|
16
|
+
let names;
|
|
17
|
+
try {
|
|
18
|
+
names = readdirSync(dir);
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const matches = [];
|
|
23
|
+
for (const name of names) {
|
|
24
|
+
if (!name.endsWith(".ts") || name.endsWith(".d.ts")) continue;
|
|
25
|
+
const path = join(dir, name);
|
|
26
|
+
try {
|
|
27
|
+
const { entries } = discoverCatalogEntries(readFileSync(path, "utf8"), path);
|
|
28
|
+
if (entries.length > 0) matches.push(path);
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
31
|
+
return matches;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Choose the config file to operate on from a set of candidate paths. Exactly
|
|
36
|
+
* one is required; zero or many is an error the caller surfaces.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
function pickConfigCandidate(matches) {
|
|
41
|
+
if (matches.length === 1) return {
|
|
42
|
+
ok: true,
|
|
43
|
+
file: matches[0]
|
|
44
|
+
};
|
|
45
|
+
if (matches.length === 0) return {
|
|
46
|
+
ok: false,
|
|
47
|
+
message: "No config file found. Pass a file path explicitly."
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
message: `Multiple config files found; pass one explicitly: ${matches.join(", ")}`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Restrict entries to a single catalog by name, or return all when undefined.
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
function filterEntriesByCatalog(entries, catalog) {
|
|
60
|
+
return catalog === void 0 ? [...entries] : entries.filter((e) => e.catalog === catalog);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { filterEntriesByCatalog, findConfigFiles, pickConfigCandidate };
|
package/cli/summary.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { toAnsi } from "./ui/ansi.js";
|
|
2
|
+
|
|
3
|
+
//#region src/cli/summary.ts
|
|
4
|
+
/**
|
|
5
|
+
* Build the pending-decisions summary as styled lines: one line per real
|
|
6
|
+
* change, peer changes indented, a dim tally, then interop adjustments and
|
|
7
|
+
* conflicts. Pure; color is applied by `renderSummary`/`toAnsi`.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
function summaryLines(decisions, interop) {
|
|
12
|
+
const lines = [];
|
|
13
|
+
let toUpdate = 0;
|
|
14
|
+
let major = 0;
|
|
15
|
+
let resync = 0;
|
|
16
|
+
let materialize = 0;
|
|
17
|
+
let upToDate = 0;
|
|
18
|
+
for (const { item, chosen } of decisions) {
|
|
19
|
+
const { entry } = item;
|
|
20
|
+
if (chosen.kind !== "keep") {
|
|
21
|
+
toUpdate++;
|
|
22
|
+
if (chosen.isMajor) major++;
|
|
23
|
+
lines.push({
|
|
24
|
+
indent: 0,
|
|
25
|
+
gutter: "~",
|
|
26
|
+
segments: [{
|
|
27
|
+
text: `${entry.catalog} › ${entry.pkg} ${entry.currentRange} → ${chosen.range}`,
|
|
28
|
+
style: "changed"
|
|
29
|
+
}]
|
|
30
|
+
});
|
|
31
|
+
if (entry.peer && chosen.peerRange && chosen.peerRange !== entry.peer.value) lines.push({
|
|
32
|
+
indent: 1,
|
|
33
|
+
gutter: "~",
|
|
34
|
+
segments: [{
|
|
35
|
+
text: `↳ peer ${entry.peer.value} → ${chosen.peerRange}`,
|
|
36
|
+
style: "changed"
|
|
37
|
+
}]
|
|
38
|
+
});
|
|
39
|
+
else if (!entry.peer && entry.strategy && chosen.peerRange) {
|
|
40
|
+
lines.push({
|
|
41
|
+
indent: 1,
|
|
42
|
+
gutter: "+",
|
|
43
|
+
segments: [{
|
|
44
|
+
text: `↳ peer (new) → ${chosen.peerRange}`,
|
|
45
|
+
style: "added"
|
|
46
|
+
}]
|
|
47
|
+
});
|
|
48
|
+
materialize++;
|
|
49
|
+
}
|
|
50
|
+
} else if (entry.peer && item.driftPeer) {
|
|
51
|
+
resync++;
|
|
52
|
+
lines.push({
|
|
53
|
+
indent: 0,
|
|
54
|
+
gutter: "~",
|
|
55
|
+
segments: [{
|
|
56
|
+
text: `${entry.catalog} › ${entry.pkg} (resync peer)`,
|
|
57
|
+
style: "changed"
|
|
58
|
+
}]
|
|
59
|
+
});
|
|
60
|
+
lines.push({
|
|
61
|
+
indent: 1,
|
|
62
|
+
gutter: "~",
|
|
63
|
+
segments: [{
|
|
64
|
+
text: `↳ peer ${entry.peer.value} → ${item.driftPeer}`,
|
|
65
|
+
style: "changed"
|
|
66
|
+
}]
|
|
67
|
+
});
|
|
68
|
+
} else if (!entry.peer && item.materializePeer) {
|
|
69
|
+
materialize++;
|
|
70
|
+
lines.push({
|
|
71
|
+
indent: 0,
|
|
72
|
+
gutter: "+",
|
|
73
|
+
segments: [{
|
|
74
|
+
text: `${entry.catalog} › ${entry.pkg} (materialize peer)`,
|
|
75
|
+
style: "added"
|
|
76
|
+
}]
|
|
77
|
+
});
|
|
78
|
+
lines.push({
|
|
79
|
+
indent: 1,
|
|
80
|
+
gutter: "+",
|
|
81
|
+
segments: [{
|
|
82
|
+
text: `↳ peer (new) → ${item.materializePeer}`,
|
|
83
|
+
style: "added"
|
|
84
|
+
}]
|
|
85
|
+
});
|
|
86
|
+
} else upToDate++;
|
|
87
|
+
}
|
|
88
|
+
lines.push({
|
|
89
|
+
indent: 0,
|
|
90
|
+
gutter: " ",
|
|
91
|
+
segments: [{
|
|
92
|
+
text: `${toUpdate} to update · ${major} major · ${resync} resync · ${materialize} new peer · ${upToDate} up to date`,
|
|
93
|
+
style: "unchanged"
|
|
94
|
+
}]
|
|
95
|
+
});
|
|
96
|
+
if (interop) {
|
|
97
|
+
for (const a of interop.adjustments) {
|
|
98
|
+
lines.push({
|
|
99
|
+
indent: 0,
|
|
100
|
+
gutter: "~",
|
|
101
|
+
segments: [{
|
|
102
|
+
text: `↓ ${a.pkg} ${a.from} → ${a.to}`,
|
|
103
|
+
style: "changed"
|
|
104
|
+
}]
|
|
105
|
+
});
|
|
106
|
+
lines.push({
|
|
107
|
+
indent: 1,
|
|
108
|
+
gutter: "~",
|
|
109
|
+
segments: [{
|
|
110
|
+
text: `↳ peer → ${a.peer}`,
|
|
111
|
+
style: "changed"
|
|
112
|
+
}]
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
for (const c of interop.conflicts) lines.push({
|
|
116
|
+
indent: 0,
|
|
117
|
+
gutter: "⚠",
|
|
118
|
+
segments: [{
|
|
119
|
+
text: `${c.pkg} (kept ${c.ceiling}) blocked by ${c.blockedBy}`,
|
|
120
|
+
style: "warn"
|
|
121
|
+
}]
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return lines;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Render the summary to a string. Color defaults off so non-TTY/test callers
|
|
128
|
+
* get clean text; the upgrade command passes the detected color flag.
|
|
129
|
+
*
|
|
130
|
+
* @internal
|
|
131
|
+
*/
|
|
132
|
+
function renderSummary(decisions, interop, opts) {
|
|
133
|
+
return toAnsi(summaryLines(decisions, interop), { color: opts?.color ?? false });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
//#endregion
|
|
137
|
+
export { renderSummary, summaryLines };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
2
|
+
import { createElement, useState } from "react";
|
|
3
|
+
import { Tab, Tabs } from "ink-tab";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/ui/Preview.ts
|
|
6
|
+
const INK_COLOR = {
|
|
7
|
+
added: "green",
|
|
8
|
+
removed: "red",
|
|
9
|
+
changed: "yellow",
|
|
10
|
+
warn: "red",
|
|
11
|
+
local: "magenta",
|
|
12
|
+
unmanaged: "gray",
|
|
13
|
+
unchanged: "gray",
|
|
14
|
+
plain: void 0
|
|
15
|
+
};
|
|
16
|
+
const TabC = Tab;
|
|
17
|
+
const TabsC = Tabs;
|
|
18
|
+
function renderLines(lines) {
|
|
19
|
+
return createElement(Box, { flexDirection: "column" }, ...lines.map((l, i) => {
|
|
20
|
+
const indent = " ".repeat(l.indent);
|
|
21
|
+
const tag = l.tag ? ` (${l.tag})` : "";
|
|
22
|
+
const body = l.segments.map((s, j) => {
|
|
23
|
+
const color = INK_COLOR[s.style];
|
|
24
|
+
return createElement(Text, {
|
|
25
|
+
key: j,
|
|
26
|
+
...color ? { color } : {}
|
|
27
|
+
}, s.text);
|
|
28
|
+
});
|
|
29
|
+
return createElement(Text, { key: i }, `${l.gutter} ${indent}`, ...body, tag);
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Interactive export preview: an ink-tab bar over the Changes / Full /
|
|
34
|
+
* Simulated views. `q`/Esc exits. Written with React.createElement (no JSX).
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
function Preview({ views, onExit }) {
|
|
39
|
+
const app = useApp();
|
|
40
|
+
const [active, setActive] = useState("changes");
|
|
41
|
+
useInput((input, key) => {
|
|
42
|
+
if (input === "q" || key.escape) {
|
|
43
|
+
onExit();
|
|
44
|
+
app.exit();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return createElement(Box, { flexDirection: "column" }, createElement(TabsC, { onChange: (name) => setActive(name) }, createElement(TabC, {
|
|
48
|
+
name: "changes",
|
|
49
|
+
key: "changes"
|
|
50
|
+
}, "Changes"), createElement(TabC, {
|
|
51
|
+
name: "full",
|
|
52
|
+
key: "full"
|
|
53
|
+
}, "Full"), createElement(TabC, {
|
|
54
|
+
name: "simulated",
|
|
55
|
+
key: "simulated"
|
|
56
|
+
}, "Simulated")), renderLines(views[active]));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
export { Preview };
|
package/cli/ui/Walk.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { initWalk, walkStep } from "../walk-reducer.js";
|
|
2
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
3
|
+
import { createElement, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/ui/Walk.ts
|
|
6
|
+
/**
|
|
7
|
+
* Interactive per-package upgrade selector rendered with Ink.
|
|
8
|
+
*
|
|
9
|
+
* Written with React.createElement (no JSX) so the file can be plain .ts
|
|
10
|
+
* without requiring TSX transform configuration.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
function Walk({ items, onDone }) {
|
|
15
|
+
const app = useApp();
|
|
16
|
+
const [state, setState] = useState(() => initWalk(items));
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (state.done) {
|
|
19
|
+
onDone(state.decisions);
|
|
20
|
+
app.exit();
|
|
21
|
+
}
|
|
22
|
+
}, []);
|
|
23
|
+
useInput((_input, key) => {
|
|
24
|
+
if (state.done) return;
|
|
25
|
+
const which = key.upArrow ? "up" : key.downArrow ? "down" : key.return ? "enter" : null;
|
|
26
|
+
if (!which) return;
|
|
27
|
+
const next = walkStep(state, items, which);
|
|
28
|
+
setState(next);
|
|
29
|
+
if (next.done) {
|
|
30
|
+
onDone(next.decisions);
|
|
31
|
+
app.exit();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (state.done || state.index >= items.length) return createElement(Text, null, "Done.");
|
|
35
|
+
const item = items[state.index];
|
|
36
|
+
const e = item.entry;
|
|
37
|
+
return createElement(Box, { flexDirection: "column" }, createElement(Text, null, [
|
|
38
|
+
`${e.catalog} › ${e.pkg} current ${e.currentRange}`,
|
|
39
|
+
e.peer ? ` peer ${e.peer.value}` : "",
|
|
40
|
+
e.strategy ? ` strategy: ${e.strategy}` : ""
|
|
41
|
+
].join("")), ...item.candidates.map((c, i) => {
|
|
42
|
+
const selected = i === state.cursor;
|
|
43
|
+
const cursor = selected ? "❯ " : " ";
|
|
44
|
+
const base = c.kind === "keep" ? `keep ${c.range}` : `${c.range} ${c.kind}`;
|
|
45
|
+
const colorProps = selected ? { color: "cyan" } : c.isMajor ? { color: "yellow" } : {};
|
|
46
|
+
const text = c.kind === "keep" ? `${cursor}${base}` : `${cursor}${base}${c.isMajor ? " ⚠ major" : ""}`;
|
|
47
|
+
return createElement(Text, {
|
|
48
|
+
key: c.kind,
|
|
49
|
+
...colorProps
|
|
50
|
+
}, text);
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { Walk };
|
package/cli/ui/ansi.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { paint, tagSuffix } from "./styled.js";
|
|
2
|
+
|
|
3
|
+
//#region src/cli/ui/ansi.ts
|
|
4
|
+
/**
|
|
5
|
+
* Render styled lines to a string. Each line is
|
|
6
|
+
* `<gutter><space><2-space-indent><painted segments><tag>`.
|
|
7
|
+
* Pure: color is decided by the caller, never read from the environment.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
function toAnsi(lines, opts) {
|
|
12
|
+
return lines.map((l) => {
|
|
13
|
+
const indent = " ".repeat(l.indent);
|
|
14
|
+
const body = l.segments.map((s) => paint(s.text, s.style, opts.color)).join("");
|
|
15
|
+
return `${l.gutter} ${indent}${body}${tagSuffix(l.tag)}`;
|
|
16
|
+
}).join("\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { toAnsi };
|