everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
187 lines (185 loc) • 8.11 kB
JavaScript
import { isPlainObject } from "./merge.mjs";
import { resolveSlotVersion } from "../deploy/version-manifest.mjs";
import { ensureObjectContainers, walkConfigSlots } from "../config-slots.mjs";
import { SHARED_DEP_SPECS } from "every-plugin/shared-deps-spec";
//#region src/services/resolution/release-plane.ts
/**
* The release plane (ADR 0034 amendment — release-scoped extends
* inheritance): a platform-pinned child composes the slots the pinned
* release recorded instead of drifting with the parent's live config
* pointer. One implementation, two adapters: the deploy train stamps the
* child's publish payload before it publishes, and every consumer boot
* stamps the extends-merged shape before composition — so production, local
* dev, and type generation all resolve the same release bytes.
*
* Without the plane, a root publish (rc.N+1) moves the inherited auth pin
* while a platform-pinned child's own ui stays built against rc.N — the
* host's shared-singleton guard (#546) refuses the composition at boot.
*/
/** Keys that name where a slot's code comes from — owning the slot's bytes. */
const SLOT_SOURCE_KEYS = [
"production",
"development",
"pin",
"extends",
"proxy"
];
/**
* A slot is authored when its entry names a code source (or opts out with
* `ui: false` / owns it via a bare plugin ref). An entry that only configures
* an inherited slot — `variables`, `secrets`, `routes`, … — does not own its
* bytes, so the release still pins them.
*/
function declaresSlotSource(entry) {
if (entry === false || typeof entry === "string") return true;
return SLOT_SOURCE_KEYS.some((key) => entry[key] !== void 0);
}
/** Authored slot paths: every slot whose code source the leaf itself declares. */
function authoredSlotPaths(config) {
const paths = /* @__PURE__ */ new Set();
walkConfigSlots(config, ({ slot, entry, isPluginRef }) => {
if (declaresSlotSource(entry)) paths.add(slot);
if (isPluginRef) paths.add(`${slot}.ui`);
});
return paths;
}
function stampInheritedReleaseSlots(input) {
const slots = input.slots ?? {};
const authored = authoredSlotPaths(input.authoredConfig);
const stamped = [];
const omitted = [];
for (const [slotPath, record] of Object.entries(slots)) {
const isAppSlot = /^app\.[^.]+(\.ui)?$/.test(slotPath);
if (input.mode === "payload" && !isAppSlot) continue;
if (authored.has(slotPath)) continue;
if (input.mode === "consumer" && !slotEntryAt(input.config, slotPath)) {
omitted.push(slotPath);
continue;
}
stampSlotRecord(input.config, slotPath, record);
stamped.push(slotPath);
}
return {
stamped,
omitted
};
}
/** The entry object at a dotted slot path (2–3 segments), if it exists. */
function slotEntryAt(config, slotPath) {
const segments = slotPath.split(".");
if (segments.length !== 2 && segments.length !== 3 || !segments[0] || !segments[1]) return;
let node = config;
for (const segment of segments.slice(0, -1)) {
if (!isPlainObject(node)) return void 0;
node = node[segment];
}
if (!isPlainObject(node)) return void 0;
const leaf = segments.at(-1);
if (leaf === void 0) return void 0;
const entry = node[leaf];
return isPlainObject(entry) ? entry : void 0;
}
/** Write `{ production, pin }` for one dotted slot path (2–3 segments),
* merging into an existing entry rather than replacing it. */
function stampSlotRecord(config, slotPath, record) {
const segments = slotPath.split(".");
if (segments.length !== 2 && segments.length !== 3 || !segments[0] || !segments[1]) return;
const leaf = segments.at(-1);
const container = ensureObjectContainers(config, segments.slice(0, -1));
if (!leaf) return;
const existing = container[leaf];
const entry = isPlainObject(existing) ? { ...existing } : {};
entry.production = record.base;
entry.pin = {
manifest: record.pin.manifest,
integrity: record.pin.integrity
};
container[leaf] = entry;
}
/**
* The publish key an owned slot realigns through: the workspace key the
* scoped deploy train and `bos plugin publish` understand — `app.<key>` and
* `app.<key>.ui` publish as the app key (`ui`, `api`, …), `plugins.<id>` and
* `plugins.<id>.ui` as the plugin id. Slot paths themselves are not keys.
*/
function slotPublishKey(slot) {
const [domain, key] = slot.split(".");
return domain === "app" || domain === "plugins" ? key ?? slot : slot;
}
/** The repair instruction for an owned slot diverging on shared singletons:
* app slots republish through the scoped deploy train (they are not plugins);
* plugin slots through the single-plugin publish. */
function ownedSlotRepair(slot) {
const key = slotPublishKey(slot);
return `run ${slot.startsWith("app.") ? `bos deploy --packages ${key}` : `bos plugin publish ${key}`} (built against an older chain)`;
}
/**
* The dev-boot chain audit (ADR 0034 + #546's shared-singleton trust):
* every pinned remote slot's embedded singleton versions are compared
* against the local workspace catalog — the same `satisfiesConstraint`
* family the deploy-train compat-window gate speaks, so one mismatch
* reads the same way in the train and on screen. One line per divergent
* slot, with the repair named for its ownership: inherited →
* `bos upgrade` (the pin trails the chain serving the slot); owned →
* `bos plugin publish <key>` (built against an older chain). Empty out:
* the whole plane composes coherently.
*
* Two plane-shape signals ride beside the version check:
* - a config slot the pinned manifest does not record is genuinely
* provisioned upstream after the pin — it composes live, unpinned bytes,
* and `bos upgrade` claims it;
* - a manifest slot the merged config does not compose is NOT an upgrade
* candidate — a child declaring its own `plugins` domain wholesale-
* replaces the inherited one by design, so that is an uncomposed-slot
* note, never a "run bos upgrade" instruction (an upgrade cannot change
* the merged shape).
*/
async function auditReleasePlane(input) {
const lines = [];
const strictness = new Map(SHARED_DEP_SPECS.map((spec) => [spec.name, spec.critical]));
const manifestSlotPaths = new Set(input.manifestSlotPaths ?? []);
for (const { slot, base, pin } of input.slots) {
const kind = input.authoredPaths.has(slot) ? "owned" : "inherited";
const repair = kind === "owned" ? ownedSlotRepair(slot) : "run bos upgrade (the pin trails the chain serving this slot)";
try {
const resolved = await resolveSlotVersion({
base,
pin,
...input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}
});
for (const [name, version] of Object.entries(resolved.sharedVersions ?? {})) {
if (!strictness.get(name)) continue;
const local = input.localVersionOf(name);
if (local !== void 0 && local === version) continue;
lines.push({
slot,
kind,
message: `${slot} was built against ${name}@${version}, this runtime composes ${name}@${local ?? "unknown"} — ${repair}`
});
break;
}
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
lines.push({
slot,
kind,
message: `${slot}'s version manifest does not serve at its production URL (${reason}) — ${repair}`
});
}
}
const pinnedSlots = new Set(input.slots.map((ref) => ref.slot));
for (const slot of input.configSlotPaths ?? []) if (!manifestSlotPaths.has(slot) && !pinnedSlots.has(slot) && !input.authoredPaths.has(slot)) lines.push({
slot,
kind: "provisioned",
message: `slot "${slot}" is served by the extends chain but missing from the ${input.pinnedReleaseVersion ?? "pinned"} release plane — it composes live, unpinned bytes; run bos upgrade to re-pin and claim it`
});
for (const slot of input.provisionedSlots ?? []) lines.push({
slot,
kind: "uncomposed",
message: `slot "${slot}" is recorded in the ${input.pinnedReleaseVersion ?? "pinned"} release but not composed by this config — declare it in your config's app/plugins domain to compose it`
});
return lines;
}
//#endregion
export { auditReleasePlane, authoredSlotPaths, slotPublishKey, stampInheritedReleaseSlots };
//# sourceMappingURL=release-plane.mjs.map