everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
162 lines (160 loc) • 7.67 kB
JavaScript
import { isPlainObject } from "../resolution/merge.mjs";
import { resolveSlotVersion } from "./version-manifest.mjs";
import { readWorkspaceCatalog } from "../workspace/catalog.mjs";
import { ensureObjectContainers, walkConfigSlots } from "../config-slots.mjs";
import { findWorkspaceRoot } from "../workspace/workspace.mjs";
import { readInstalledFrameworkVersion } from "../framework-version.mjs";
import { checkPlatformCompat, resolvePlatformManifest } from "../platform/manifest.mjs";
import { collectSlotPinRefs } from "./rollback.mjs";
//#region src/services/deploy/composition.ts
/**
* The deploy train's composition plane (ADR 0034 amendment — release-scoped
* extends inheritance): every remote slot the leaf does not author is a
* base-runtime slot, and its bytes must come from the child's pinned release
* instead of drifting with the parent's live config pointer.
*
* Without this 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 and
* the child's health degrades after its next redeploy. Two layers close it:
* the release manifest records its own slot-pin map (the `slots` field,
* composed on the release leg), and the deploy train stamps those pins onto
* the inherited slots of the child's publish payload — child-wins over the
* parent's live pointer at every consumer boot.
*/
/**
* Resolve the release manifest for a deploy: the authored `platform` pin via
* slot-first resolution (children), otherwise the latest pointer (root
* runtimes). Returns null when no manifest is published — a normal state for
* children pinned to pre-manifest releases. Resolution failures surface; the
* caller warns and continues unpinned (the composition gate still
* cross-checks the actual resolved bytes).
*/
async function resolveReleaseManifestForDeploy(input) {
return (await resolvePlatformManifest({
configDir: input.configDir,
localConfig: input.authoredConfig,
...input.resolveOptions
}))?.manifest ?? null;
}
/** Authored slot paths: every slot the leaf itself declares. */
function authoredSlotPaths(config) {
const paths = /* @__PURE__ */ new Set();
walkConfigSlots(config, ({ slot, isPluginRef }) => {
paths.add(slot);
if (isPluginRef) paths.add(`${slot}.ui`);
});
return paths;
}
/**
* Stamp inherited slots: for every `app.*` slot in the release manifest's
* `slots` map that the authored config does not declare, write the release's
* recorded production URL + pin into the payload. Child-wins deep merge then
* shadows the parent's live config pointer at every consumer boot, so the
* child composes exactly the bytes its pinned release published. Slots the
* child authors stay untouched, and a manifest without `slots` (pre-amendment
* releases) stamps nothing — the composition gate covers those children.
*/
function stampInheritedSlotsFromRelease(input) {
const slots = input.manifest.slots ?? {};
const authored = authoredSlotPaths(input.authoredConfig);
const stamped = [];
for (const [slotPath, record] of Object.entries(slots)) {
if (!/^app\.[^.]+(\.ui)?$/.test(slotPath)) continue;
if (authored.has(slotPath)) continue;
stampSlotRecord(input.payload, slotPath, record);
stamped.push(slotPath);
}
return { stamped };
}
/** Write `{ production, pin }` for one dotted slot path (2–3 segments),
* merging into an existing entry rather than replacing it. */
function stampSlotRecord(payload, slotPath, record) {
const segments = slotPath.split(".");
if (segments.length !== 2 && segments.length !== 3 || !segments[1]) return;
const leaf = segments[segments.length - 1];
const container = ensureObjectContainers(payload, 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 deploy-train composition gate: cross-compare the shared-singleton
* versions of every pinned UI slot in the extends-merged shape (the boot
* guard's mirror, #546), plus — for authored platform pins — the ADR 0034
* compat check of the child against its own release. Runs after pin
* verification, before publish.
*/
async function runCompositionGate(input) {
const violations = [...await compositionSkewViolations(input.resolvedShape, {
...input.fetchImpl ? { fetchImpl: input.fetchImpl } : {},
...input.verifiedSlots ? { verifiedSlots: input.verifiedSlots } : {}
})];
if (typeof input.authoredConfig.platform === "string" && input.resolvedRelease) violations.push(...platformCompatViolations(input.resolvedRelease, input.configDir));
return {
ok: violations.length === 0,
violations
};
}
/**
* Cross-compare each pinned UI slot's embedded shared-singleton versions. An
* unresolvable pin the pin/SRI verification leg already proved servable is
* skipped (transient fetch failure); any unresolvable pin outside that set is
* reported — the boot's version-manifest derivation would refuse it anyway.
*/
async function compositionSkewViolations(merged, opts = {}) {
const uiSlots = collectSlotPinRefs(merged).filter((ref) => ref.slot === "app.ui" || ref.slot.endsWith(".ui"));
if (uiSlots.length === 0) return [];
const versionsByName = /* @__PURE__ */ new Map();
const violations = [];
await Promise.all(uiSlots.map(async ({ slot, base, pin }) => {
try {
const resolved = await resolveSlotVersion({
base,
pin,
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
});
for (const [name, version] of Object.entries(resolved.sharedVersions ?? {})) {
const uses = versionsByName.get(name) ?? [];
uses.push({
slot,
version
});
versionsByName.set(name, uses);
}
} catch (error) {
if (opts.verifiedSlots?.has(slot)) return;
const reason = error instanceof Error ? error.message : String(error);
violations.push(`Pinned UI slot "${slot}" version manifest does not serve at its production URL (${reason}) — a consumer boot cannot derive its shared singletons; investigate the storage origin or redeploy the bundle.`);
}
}));
for (const [name, uses] of versionsByName) {
const first = uses[0];
if (!first) continue;
const diverged = uses.find((use) => use.version !== first.version);
if (!diverged) continue;
violations.push(`Shared singleton "${name}" skew across UI sources: "${first.slot}" was built against ${first.version}, "${diverged.slot}" against ${diverged.version} — the composition refuses to load a split module graph. Redeploy the older bundle: \`bos upgrade\` realigns the platform pin, \`bos deploy\` rebuilds against it (issue #546).`);
}
return violations;
}
/**
* The ADR 0034 compat check, scoping the child's installed framework version
* and workspace catalog against its OWN pinned release — a child resolving
* via slot-first never fails for being older than the parent's latest.
*/
function platformCompatViolations(manifest, configDir) {
const root = findWorkspaceRoot(configDir)?.dir ?? configDir;
return checkPlatformCompat(manifest, {
platformVersion: readInstalledFrameworkVersion(configDir, "everything-dev"),
catalog: readWorkspaceCatalog(root)
}).map((violation) => `${violation.message} — run \`bos upgrade\` to realign your platform pin with \`bos deploy\``);
}
//#endregion
export { authoredSlotPaths, compositionSkewViolations, platformCompatViolations, resolveReleaseManifestForDeploy, runCompositionGate, stampInheritedSlotsFromRelease };
//# sourceMappingURL=composition.mjs.map