UNPKG

everything-dev

Version:

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

1 lines • 13.3 kB
{"version":3,"file":"composition.mjs","names":[],"sources":["../../../src/services/deploy/composition.ts"],"sourcesContent":["import type { BosConfigInput } from \"../../types\";\nimport { ensureObjectContainers, walkConfigSlots } from \"../config-slots\";\nimport { readInstalledFrameworkVersion } from \"../framework-version\";\nimport {\n checkPlatformCompat,\n type PlatformReleaseManifest,\n type ReleaseSlotRecord,\n type ResolvePlatformManifestOptions,\n resolvePlatformManifest,\n} from \"../platform/manifest\";\nimport { isPlainObject } from \"../resolution/merge\";\nimport { readWorkspaceCatalog } from \"../workspace/catalog\";\nimport { findWorkspaceRoot } from \"../workspace/workspace\";\nimport { collectSlotPinRefs } from \"./rollback\";\nimport { resolveSlotVersion } from \"./version-manifest\";\n\n/**\n * The deploy train's composition plane (ADR 0034 amendment — release-scoped\n * extends inheritance): every remote slot the leaf does not author is a\n * base-runtime slot, and its bytes must come from the child's pinned release\n * instead of drifting with the parent's live config pointer.\n *\n * Without this plane, a root publish (rc.N+1) moves the inherited auth pin\n * while a platform-pinned child's own ui stays built against rc.N — the\n * host's shared-singleton guard (#546) refuses the composition at boot and\n * the child's health degrades after its next redeploy. Two layers close it:\n * the release manifest records its own slot-pin map (the `slots` field,\n * composed on the release leg), and the deploy train stamps those pins onto\n * the inherited slots of the child's publish payload — child-wins over the\n * parent's live pointer at every consumer boot.\n */\n\n/**\n * Resolve the release manifest for a deploy: the authored `platform` pin via\n * slot-first resolution (children), otherwise the latest pointer (root\n * runtimes). Returns null when no manifest is published — a normal state for\n * children pinned to pre-manifest releases. Resolution failures surface; the\n * caller warns and continues unpinned (the composition gate still\n * cross-checks the actual resolved bytes).\n */\nexport async function resolveReleaseManifestForDeploy(input: {\n configDir: string;\n authoredConfig: Record<string, unknown>;\n /** test/registry seam — threaded to the platform-manifest resolution */\n resolveOptions?: Partial<ResolvePlatformManifestOptions>;\n}): Promise<PlatformReleaseManifest | null> {\n const resolved = await resolvePlatformManifest({\n configDir: input.configDir,\n localConfig: input.authoredConfig,\n ...input.resolveOptions,\n });\n return resolved?.manifest ?? null;\n}\n\n/** Authored slot paths: every slot the leaf itself declares. */\nexport function authoredSlotPaths(config: Record<string, unknown>): Set<string> {\n const paths = new Set<string>();\n walkConfigSlots(config, ({ slot, isPluginRef }) => {\n paths.add(slot);\n // KEEP: a bare-string plugin ref (`extends: \"bos://…\"`) owns its whole\n // surface — composed resolution derives its ui from the extended config,\n // so the ownership set must include it independently of the `ui` key.\n if (isPluginRef) paths.add(`${slot}.ui`);\n });\n return paths;\n}\n\n/** Stamped slot paths (diagnostic), in stamp order. */\nexport interface StampOutcome {\n stamped: string[];\n}\n\n/**\n * Stamp inherited slots: for every `app.*` slot in the release manifest's\n * `slots` map that the authored config does not declare, write the release's\n * recorded production URL + pin into the payload. Child-wins deep merge then\n * shadows the parent's live config pointer at every consumer boot, so the\n * child composes exactly the bytes its pinned release published. Slots the\n * child authors stay untouched, and a manifest without `slots` (pre-amendment\n * releases) stamps nothing — the composition gate covers those children.\n */\nexport function stampInheritedSlotsFromRelease(input: {\n payload: Record<string, unknown>;\n authoredConfig: Record<string, unknown>;\n manifest: PlatformReleaseManifest;\n}): StampOutcome {\n const slots = input.manifest.slots ?? {};\n const authored = authoredSlotPaths(input.authoredConfig);\n const stamped: string[] = [];\n\n for (const [slotPath, record] of Object.entries(slots)) {\n // KEEP: only app.* slots are stampable — a child's declared `plugins`\n // object owns the whole plugin domain in one wholesale replace\n // (mergeBosConfigWithExtends), so stamping parent plugin slots would\n // both expand the child's composed plugin set and produce\n // schema-invalid entries (the stamped ui record alone carries no\n // required `name`; the wholesale replace never fills it — the\n // feedback.nearbuilders.org rc.26 incident).\n if (!/^app\\.[^.]+(\\.ui)?$/.test(slotPath)) continue;\n if (authored.has(slotPath)) continue;\n stampSlotRecord(input.payload, slotPath, record);\n stamped.push(slotPath);\n }\n return { stamped };\n}\n\n/** Write `{ production, pin }` for one dotted slot path (2–3 segments),\n * merging into an existing entry rather than replacing it. */\nfunction stampSlotRecord(\n payload: Record<string, unknown>,\n slotPath: string,\n record: ReleaseSlotRecord,\n): void {\n const segments = slotPath.split(\".\");\n if ((segments.length !== 2 && segments.length !== 3) || !segments[1]) return;\n const leaf = segments[segments.length - 1];\n const container = ensureObjectContainers(payload, segments.slice(0, -1));\n if (!leaf) return;\n const existing = container[leaf];\n const entry: Record<string, unknown> = isPlainObject(existing) ? { ...existing } : {};\n entry.production = record.base;\n entry.pin = { manifest: record.pin.manifest, integrity: record.pin.integrity };\n container[leaf] = entry;\n}\n\nexport interface CompositionGateReport {\n ok: boolean;\n violations: string[];\n}\n\nexport interface CompositionGateInput {\n configDir: string;\n authoredConfig: Record<string, unknown>;\n resolvedShape: Record<string, unknown>;\n resolvedRelease: PlatformReleaseManifest | null;\n /**\n * The slot names the leaf-payload pin verification leg proved servable;\n * an unresolvable merged-shape pin outside this set is reported as a\n * violation instead of skipped-silently (inherited pins are not covered\n * by that leg).\n */\n verifiedSlots?: ReadonlySet<string>;\n /** test seam — threaded to the version-manifest fetch */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * The deploy-train composition gate: cross-compare the shared-singleton\n * versions of every pinned UI slot in the extends-merged shape (the boot\n * guard's mirror, #546), plus — for authored platform pins — the ADR 0034\n * compat check of the child against its own release. Runs after pin\n * verification, before publish.\n */\nexport async function runCompositionGate(\n input: CompositionGateInput,\n): Promise<CompositionGateReport> {\n const violations: string[] = [\n ...(await compositionSkewViolations(input.resolvedShape, {\n ...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}),\n ...(input.verifiedSlots ? { verifiedSlots: input.verifiedSlots } : {}),\n })),\n ];\n if (typeof input.authoredConfig.platform === \"string\" && input.resolvedRelease) {\n violations.push(...platformCompatViolations(input.resolvedRelease, input.configDir));\n }\n return { ok: violations.length === 0, violations };\n}\n\n/**\n * Cross-compare each pinned UI slot's embedded shared-singleton versions. An\n * unresolvable pin the pin/SRI verification leg already proved servable is\n * skipped (transient fetch failure); any unresolvable pin outside that set is\n * reported — the boot's version-manifest derivation would refuse it anyway.\n */\nexport async function compositionSkewViolations(\n merged: Record<string, unknown>,\n opts: { fetchImpl?: typeof fetch; verifiedSlots?: ReadonlySet<string> } = {},\n): Promise<string[]> {\n const uiSlots = collectSlotPinRefs(merged as BosConfigInput).filter(\n (ref) => ref.slot === \"app.ui\" || ref.slot.endsWith(\".ui\"),\n );\n if (uiSlots.length === 0) return [];\n\n const versionsByName = new Map<string, Array<{ slot: string; version: string }>>();\n const violations: string[] = [];\n\n await Promise.all(\n uiSlots.map(async ({ slot, base, pin }) => {\n try {\n const resolved = await resolveSlotVersion({\n base,\n pin,\n ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),\n });\n for (const [name, version] of Object.entries(resolved.sharedVersions ?? {})) {\n const uses = versionsByName.get(name) ?? [];\n uses.push({ slot, version });\n versionsByName.set(name, uses);\n }\n } catch (error) {\n if (opts.verifiedSlots?.has(slot)) return;\n const reason = error instanceof Error ? error.message : String(error);\n violations.push(\n `Pinned UI slot \"${slot}\" version manifest does not serve at its production URL (${reason}) — ` +\n `a consumer boot cannot derive its shared singletons; investigate the storage origin or redeploy the bundle.`,\n );\n }\n }),\n );\n\n for (const [name, uses] of versionsByName) {\n const first = uses[0];\n if (!first) continue;\n const diverged = uses.find((use) => use.version !== first.version);\n if (!diverged) continue;\n violations.push(\n `Shared singleton \"${name}\" skew across UI sources: \"${first.slot}\" was built against ` +\n `${first.version}, \"${diverged.slot}\" against ${diverged.version} — the composition refuses to load ` +\n `a split module graph. Redeploy the older bundle: \\`bos upgrade\\` realigns the platform pin, ` +\n `\\`bos deploy\\` rebuilds against it (issue #546).`,\n );\n }\n return violations;\n}\n\n/**\n * The ADR 0034 compat check, scoping the child's installed framework version\n * and workspace catalog against its OWN pinned release — a child resolving\n * via slot-first never fails for being older than the parent's latest.\n */\nexport function platformCompatViolations(\n manifest: PlatformReleaseManifest,\n configDir: string,\n): string[] {\n const root = findWorkspaceRoot(configDir)?.dir ?? configDir;\n const violations = checkPlatformCompat(manifest, {\n platformVersion: readInstalledFrameworkVersion(configDir, \"everything-dev\"),\n catalog: readWorkspaceCatalog(root),\n });\n return violations.map(\n (violation) =>\n `${violation.message} — run \\`bos upgrade\\` to realign your platform pin with \\`bos deploy\\``,\n );\n}\n"],"mappings":"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"}