UNPKG

everything-dev

Version:

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

458 lines (450 loc) • 17.9 kB
let node_fs = require("node:fs"); let node_path = require("node:path"); let effect = require("effect"); let every_plugin_ui_manifest = require("every-plugin/ui/manifest"); //#region src/services/codegen/runtime-map.ts /** * The composition inspector's generator (#591/#594): a *generated, * machine- and human-readable* effective route tree for a resolved runtime. * * Reads every ui source's route manifest (local `src/manifest.gen.json`, or * the published `<ui>/manifest.gen.json` for remote sources), applies the * same core-wins precedence the host's `constructTree` applies at runtime, * and emits: * * .bos/generated/runtime-map.json machine/human inventory * ui/src/lib/runtime-map.gen.ts typed REMOTE_ROUTES union + metadata * * This is a *static model* of composition: nav labels are runtime-only (they * live in route options, not manifests) and are reported as null. * * Loud degradation: a remote manifest that can't be fetched is reported as * UNRESOLVED and — in strict mode (`bos types gen`) — fails the run, so * typecheck catches an upstream rename instead of silently shrinking the * map. Dev/build/train paths run warn-mode (a flaky remote must not block a * boot; the runtime load fails loudly on its own). Set * BOS_MAP_ALLOW_UNRESOLVED=1 for offline work (the map is still written, * with the unresolved sources). */ var RuntimeMapError = class extends effect.Data.TaggedError("RuntimeMapError") { toString() { return this.message; } }; const envBool = (name) => process.env[name] === "1"; const isoNow = (ms) => new Date(ms).toISOString(); const CORE_UI_KEY = "ui"; const FETCH_TIMEOUT_MS = Number(process.env.BOS_MAP_FETCH_TIMEOUT_MS ?? 15e3); function joinPath(a, b) { const left = a && a !== "/" ? a.replace(/\/+$/, "") : ""; const right = b && b !== "/" ? (b ?? "").replace(/^\/+/, "") : ""; if (!left) return right ? `/${right}` : "/"; if (!right) return left; return `${left}/${right}`; } function isHttp(value) { return typeof value === "string" && /^https?:\/\//.test(value); } function manifestUrlFor(base) { return new URL("manifest.gen.json", base.endsWith("/") ? base : `${base}/`).toString(); } function describeSlot(_root, key, kind, slot, unresolved) { if (!slot) return null; const pin = slot.pin?.manifest; if (slot.localPath) { const manifestPath = (0, node_path.join)(slot.localPath, "src/manifest.gen.json"); if ((0, node_fs.existsSync)(manifestPath)) return { key, kind, source: "local", location: slot.localPath, manifestPath, pin }; if (isHttp(slot.url)) return { key, kind, source: "remote", location: slot.url, manifestUrl: manifestUrlFor(slot.url), pin, note: `local path "${slot.localPath}" is absent; using the published bundle` }; unresolved.push({ key, kind, reason: `local path "${slot.localPath}" is absent and there is no production target` }); return null; } if (isHttp(slot.url)) return { key, kind, source: "remote", location: slot.url, manifestUrl: manifestUrlFor(slot.url), pin }; unresolved.push({ key, kind, reason: "no local workspace or remote target" }); return null; } function validateManifest(source, manifest) { if (!manifest || typeof manifest !== "object" || !Array.isArray(manifest.routes)) throw new Error(`source "${source.key}": manifest is not a route manifest`); if (manifest.manifestVersion !== every_plugin_ui_manifest.MANIFEST_VERSION) throw new Error(`source "${source.key}": manifest version ${manifest.manifestVersion} (expected ${every_plugin_ui_manifest.MANIFEST_VERSION})`); } /** * Static model of every-plugin's `constructTree` precedence: core ui first, * then sources by key; core wins route/mount claims, peer collisions throw. */ function composeRuntimeMap(sources) { const ordered = [...sources.filter((source) => source.key === CORE_UI_KEY), ...sources.filter((source) => source.key !== CORE_UI_KEY).sort((a, b) => a.key.localeCompare(b.key))]; const mounts = /* @__PURE__ */ new Map(); const mountByLayoutId = /* @__PURE__ */ new Map(); const shadowed = []; for (const source of ordered) for (const record of source.manifest.routes) { if (!record.mount) continue; const existing = mounts.get(record.mount); if (existing) { if (existing.key === CORE_UI_KEY && source.key !== CORE_UI_KEY) { shadowed.push({ id: record.id, owner: source.key, kind: "mount", reason: `mount "${record.mount}" is declared by core ui` }); mountByLayoutId.set(record.id, record.mount); continue; } throw new Error(`mount "${record.mount}" declared by multiple sources: "${existing.key}" and "${source.key}"`); } mounts.set(record.mount, { key: source.key, recordId: record.id }); mountByLayoutId.set(record.id, record.mount); } const routes = []; const claims = /* @__PURE__ */ new Map(); for (const source of ordered) { const pending = source.manifest.routes.filter((record) => !record.mount); const byId = /* @__PURE__ */ new Map(); const shadowedIds = /* @__PURE__ */ new Set(); let progressed = true; while (pending.length > 0 && progressed) { progressed = false; for (let index = 0; index < pending.length; index += 1) { const record = pending[index]; let parent; if (record.parentId) { if (shadowedIds.has(record.parentId)) { shadowedIds.add(record.id); shadowed.push({ id: record.id, owner: source.key, kind: "route", reason: `parent "${record.parentId}" is shadowed` }); pending.splice(index, 1); index -= 1; progressed = true; continue; } const local = byId.get(record.parentId); if (local) parent = { fullPath: local.fullPath, mount: local.mount, key: `${source.key}:${record.parentId}` }; else if (mountByLayoutId.has(record.parentId)) { const mount = mountByLayoutId.get(record.parentId); parent = { fullPath: "", mount, key: `mount:${mount}` }; } else continue; } else { const firstSegment = record.id.split("/")[0] ?? ""; if (!firstSegment.startsWith("_")) throw new Error(`route "${record.id}" in "${source.key}" must live under a mount`); const mount = (0, every_plugin_ui_manifest.resolveMountSegment)(firstSegment.slice(1)); if (!mount) throw new Error(`unknown mount "_${firstSegment.slice(1)}" (route "${record.id}" in "${source.key}")`); if (!mounts.has(mount)) throw new Error(`mount "${mount}" is not declared by any source (route "${record.id}" in "${source.key}")`); parent = { fullPath: "", mount, key: `mount:${mount}` }; } pending.splice(index, 1); index -= 1; progressed = true; if (!(record.type === "layout" || record.path === void 0)) { const siblings = claims.get(parent.key) ?? /* @__PURE__ */ new Map(); const claimant = siblings.get(record.path); if (claimant) { if (source.key !== CORE_UI_KEY && claimant.startsWith(`${CORE_UI_KEY}:`)) { shadowedIds.add(record.id); shadowed.push({ id: record.id, owner: source.key, kind: "route", reason: `path "${record.path}" is claimed by "${claimant}"` }); continue; } throw new Error(`path collision: "${source.key}" route "${record.id}" declares "${record.path}" already claimed by "${claimant}"`); } siblings.set(record.path, `${source.key}:${record.id}`); claims.set(parent.key, siblings); } const fullPath = joinPath(parent.fullPath, record.path); byId.set(record.id, { fullPath, mount: parent.mount }); routes.push({ id: record.id, owner: source.key, mount: parent.mount, type: record.type, path: record.path ?? null, fullPath, core: source.key === CORE_UI_KEY, nav: null }); } } if (pending.length > 0) throw new Error(`unresolvable parentage in "${source.key}": ${pending.map((record) => record.id).join(", ")}`); } return { mounts: [...mounts.entries()].map(([id, value]) => ({ id, declaredBy: value.key, recordId: value.recordId })), routes, shadowed }; } async function fetchRemoteManifest(url) { const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`); return await response.json(); } async function fetchPluginManifestAsset(url) { const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`); const asset = await response.json(); if (!asset || typeof asset !== "object" || asset.kind !== "every-plugin/manifest") throw new Error("not a plugin manifest"); return asset; } function pluginManifestUrlFor(base) { return new URL("plugin.manifest.json", base.endsWith("/") ? base : `${base}/`).toString(); } /** The backend inventory (#594 review): host, api, the auth server, and every * plugin backend — the "who serves this runtime, from where" analog of the ui * sources list. Local backends are their own truth (workspace path); remote * backends fetch their published `plugin.manifest.json` for the version + * contract hash drift signal. The host is source-level only (it ships no * manifest asset — it is not an every-plugin module). */ function describeBackend(root, key, kind, slot) { if (!slot) return null; const name = typeof slot.name === "string" ? slot.name : null; const pin = slot.pin?.manifest; const dependsOn = slot.dependsOn; if (slot.localPath) return { key, kind, name, source: "local", location: slot.localPath.startsWith(root) ? slot.localPath.slice(root.length + 1) : slot.localPath, ...pin ? { pin } : {}, ...slot.integrity ? { integrity: slot.integrity } : {}, ...dependsOn ? { dependsOn } : {} }; const remote = slot.remoteUrl ?? slot.url; if (isHttp(remote)) return { key, kind, name, source: "remote", location: remote, ...kind !== "host" ? { pluginManifestUrl: pluginManifestUrlFor(remote) } : {}, ...pin ? { pin } : {}, ...slot.integrity ? { integrity: slot.integrity } : {}, ...dependsOn ? { dependsOn } : {} }; return null; } /** The TS emission requires a local core ui workspace (`ui/src/lib/`); a * remote-only core ui skips it with a note. */ function renderTypeScript(map) { const remoteRoutes = map.routes.filter((route) => !route.core).map((route) => ({ id: route.id, path: route.fullPath, mount: route.mount, owner: route.owner, type: route.type })); const json = (value) => JSON.stringify(value, null, 2); return `// Generated by bos types gen — do not edit. // Regenerate with \`bos types gen\` (or \`pnpm run types:gen\`). A static model of the // composed runtime: core ui wins route/mount claims; plugin routes are listed // below. Nav labels are runtime-only (route options, not manifests). // See docs/agents/runtime-map.md for the investigation workflows. export const RUNTIME_MAP_GENERATED_AT = ${JSON.stringify(map.generatedAt)}; export const RUNTIME_MAP_ENV = ${JSON.stringify(map.env)}; export const RUNTIME_PLATFORM = ${JSON.stringify(map.platform)}; export const RUNTIME_SOURCES = ${json(map.sources)} as const; /** The serving side: host, api, the auth server, and every plugin backend — * source-level inventory; remote entries carry the composed plugin version * and contract hash. */ export const RUNTIME_BACKENDS = ${json(map.backends)} as const; export type RuntimeBackend = (typeof RUNTIME_BACKENDS)[number]; /** Routes owned by a plugin/remote ui source (not the core). */ export const REMOTE_ROUTES = ${json(remoteRoutes)} as const; export type RemoteRoute = (typeof REMOTE_ROUTES)[number]; export type RemotePath = RemoteRoute["path"]; export const RUNTIME_MOUNTS = ${json(map.mounts)} as const; export const SHADOWED_ROUTES = ${json(map.shadowed)} as const; export const UNRESOLVED_SOURCES = ${json(map.unresolved)} as const; export type UnresolvedSource = (typeof UNRESOLVED_SOURCES)[number]; `; } const generateRuntimeMapEffect = effect.Effect.fn("generateRuntimeMap")(function* (configDir, runtimeConfig, opts) { const allowUnresolved = envBool("BOS_MAP_ALLOW_UNRESOLVED"); const unresolved = []; const descriptors = [describeSlot(configDir, CORE_UI_KEY, "core", runtimeConfig.ui, unresolved), describeSlot(configDir, "auth", "auth-ui", runtimeConfig.auth?.ui, unresolved)]; for (const [key, plugin] of Object.entries(runtimeConfig.plugins ?? {})) { if (key === "auth" && runtimeConfig.auth !== void 0) continue; descriptors.push(describeSlot(configDir, key, "plugin-ui", plugin.ui, unresolved)); } const sources = []; for (const descriptor of descriptors) { if (!descriptor) continue; if (descriptor.source === "local" && descriptor.manifestPath) { const manifest = JSON.parse((0, node_fs.readFileSync)(descriptor.manifestPath, "utf8")); validateManifest(descriptor, manifest); sources.push({ ...descriptor, manifest }); } else if (descriptor.manifestUrl) yield* effect.Effect.tryPromise({ try: async () => { try { const manifest = await fetchRemoteManifest(descriptor.manifestUrl); validateManifest(descriptor, manifest); sources.push({ ...descriptor, manifest }); } catch (error) { unresolved.push({ key: descriptor.key, kind: descriptor.kind, reason: `fetch ${descriptor.manifestUrl} failed: ${error instanceof Error ? error.message : String(error)}` }); } }, catch: (cause) => new RuntimeMapError({ message: String(cause) }) }); } const backendSlots = [ describeBackend(configDir, "host", "host", runtimeConfig.host), describeBackend(configDir, "api", "api", runtimeConfig.api), describeBackend(configDir, "auth", "auth", runtimeConfig.auth) ]; for (const [key, plugin] of Object.entries(runtimeConfig.plugins ?? {})) { if (key === "auth" && runtimeConfig.auth !== void 0) continue; backendSlots.push(describeBackend(configDir, key, "plugin", plugin)); } const backends = yield* effect.Effect.tryPromise({ try: async () => { const collected = []; for (const backend of backendSlots) { if (!backend) continue; if (backend.pluginManifestUrl) try { const asset = await fetchPluginManifestAsset(backend.pluginManifestUrl); collected.push({ ...backend, pluginVersion: asset.plugin?.version, contractSha256: asset.contract?.types?.sha256 }); } catch (error) { const reason = `plugin manifest ${backend.pluginManifestUrl} failed: ${error instanceof Error ? error.message : String(error)}`; unresolved.push({ key: backend.key, kind: backend.kind, reason }); collected.push({ ...backend, note: reason }); } else collected.push(backend); } return collected; }, catch: (cause) => new RuntimeMapError({ message: String(cause) }) }); if (sources.length === 0 && unresolved.length === 0 && backends.length === 0) return null; if (unresolved.length > 0 && !allowUnresolved) { for (const entry of unresolved) yield* effect.Effect.logError(`[runtime-map] UNRESOLVED ${entry.key} (${entry.kind}): ${entry.reason}`); if (opts?.strict) return yield* new RuntimeMapError({ message: `runtime map is incomplete — ${unresolved.map((entry) => `${entry.key} (${entry.kind}): ${entry.reason}`).join("; ")} — fix the source or set BOS_MAP_ALLOW_UNRESOLVED=1 for offline work` }); } const composed = composeRuntimeMap(sources); const map = { generatedAt: isoNow(yield* effect.Clock.currentTimeMillis), env: opts?.env ?? "unknown", platform: opts?.platform ?? null, extends: opts?.extendsChain?.join(" → ") ?? null, sources: sources.map((source) => ({ key: source.key, kind: source.kind, source: source.source, location: source.source === "local" && source.location.startsWith(configDir) ? source.location.slice(configDir.length + 1) : source.location, manifestUrl: source.manifestUrl ?? null, pin: source.pin ?? null, note: source.note ?? null })), backends, unresolved, mounts: composed.mounts, routes: composed.routes, shadowed: composed.shadowed }; const jsonPath = (0, node_path.join)(configDir, ".bos/generated/runtime-map.json"); (0, node_fs.mkdirSync)((0, node_path.dirname)(jsonPath), { recursive: true }); (0, node_fs.writeFileSync)(jsonPath, `${JSON.stringify(map, null, 2)}\n`); let tsPath; const uiLocalPath = runtimeConfig.ui?.source === "local" ? runtimeConfig.ui.localPath : void 0; if (uiLocalPath && (0, node_fs.existsSync)((0, node_path.join)(uiLocalPath, "src"))) { tsPath = (0, node_path.join)(uiLocalPath, "src/lib/runtime-map.gen.ts"); (0, node_fs.mkdirSync)((0, node_path.dirname)(tsPath), { recursive: true }); (0, node_fs.writeFileSync)(tsPath, renderTypeScript(map)); } const remoteCount = map.routes.filter((route) => !route.core).length; const remoteBackends = backends.filter((backend) => backend.source === "remote").length; yield* effect.Effect.log(`[runtime-map] ${map.routes.length} routes (${remoteCount} remote) from ${sources.length} sources; ${backends.length} backends (${remoteBackends} remote)`); for (const entry of unresolved) yield* effect.Effect.logWarning(`[runtime-map] unresolved ${entry.key}: ${entry.reason}`); yield* effect.Effect.log(`[runtime-map] wrote ${jsonPath}${tsPath ? ` + ${tsPath}` : " (no local core ui — ts emission skipped)"}`); return { routeCount: map.routes.length, remoteCount, sourceCount: sources.length, backendCount: backends.length, unresolved, jsonPath, tsPath }; }); //#endregion exports.RuntimeMapError = RuntimeMapError; exports.composeRuntimeMap = composeRuntimeMap; exports.generateRuntimeMapEffect = generateRuntimeMapEffect; //# sourceMappingURL=runtime-map.cjs.map