UNPKG

everything-dev

Version:

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

190 lines (188 loc) • 8.4 kB
const require_merge = require("./merge.cjs"); const require_services_deploy_version_manifest = require("../deploy/version-manifest.cjs"); const require_config_slots = require("../config-slots.cjs"); let every_plugin_shared_deps_spec = require("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(); require_config_slots.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 (!require_merge.isPlainObject(node)) return void 0; node = node[segment]; } if (!require_merge.isPlainObject(node)) return void 0; const leaf = segments.at(-1); if (leaf === void 0) return void 0; const entry = node[leaf]; return require_merge.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 = require_config_slots.ensureObjectContainers(config, 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 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(every_plugin_shared_deps_spec.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 require_services_deploy_version_manifest.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 exports.auditReleasePlane = auditReleasePlane; exports.authoredSlotPaths = authoredSlotPaths; exports.slotPublishKey = slotPublishKey; exports.stampInheritedReleaseSlots = stampInheritedReleaseSlots; //# sourceMappingURL=release-plane.cjs.map