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
package/plugin/freeze.js
ADDED
|
@@ -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 };
|
package/plugin/index.js
ADDED
|
@@ -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
|