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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/bin/rolldown-pnpm-config.js +21 -0
  4. package/catalogs.js +27 -0
  5. package/cli/commands/export.js +118 -0
  6. package/cli/commands/preview.js +73 -0
  7. package/cli/commands/upgrade.js +437 -0
  8. package/cli/diff/build.js +122 -0
  9. package/cli/diff/render.js +103 -0
  10. package/cli/discover.js +128 -0
  11. package/cli/drift.js +22 -0
  12. package/cli/edits.js +41 -0
  13. package/cli/effective.js +42 -0
  14. package/cli/evaluate.js +91 -0
  15. package/cli/interop.js +285 -0
  16. package/cli/local-merge.js +75 -0
  17. package/cli/peer-range.js +34 -0
  18. package/cli/plan.js +66 -0
  19. package/cli/preview-views.js +34 -0
  20. package/cli/release-age.js +74 -0
  21. package/cli/resolve.js +109 -0
  22. package/cli/rewrite.js +22 -0
  23. package/cli/select-file.js +64 -0
  24. package/cli/summary.js +137 -0
  25. package/cli/ui/Preview.js +60 -0
  26. package/cli/ui/Walk.js +55 -0
  27. package/cli/ui/ansi.js +20 -0
  28. package/cli/ui/env.js +20 -0
  29. package/cli/ui/run-preview.js +23 -0
  30. package/cli/ui/run-walk.js +29 -0
  31. package/cli/ui/styled.js +27 -0
  32. package/cli/walk-plan.js +35 -0
  33. package/cli/walk-reducer.js +61 -0
  34. package/cli/workspace-file.js +58 -0
  35. package/cli/workspace-overlay.js +21 -0
  36. package/descriptors/build.js +248 -0
  37. package/descriptors/hoisting.js +175 -0
  38. package/descriptors/index.js +38 -0
  39. package/descriptors/lockfile.js +117 -0
  40. package/descriptors/misc.js +144 -0
  41. package/descriptors/network.js +108 -0
  42. package/descriptors/resolution.js +250 -0
  43. package/descriptors/runtime-cfg.js +90 -0
  44. package/descriptors/schemas.js +26 -0
  45. package/descriptors/workspace.js +116 -0
  46. package/index.d.ts +363 -0
  47. package/index.js +3 -0
  48. package/package.json +60 -0
  49. package/plugin/freeze.js +79 -0
  50. package/plugin/index.js +48 -0
  51. package/plugin/serialize.js +26 -0
  52. package/registry.js +8 -0
  53. package/runtime/ctx.js +39 -0
  54. package/runtime/enforcement.js +36 -0
  55. package/runtime/strategies/arrays.js +37 -0
  56. package/runtime/strategies/catalogs.js +36 -0
  57. package/runtime/strategies/maps.js +46 -0
  58. package/runtime/strategies/overrides.js +57 -0
  59. package/runtime/strategies/scalar.js +57 -0
  60. package/runtime/strategies/table.js +27 -0
  61. package/runtime/warnings.js +61 -0
  62. package/runtime.d.ts +111 -0
  63. package/runtime.js +54 -0
  64. package/tsdoc-metadata.json +11 -0
  65. package/virtual.d.ts +18 -0
@@ -0,0 +1,79 @@
1
+ import { normalizeCatalogs } from "../catalogs.js";
2
+ import { DESCRIPTORS, deriveSchemas } from "../descriptors/index.js";
3
+ import { FIELD_REGISTRY } from "../registry.js";
4
+ import { Data, Effect, Schema } from "effect";
5
+
6
+ //#region src/plugin/freeze.ts
7
+ /**
8
+ * Typed failure for invalid plugin configuration, surfaced as a build error.
9
+ *
10
+ * @internal
11
+ */
12
+ var ConfigError = class extends Data.TaggedError("ConfigError") {};
13
+ /** Per-field value-shape schemas; only declared fields are validated. */
14
+ const FIELD_SCHEMAS = deriveSchemas(DESCRIPTORS);
15
+ const CatalogsSchema = FIELD_SCHEMAS.catalogs;
16
+ /** Keys recognized by the wrapped `{ value, enforcement?, excludeByRepo? }` form. */
17
+ const WRAPPED_KEYS = /* @__PURE__ */ new Set([
18
+ "value",
19
+ "enforcement",
20
+ "excludeByRepo"
21
+ ]);
22
+ /** True only when `input` is the wrapped form: a `value` key and no foreign keys.
23
+ * This disambiguates from a record-valued field that happens to contain a
24
+ * `value` key (e.g. `overrides: { value: ">=1", lodash: ">=4" }`), which has
25
+ * keys outside the recognized set and is therefore treated as a bare value. */
26
+ function isWrappedField(input) {
27
+ const keys = Object.keys(input);
28
+ return keys.includes("value") && keys.every((k) => WRAPPED_KEYS.has(k));
29
+ }
30
+ function normalizeField(input) {
31
+ if (input !== null && typeof input === "object" && isWrappedField(input)) {
32
+ const o = input;
33
+ return {
34
+ value: o.value,
35
+ ...o.enforcement !== void 0 ? { enforcement: o.enforcement } : {},
36
+ ...o.excludeByRepo !== void 0 ? { options: { excludeByRepo: o.excludeByRepo } } : {}
37
+ };
38
+ }
39
+ return { value: input };
40
+ }
41
+ /**
42
+ * Validate + freeze the plugin config into base data + a strategy manifest. The
43
+ * only place Effect runs; invoked once at build time inside the plugin.
44
+ *
45
+ * @internal
46
+ */
47
+ function freeze(config) {
48
+ return Effect.gen(function* () {
49
+ const base = {};
50
+ const manifest = {};
51
+ base.catalogs = yield* Schema.decodeUnknown(CatalogsSchema)(normalizeCatalogs(config.catalogs)).pipe(Effect.mapError((error) => new ConfigError({ message: `Invalid catalogs: ${String(error)}` })));
52
+ manifest.catalogs = {
53
+ strategy: "catalogs",
54
+ enforcement: "warn"
55
+ };
56
+ 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" }));
57
+ for (const [field, reg] of Object.entries(FIELD_REGISTRY)) {
58
+ if (field === "catalogs") continue;
59
+ const raw = config[field];
60
+ if (raw === void 0) continue;
61
+ const decl = normalizeField(raw);
62
+ const schema = FIELD_SCHEMAS[field];
63
+ base[field] = schema ? yield* Schema.decodeUnknown(schema)(decl.value).pipe(Effect.mapError((error) => new ConfigError({ message: `Invalid ${field}: ${String(error)}` }))) : decl.value;
64
+ manifest[field] = {
65
+ strategy: reg.strategy,
66
+ enforcement: decl.enforcement ?? reg.enforcement,
67
+ ...decl.options ? { options: decl.options } : {}
68
+ };
69
+ }
70
+ return {
71
+ base,
72
+ manifest,
73
+ name: config.name
74
+ };
75
+ });
76
+ }
77
+
78
+ //#endregion
79
+ export { ConfigError, freeze };
@@ -0,0 +1,48 @@
1
+ import { freeze } from "./freeze.js";
2
+ import { emitCatalogsModule, emitPnpmfileModule } from "./serialize.js";
3
+ import { Effect } from "effect";
4
+
5
+ //#region src/plugin/index.ts
6
+ const PNPMFILE_SPEC = "rolldown-pnpm-config/virtual/pnpmfile";
7
+ const CATALOGS_SPEC = "rolldown-pnpm-config/virtual/catalogs";
8
+ /**
9
+ * Internal factory for the rolldown plugin. Accepts an optional DI seam for
10
+ * testing (freeze spy). The Effect freeze runs once (memoized) and is reused
11
+ * across every tsdown pass (JS, dts, declarations, looseFiles).
12
+ *
13
+ * @internal
14
+ */
15
+ function createPnpmConfigPlugin(config, deps = { freeze }) {
16
+ let frozen;
17
+ const getFrozen = () => frozen ??= Effect.runPromise(deps.freeze(config));
18
+ return {
19
+ name: "rolldown-pnpm-config",
20
+ resolveId(source) {
21
+ if (source === PNPMFILE_SPEC || source === CATALOGS_SPEC) return `\0${source}`;
22
+ return null;
23
+ },
24
+ async load(id) {
25
+ if (id === `\0${PNPMFILE_SPEC}`) {
26
+ const { base, manifest, name } = await getFrozen();
27
+ return emitPnpmfileModule(base, manifest, name);
28
+ }
29
+ if (id === `\0${CATALOGS_SPEC}`) {
30
+ const { base } = await getFrozen();
31
+ return emitCatalogsModule(base.catalogs ?? {});
32
+ }
33
+ return null;
34
+ }
35
+ };
36
+ }
37
+ /**
38
+ * Rolldown plugin that serves the two virtual pnpm-config modules. Pass a
39
+ * `PluginConfig` object and the plugin handles the rest.
40
+ *
41
+ * @public
42
+ */
43
+ function PnpmConfigPlugin(config) {
44
+ return createPnpmConfigPlugin(config);
45
+ }
46
+
47
+ //#endregion
48
+ export { PnpmConfigPlugin, createPnpmConfigPlugin };
@@ -0,0 +1,26 @@
1
+ //#region src/plugin/serialize.ts
2
+ /** Recursively sort object keys for deterministic output; arrays keep order. */
3
+ function sortKeys(value) {
4
+ if (Array.isArray(value)) return value.map(sortKeys);
5
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, sortKeys(v)]));
6
+ return value;
7
+ }
8
+ /** Source for the `catalogs` virtual module: a sorted Map literal (plain-JS branch). */
9
+ function emitCatalogsModule(catalogs) {
10
+ const sorted = sortKeys(catalogs);
11
+ return `export const catalogs = new Map([${Object.entries(sorted).map(([name, entries]) => {
12
+ const inner = Object.entries(entries).map(([pkg, range]) => `[${JSON.stringify(pkg)}, ${JSON.stringify(range)}]`).join(", ");
13
+ return `[${JSON.stringify(name)}, new Map([${inner}])]`;
14
+ }).join(", ")}]);\n`;
15
+ }
16
+ /** Source for the `pnpmfile` virtual module: createHooks over base + manifest. */
17
+ function emitPnpmfileModule(base, manifest, name) {
18
+ return [
19
+ "import { createHooks } from \"rolldown-pnpm-config/runtime\";",
20
+ `export const hooks = createHooks(${JSON.stringify(sortKeys(base))}, ${JSON.stringify(sortKeys(manifest))}, ${JSON.stringify(name)});`,
21
+ ""
22
+ ].join("\n");
23
+ }
24
+
25
+ //#endregion
26
+ export { emitCatalogsModule, emitPnpmfileModule, sortKeys };
package/registry.js ADDED
@@ -0,0 +1,8 @@
1
+ import { DESCRIPTORS, deriveRegistry } from "./descriptors/index.js";
2
+
3
+ //#region src/registry.ts
4
+ /** Maps each known pnpm field to its strategy + default enforcement. Derived from the descriptor table. @internal */
5
+ const FIELD_REGISTRY = deriveRegistry(DESCRIPTORS);
6
+
7
+ //#endregion
8
+ export { FIELD_REGISTRY };
package/runtime/ctx.js ADDED
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ //#region src/runtime/ctx.ts
5
+ /**
6
+ * Resolve the consuming repo's root package name. Prefers pnpm's
7
+ * `rootProjectManifest.name`, falling back to reading `package.json` from the
8
+ * workspace/lockfile dir.
9
+ *
10
+ * @internal
11
+ */
12
+ function resolveRootName(config) {
13
+ const c = config;
14
+ if (c.rootProjectManifest?.name) return c.rootProjectManifest.name;
15
+ const rootDir = c.rootProjectManifestDir ?? c.lockfileDir ?? c.workspaceDir ?? c.dir ?? process.cwd();
16
+ try {
17
+ return JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8")).name;
18
+ } catch {
19
+ return;
20
+ }
21
+ }
22
+ /**
23
+ * Drop packages assigned to the consuming repo from a merged hoist list.
24
+ * Drop packages listed in the per-repo exclusion table (`byRepo`).
25
+ *
26
+ * @param merged - The full merged hoist-pattern list before repo-specific exclusions.
27
+ * @param ctx - Runtime context; `ctx.rootName` is the consuming repo's root `package.json` `name`.
28
+ * @param byRepo Keyed by consuming-repo package.json name; value = hoist patterns dropped in that repo.
29
+ * @internal
30
+ */
31
+ function excludeByRepo(merged, ctx, byRepo) {
32
+ const exclude = ctx.rootName ? byRepo[ctx.rootName] : void 0;
33
+ if (!exclude || exclude.length === 0) return merged;
34
+ const set = new Set(exclude);
35
+ return merged.filter((p) => !set.has(p));
36
+ }
37
+
38
+ //#endregion
39
+ export { excludeByRepo, resolveRootName };
@@ -0,0 +1,36 @@
1
+ //#region src/runtime/enforcement.ts
2
+ /**
3
+ * Thrown when an `error`-enforced field diverges. A zero-dependency plain
4
+ * `Error` subclass (NOT an Effect type) so it survives in the bundled pnpmfile.
5
+ * It is intended to fail the install and must never be swallowed by an install
6
+ * guard — see the note in `runtime/index.ts`.
7
+ *
8
+ * @internal
9
+ */
10
+ var EnforcementError = class extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "EnforcementError";
14
+ }
15
+ };
16
+ /**
17
+ * Apply enforcement to a strategy result, partitioning its divergences into
18
+ * override and security buckets for the runtime to print. When enforcement is
19
+ * `error` and there is at least one divergence, throws {@link EnforcementError}.
20
+ *
21
+ * @internal
22
+ */
23
+ function applyEnforcement(field, result, enforcement) {
24
+ const overrides = [];
25
+ const security = [];
26
+ if (result.divergences.length > 0 && enforcement === "error") throw new EnforcementError(`Field "${field}" is enforced (error) but the local config diverges: ${result.divergences.map((d) => d.setting).join(", ")}`);
27
+ if (result.divergences.length > 0 && enforcement === "warn") for (const d of result.divergences) (d.kind === "security" ? security : overrides).push(d);
28
+ return {
29
+ value: result.merged,
30
+ overrides,
31
+ security
32
+ };
33
+ }
34
+
35
+ //#endregion
36
+ export { EnforcementError, applyEnforcement };
@@ -0,0 +1,37 @@
1
+ //#region src/runtime/strategies/arrays.ts
2
+ function unionSort(managed, local) {
3
+ const set = new Set(managed);
4
+ for (const item of local ?? []) set.add(item);
5
+ return [...set].sort((a, b) => a.localeCompare(b));
6
+ }
7
+ /**
8
+ * Union + sort string arrays; child entries are added to the managed set. Quiet.
9
+ *
10
+ * @internal
11
+ */
12
+ const arrayUnion = (base, local) => ({
13
+ merged: unionSort(base ?? [], local),
14
+ divergences: []
15
+ });
16
+ /**
17
+ * Per-axis union of a record of string arrays; drops empty axes. Quiet.
18
+ *
19
+ * @internal
20
+ */
21
+ const arrayRecordUnion = (base, local) => {
22
+ const managed = base ?? {};
23
+ const child = local ?? {};
24
+ const keys = /* @__PURE__ */ new Set([...Object.keys(managed), ...Object.keys(child)]);
25
+ const result = {};
26
+ for (const key of [...keys].sort((a, b) => a.localeCompare(b))) {
27
+ const merged = unionSort(managed[key] ?? [], child[key]);
28
+ if (merged.length > 0) result[key] = merged;
29
+ }
30
+ return {
31
+ merged: result,
32
+ divergences: []
33
+ };
34
+ };
35
+
36
+ //#endregion
37
+ export { arrayRecordUnion, arrayUnion };
@@ -0,0 +1,36 @@
1
+ //#region src/runtime/strategies/catalogs.ts
2
+ /**
3
+ * Merge each named catalog; child wins per package. Emits override divergences
4
+ * when a local version differs from the managed one.
5
+ *
6
+ * @internal
7
+ */
8
+ const catalogs = (base, local) => {
9
+ const managed = base ?? {};
10
+ const child = local ?? {};
11
+ const divergences = [];
12
+ const merged = { ...child };
13
+ for (const [name, entries] of Object.entries(managed)) {
14
+ const childCat = child[name] ?? {};
15
+ const out = { ...entries };
16
+ for (const [pkg, childVersion] of Object.entries(childCat)) {
17
+ const managedVersion = entries[pkg];
18
+ if (managedVersion !== void 0 && managedVersion !== childVersion) divergences.push({
19
+ setting: `catalogs.${name}.${pkg}`,
20
+ managedValue: managedVersion,
21
+ localValue: childVersion,
22
+ detail: "Local version overrides the managed version.",
23
+ kind: "override"
24
+ });
25
+ out[pkg] = childVersion;
26
+ }
27
+ merged[name] = out;
28
+ }
29
+ return {
30
+ merged,
31
+ divergences
32
+ };
33
+ };
34
+
35
+ //#endregion
36
+ export { catalogs };
@@ -0,0 +1,46 @@
1
+ //#region src/runtime/strategies/maps.ts
2
+ /**
3
+ * `{...managed, ...child}` — child wins per key. Quiet. Merges two maps,
4
+ * preferring child values.
5
+ *
6
+ * @internal
7
+ */
8
+ const mapChildWins = (base, local) => {
9
+ const managed = base ?? {};
10
+ const child = local;
11
+ return {
12
+ merged: child ? {
13
+ ...managed,
14
+ ...child
15
+ } : { ...managed },
16
+ divergences: []
17
+ };
18
+ };
19
+ /**
20
+ * `{...managed, ...child}`; flags enabling a build the managed config blocked.
21
+ * Detects allow-builds loosening.
22
+ *
23
+ * @internal
24
+ */
25
+ const allowBuilds = (base, local) => {
26
+ const managed = base ?? {};
27
+ const child = local ?? {};
28
+ const divergences = [];
29
+ for (const [pkg, childAllowed] of Object.entries(child)) if (childAllowed === true && managed[pkg] === false) divergences.push({
30
+ setting: `allowBuilds.${pkg}`,
31
+ managedValue: "false",
32
+ localValue: "true",
33
+ detail: `Enables build scripts for "${pkg}" that the managed config blocked.`,
34
+ kind: "security"
35
+ });
36
+ return {
37
+ merged: {
38
+ ...managed,
39
+ ...child
40
+ },
41
+ divergences
42
+ };
43
+ };
44
+
45
+ //#endregion
46
+ export { allowBuilds, mapChildWins };
@@ -0,0 +1,57 @@
1
+ //#region src/runtime/strategies/overrides.ts
2
+ function mergeMapDetect(prefix, managed, child) {
3
+ const merged = { ...managed };
4
+ const divergences = [];
5
+ for (const [k, childVersion] of Object.entries(child)) {
6
+ const managedVersion = managed[k];
7
+ if (managedVersion !== void 0 && managedVersion !== childVersion) divergences.push({
8
+ setting: `${prefix}.${k}`,
9
+ managedValue: managedVersion,
10
+ localValue: childVersion,
11
+ detail: "Local version overrides the managed version.",
12
+ kind: "override"
13
+ });
14
+ merged[k] = childVersion;
15
+ }
16
+ return {
17
+ merged,
18
+ divergences
19
+ };
20
+ }
21
+ /**
22
+ * Security overrides: child wins per key; any diff → override divergence.
23
+ * Merge overrides, flagging local divergences.
24
+ *
25
+ * @internal
26
+ */
27
+ const overrides = (base, local) => {
28
+ const { merged, divergences } = mergeMapDetect("overrides", base ?? {}, local ?? {});
29
+ return {
30
+ merged,
31
+ divergences
32
+ };
33
+ };
34
+ /**
35
+ * peerDependencyRules: `allowedVersions` is override-detected; `ignoreMissing`
36
+ * and `allowAny` are unioned + sorted. Merges peer-dependency rules,
37
+ * flagging version overrides.
38
+ *
39
+ * @internal
40
+ */
41
+ const peerDependencyRules = (base, local) => {
42
+ const managed = base ?? {};
43
+ const child = local ?? {};
44
+ const av = mergeMapDetect("peerDependencyRules.allowedVersions", managed.allowedVersions ?? {}, child.allowedVersions ?? {});
45
+ const union = (s = [], c = []) => [.../* @__PURE__ */ new Set([...s, ...c])].sort((a, b) => a.localeCompare(b));
46
+ return {
47
+ merged: {
48
+ allowedVersions: av.merged,
49
+ ignoreMissing: union(managed.ignoreMissing, child.ignoreMissing),
50
+ allowAny: union(managed.allowAny, child.allowAny)
51
+ },
52
+ divergences: av.divergences
53
+ };
54
+ };
55
+
56
+ //#endregion
57
+ export { overrides, peerDependencyRules };
@@ -0,0 +1,57 @@
1
+ //#region src/runtime/strategies/scalar.ts
2
+ /**
3
+ * `child ?? base` — quiet (no divergences). Prefer local, fall back to managed.
4
+ *
5
+ * @internal
6
+ */
7
+ const scalar = (base, local) => ({
8
+ merged: local ?? base,
9
+ divergences: []
10
+ });
11
+ /**
12
+ * `child ?? base`; flags when child disables a managed boolean. The strategy is
13
+ * field-agnostic, so it emits `setting: ""`; the runtime fills the field name.
14
+ * Detects flag loosening.
15
+ *
16
+ * @internal
17
+ */
18
+ const securityFlag = (base, local) => {
19
+ const merged = local ?? base;
20
+ const divergences = [];
21
+ if (base === true && local === false) divergences.push({
22
+ setting: "",
23
+ managedValue: "true",
24
+ localValue: "false",
25
+ detail: "Disables a security check the managed config enabled.",
26
+ kind: "security"
27
+ });
28
+ return {
29
+ merged,
30
+ divergences
31
+ };
32
+ };
33
+ /**
34
+ * `child ?? base`; flags when child lowers the value. Field-agnostic, so it
35
+ * emits `setting: ""`; the runtime fills the field name. Detects
36
+ * minimum-release-age loosening.
37
+ *
38
+ * @internal
39
+ */
40
+ const securityMin = (base, local) => {
41
+ const merged = local ?? base;
42
+ const divergences = [];
43
+ if (typeof base === "number" && typeof local === "number" && local < base) divergences.push({
44
+ setting: "",
45
+ managedValue: String(base),
46
+ localValue: String(local),
47
+ detail: `Shortens the release-age quarantine from ${base} to ${local} minutes.`,
48
+ kind: "security"
49
+ });
50
+ return {
51
+ merged,
52
+ divergences
53
+ };
54
+ };
55
+
56
+ //#endregion
57
+ export { scalar, securityFlag, securityMin };
@@ -0,0 +1,27 @@
1
+ import { arrayRecordUnion, arrayUnion } from "./arrays.js";
2
+ import { catalogs } from "./catalogs.js";
3
+ import { allowBuilds, mapChildWins } from "./maps.js";
4
+ import { overrides, peerDependencyRules } from "./overrides.js";
5
+ import { scalar, securityFlag, securityMin } from "./scalar.js";
6
+
7
+ //#region src/runtime/strategies/table.ts
8
+ /**
9
+ * Built-in strategies keyed by manifest name.
10
+ *
11
+ * @internal
12
+ */
13
+ const STRATEGY_TABLE = {
14
+ scalar,
15
+ catalogs,
16
+ mapChildWins,
17
+ arrayUnion,
18
+ arrayRecordUnion,
19
+ overrides,
20
+ peerDependencyRules,
21
+ securityFlag,
22
+ securityMin,
23
+ allowBuilds
24
+ };
25
+
26
+ //#endregion
27
+ export { STRATEGY_TABLE };
@@ -0,0 +1,61 @@
1
+ //#region src/runtime/warnings.ts
2
+ const WARNING_BOX_WIDTH = 75;
3
+ function pad(line) {
4
+ return `│${line}${" ".repeat(Math.max(0, WARNING_BOX_WIDTH - line.length - 2))}│`;
5
+ }
6
+ /**
7
+ * Format override divergences into a prominent warning box for console output,
8
+ * tagged with the emitting config's `name`. `Divergence.setting` is the
9
+ * already-resolved config path, printed directly.
10
+ *
11
+ * @internal
12
+ */
13
+ function formatOverrideWarning(divergences, name) {
14
+ if (divergences.length === 0) return "";
15
+ const border = "─".repeat(WARNING_BOX_WIDTH - 2);
16
+ const lines = [];
17
+ lines.push(`┌${border}┐`);
18
+ lines.push(pad(` [${name}]`));
19
+ lines.push(pad(" ⚠️ CATALOG OVERRIDE DETECTED"));
20
+ lines.push(`├${border}┤`);
21
+ lines.push(pad(" The following entries override managed versions:"));
22
+ lines.push(pad(""));
23
+ for (const d of divergences) {
24
+ lines.push(pad(` ${d.setting}`));
25
+ lines.push(pad(` Managed version: ${d.managedValue}`));
26
+ lines.push(pad(` Local override: ${d.localValue}`));
27
+ lines.push(pad(""));
28
+ }
29
+ lines.push(pad(" Local versions will be used. To use the managed defaults, remove"));
30
+ lines.push(pad(" these entries from your pnpm-workspace.yaml."));
31
+ lines.push(`└${border}┘`);
32
+ return lines.join("\n");
33
+ }
34
+ /**
35
+ * Format security-loosening divergences into a prominent box, tagged with the
36
+ * emitting config's `name`.
37
+ *
38
+ * @internal
39
+ */
40
+ function formatSecurityWarning(divergences, name) {
41
+ if (divergences.length === 0) return "";
42
+ const border = "─".repeat(WARNING_BOX_WIDTH - 2);
43
+ const lines = [];
44
+ lines.push(`┌${border}┐`);
45
+ lines.push(pad(` [${name}]`));
46
+ lines.push(pad(" ⚠️ SECURITY OVERRIDE DETECTED"));
47
+ lines.push(`├${border}┤`);
48
+ lines.push(pad(" The following entries weaken managed security defaults:"));
49
+ lines.push(pad(""));
50
+ for (const d of divergences) {
51
+ lines.push(pad(` ${d.setting}: managed=${d.managedValue} -> local=${d.localValue}`));
52
+ lines.push(pad(` ${d.detail}`));
53
+ lines.push(pad(""));
54
+ }
55
+ lines.push(pad(" Local values will be used. Review these before shipping."));
56
+ lines.push(`└${border}┘`);
57
+ return lines.join("\n");
58
+ }
59
+
60
+ //#endregion
61
+ export { formatOverrideWarning, formatSecurityWarning };
package/runtime.d.ts ADDED
@@ -0,0 +1,111 @@
1
+ //#region src/runtime/types.d.ts
2
+ /**
3
+ * Minimal pnpm config shape — only the fields this plugin reads/writes.
4
+ *
5
+ * @public
6
+ */
7
+ interface PnpmConfig {
8
+ /** Named catalogs injected into pnpm's workspace configuration. */
9
+ catalogs?: Record<string, Record<string, string>>;
10
+ [key: string]: unknown;
11
+ }
12
+ /**
13
+ * The pnpm pnpmfile hooks object.
14
+ *
15
+ * @public
16
+ */
17
+ interface PnpmHooks {
18
+ /** Merges the frozen, managed config into the consumer's pnpm config. */
19
+ updateConfig(config: PnpmConfig): PnpmConfig;
20
+ }
21
+ /**
22
+ * A single detected difference between the managed value and the consumer's
23
+ * local value, classified as either an override or a security loosening.
24
+ *
25
+ * @internal
26
+ */
27
+ interface Divergence {
28
+ readonly setting: string;
29
+ readonly managedValue: string;
30
+ readonly localValue: string;
31
+ readonly detail: string;
32
+ readonly kind: "override" | "security";
33
+ }
34
+ /**
35
+ * Per-install context resolved once and threaded into every strategy.
36
+ *
37
+ * @internal
38
+ */
39
+ interface RuntimeCtx {
40
+ readonly rootName: string | undefined;
41
+ }
42
+ /**
43
+ * The result of running a strategy: the merged value plus any divergences the
44
+ * strategy detected along the way.
45
+ *
46
+ * @internal
47
+ */
48
+ interface StrategyResult {
49
+ readonly merged: unknown;
50
+ readonly divergences: readonly Divergence[];
51
+ }
52
+ /**
53
+ * A pure merge function for one field: combine the managed base with the local
54
+ * value, reporting any divergences.
55
+ *
56
+ * @internal
57
+ */
58
+ type Strategy = (base: unknown, local: unknown, ctx: RuntimeCtx) => StrategyResult;
59
+ /**
60
+ * How a field's divergences are enforced: silent, console warning, or a thrown
61
+ * error that fails the install. Part of the public authoring API via
62
+ * `FieldInput` and the runtime `createHooks` manifest.
63
+ *
64
+ * @public
65
+ */
66
+ type Enforcement = "absent" | "warn" | "error";
67
+ /**
68
+ * One field's manifest entry: which strategy merges it, how it is enforced, and
69
+ * any strategy-specific options (e.g. a refine table). Part of the public
70
+ * `createHooks` contract.
71
+ *
72
+ * @public
73
+ */
74
+ interface ManifestEntry {
75
+ readonly strategy: string;
76
+ readonly enforcement: Enforcement;
77
+ readonly options?: Record<string, unknown>;
78
+ }
79
+ /**
80
+ * The field → strategy/enforcement manifest emitted at build time and consumed
81
+ * by the public `createHooks`.
82
+ *
83
+ * @public
84
+ */
85
+ type Manifest = Record<string, ManifestEntry>;
86
+ /**
87
+ * The field → frozen value base emitted at build time and consumed by the
88
+ * public `createHooks`.
89
+ *
90
+ * @public
91
+ */
92
+ type Base = Record<string, unknown>;
93
+ //#endregion
94
+ //#region src/runtime/index.d.ts
95
+ /**
96
+ * Build the pnpm hooks from frozen base data + a field→strategy manifest.
97
+ * Zero dependencies — bundled verbatim into the shipped pnpmfile.
98
+ *
99
+ * @remarks
100
+ * `updateConfig` deliberately has no catch-and-fall-back-to-local guard: an
101
+ * `error`-enforced divergence throws `EnforcementError`, which is meant to
102
+ * propagate and fail the install. If a swallow-guard is ever added here, it MUST
103
+ * rethrow `EnforcementError` (check `err instanceof EnforcementError` /
104
+ * `err.name === "EnforcementError"`) rather than fall back to the local config.
105
+ *
106
+ * @public
107
+ */
108
+ declare function createHooks(base: Base, manifest: Manifest, name: string): PnpmHooks;
109
+ //#endregion
110
+ export { Base, Divergence, Enforcement, Manifest, ManifestEntry, PnpmConfig, PnpmHooks, RuntimeCtx, Strategy, StrategyResult, createHooks };
111
+ //# sourceMappingURL=runtime.d.ts.map