UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

167 lines (165 loc) • 8.05 kB
const require_merge = require('../resolution/merge.cjs'); const require_services_deploy_version_manifest = require('./version-manifest.cjs'); const require_catalog = require('../workspace/catalog.cjs'); const require_config_slots = require('../config-slots.cjs'); const require_workspace = require('../workspace/workspace.cjs'); const require_framework_version = require('../framework-version.cjs'); const require_manifest = require('../platform/manifest.cjs'); const require_rollback = require('./rollback.cjs'); //#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 require_manifest.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(); require_config_slots.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 = require_config_slots.ensureObjectContainers(payload, segments.slice(0, -1)); if (!leaf) return; const existing = container[leaf]; const entry = require_merge.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 = require_rollback.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 require_services_deploy_version_manifest.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 = require_workspace.findWorkspaceRoot(configDir)?.dir ?? configDir; return require_manifest.checkPlatformCompat(manifest, { platformVersion: require_framework_version.readInstalledFrameworkVersion(configDir, "everything-dev"), catalog: require_catalog.readWorkspaceCatalog(root) }).map((violation) => `${violation.message} — run \`bos upgrade\` to realign your platform pin with \`bos deploy\``); } //#endregion exports.authoredSlotPaths = authoredSlotPaths; exports.compositionSkewViolations = compositionSkewViolations; exports.platformCompatViolations = platformCompatViolations; exports.resolveReleaseManifestForDeploy = resolveReleaseManifestForDeploy; exports.runCompositionGate = runCompositionGate; exports.stampInheritedSlotsFromRelease = stampInheritedSlotsFromRelease; //# sourceMappingURL=composition.cjs.map