everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
456 lines (448 loc) • 17.6 kB
JavaScript
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { Clock, Data, Effect } from "effect";
import { MANIFEST_VERSION, resolveMountSegment } from "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 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 = join(slot.localPath, "src/manifest.gen.json");
if (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 !== MANIFEST_VERSION) throw new Error(`source "${source.key}": manifest version ${manifest.manifestVersion} (expected ${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 = 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.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(readFileSync(descriptor.manifestPath, "utf8"));
validateManifest(descriptor, manifest);
sources.push({
...descriptor,
manifest
});
} else if (descriptor.manifestUrl) yield* 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.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.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* 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 = join(configDir, ".bos/generated/runtime-map.json");
mkdirSync(dirname(jsonPath), { recursive: true });
writeFileSync(jsonPath, `${JSON.stringify(map, null, 2)}\n`);
let tsPath;
const uiLocalPath = runtimeConfig.ui?.source === "local" ? runtimeConfig.ui.localPath : void 0;
if (uiLocalPath && existsSync(join(uiLocalPath, "src"))) {
tsPath = join(uiLocalPath, "src/lib/runtime-map.gen.ts");
mkdirSync(dirname(tsPath), { recursive: true });
writeFileSync(tsPath, renderTypeScript(map));
}
const remoteCount = map.routes.filter((route) => !route.core).length;
const remoteBackends = backends.filter((backend) => backend.source === "remote").length;
yield* 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.logWarning(`[runtime-map] unresolved ${entry.key}: ${entry.reason}`);
yield* 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
export { RuntimeMapError, composeRuntimeMap, generateRuntimeMapEffect };
//# sourceMappingURL=runtime-map.mjs.map