everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
252 lines (250 loc) • 8.77 kB
JavaScript
import { mergeBosConfigWithExtends } from "../merge.mjs";
import { AppDescriptorSchema } from "./schema.mjs";
//#region src/descriptor/resolve.ts
function isPlainObject(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
const devRef = (ref) => ref.path ? `local:${ref.path}` : ref.development;
function pick(source, keys) {
const out = {};
for (const key of keys) if (source[key] !== void 0) out[key] = source[key];
return out;
}
const ATTACHMENT_FIELDS = [
"name",
"proxy",
"variables",
"secrets",
"routes",
"shared",
"connectSrc",
"dependsOn",
"version"
];
/** Fields the attachment slots carry beyond the shared attachment set. */
const SLOT_INPUT_FIELDS = [
"development",
"extends",
"integrity"
];
/** Fields a config-entry may carry that map onto an attachment's slot-free set. */
const ENTRY_ONLY_FIELDS = ["name", "extends"];
const LOCAL_PREFIX = "local:";
function stripLocal(development) {
return development?.startsWith(LOCAL_PREFIX) ? development.slice(6) : void 0;
}
function attachmentToEntry(attachment) {
const development = devRef(attachment);
return {
...attachment.extends ? { extends: attachment.extends } : {},
...development ? { development } : {},
...pick(attachment, [...ATTACHMENT_FIELDS]),
...attachment.ui ? { ui: {
...attachment.ui.name ? { name: attachment.ui.name } : {},
...attachment.ui.path ? { development: `local:${attachment.ui.path}` } : attachment.ui.development ? { development: attachment.ui.development } : {},
...attachment.ui.integrity ? { integrity: attachment.ui.integrity } : {},
...attachment.ui.pin ? { pin: attachment.ui.pin } : {}
} } : {}
};
}
/** Descriptor → authoring-shape `BosConfigInput` (local refs only, no pipeline state). */
function toConfigInput(descriptor) {
const input = { ...pick(descriptor, [
"account",
"domain",
"title",
"description",
"repository",
"testnet",
"staging",
"ci",
"cdn",
"publish"
]) };
const app = {};
if (descriptor.host) {
const development = devRef(descriptor.host);
if (development) app.host = {
development,
...pick(descriptor.host, ["secrets"])
};
}
if (descriptor.api) {
const development = devRef(descriptor.api);
if (development) app.api = {
development,
...pick(descriptor.api, [
"proxy",
"variables",
"secrets",
"routes",
"shared",
"dependsOn"
])
};
}
if (descriptor.ui) {
const development = devRef(descriptor.ui);
if (development) app.ui = { development };
}
if (descriptor.auth) app.auth = attachmentToEntry(descriptor.auth);
if (Object.keys(app).length > 0) input.app = app;
if (descriptor.plugins) {
const plugins = {};
for (const [key, attachment] of Object.entries(descriptor.plugins)) {
const entry = attachmentToEntry(attachment);
if (entry.name === key) delete entry.name;
plugins[key] = entry;
}
input.plugins = plugins;
}
return input;
}
function applyPipeline(target, fields) {
for (const [key, value] of Object.entries(fields)) if (value !== void 0) target[key] = value;
}
function applyDeployMap(config, entry) {
if (!entry) return;
if (entry.app && isPlainObject(config.app)) for (const [slot, fields] of Object.entries(entry.app)) {
const target = config.app[slot];
if (fields && isPlainObject(target)) applyPipeline(target, fields);
}
if (entry.plugins && isPlainObject(config.plugins)) for (const [key, fields] of Object.entries(entry.plugins)) {
const target = config.plugins[key];
if (fields && isPlainObject(target)) applyPipeline(target, fields);
}
}
/**
* Flatten one app against the registry: `extends` inherits the base App's
* composition, the child's entries win (depth-1 — chains are deferred,
* wayfinder ticket 08). The consumed `extends` key is dropped from the
* output; the deploy map injects pipeline state.
*
* `extends` accepts a registry name or an imported App descriptor value —
* an imported parent is an inlined parent: same materialization, no fetch.
*/
function resolveApp(name, registry, deployMap = {}, seen = /* @__PURE__ */ new Set()) {
if (seen.has(name)) throw new Error(`Circular extends detected: ${name}`);
const raw = registry[name];
if (!raw) throw new Error(`App "${name}" not found in registry`);
const descriptor = AppDescriptorSchema.parse(raw);
let parentInput;
if (descriptor.extends) {
if (typeof descriptor.extends === "string") {
const parentRaw = registry[descriptor.extends];
if (!parentRaw) throw new Error(`extends target "${descriptor.extends}" not found in registry`);
parentInput = flattenParent(parentRaw);
} else parentInput = flattenParent(descriptor.extends);
}
const childInput = toConfigInput(descriptor);
const resolved = { ...parentInput ? mergeBosConfigWithExtends(parentInput, childInput) : childInput };
delete resolved.extends;
applyDeployMap(resolved, deployMap[name]);
return resolved;
}
function flattenParent(parentRaw) {
const parentDescriptor = AppDescriptorSchema.parse(parentRaw);
if (parentDescriptor.extends) {
const parentName = typeof parentDescriptor.extends === "string" ? parentDescriptor.extends : parentDescriptor.extends.name;
throw new Error(`extends chains deeper than one level are not supported yet ("${parentDescriptor.name}" extends "${parentName ?? "?"}") — wayfinder ticket 08`);
}
return toConfigInput(parentDescriptor);
}
/**
* Dev-time overlay — merged child-wins over a resolved config by the dev
* runner (ports, local-vs-remote sources, seeding). Overlays are never
* published: they exist only in `bos.dev.ts` and apply when the environment
* is development.
*/
function applyDevOverlay(resolved, overlay) {
return mergeBosConfigWithExtends(resolved, toConfigInput(overlay));
}
/** Resolve every app in the registry. */
function resolveApps(registry, deployMap = {}) {
return Object.fromEntries(Object.keys(registry).map((name) => [name, resolveApp(name, registry, deployMap)]));
}
/**
* The inverse of `toConfigInput` for the scaffold surface: an authoring-shape
* config (as personalizeConfig computes it) back into the authored
* descriptor form a child's `bos.app.ts` embeds. Driven by the same field
* maps as `toConfigInput` so the two directions cannot drift; pipeline state
* (production URLs, integrity) is dropped — it is deploy-map territory,
* never authored.
*/
function configInputToDescriptor(input) {
const source = input;
const descriptor = { name: input.domain ?? input.account ?? "child.app" };
for (const field of [
"extends",
"account",
"domain",
"title",
"description",
"repository",
"testnet",
"staging",
"starter",
"ci",
"cdn",
"publish"
]) if (source[field] !== void 0) descriptor[field] = source[field];
const app = input.app ?? {};
const slot = (key) => {
const entry = app[key];
if (!entry?.development) return void 0;
const out = { path: stripLocal(entry.development) };
for (const field of ATTACHMENT_FIELDS) {
if (field === "name") continue;
if (entry[field] !== void 0) out[field] = entry[field];
}
return out;
};
for (const key of [
"host",
"ui",
"api"
]) {
const value = slot(key);
if (value) descriptor[key] = value;
}
const auth = app.auth;
if (auth?.development) descriptor.auth = entryToAttachment("auth", auth);
const plugins = {};
for (const [key, raw] of Object.entries(input.plugins ?? {})) if (typeof raw === "string") plugins[key] = {
name: key,
extends: raw
};
else if (raw && typeof raw === "object") plugins[key] = entryToAttachment(key, raw);
if (Object.keys(plugins).length > 0) descriptor.plugins = plugins;
return AppDescriptorSchema.parse(descriptor);
}
/** Config-entry → authored attachment: name/extends + the shared field map. */
function entryToAttachment(key, entry) {
const out = {};
for (const field of ENTRY_ONLY_FIELDS) {
const value = entry[field];
if (value !== void 0) out[field] = field === "name" ? value : value;
else if (field === "name") out.name = key;
}
const development = entry.development;
const path = stripLocal(development);
if (path !== void 0) out.path = path;
for (const field of ATTACHMENT_FIELDS) {
if (field === "name") continue;
if (entry[field] !== void 0) out[field] = entry[field];
}
const ui = entry.ui;
if (ui && typeof ui === "object") {
const uiPath = stripLocal(ui.development);
out.ui = {
...typeof ui.name === "string" ? { name: ui.name } : {},
...uiPath !== void 0 ? { path: uiPath } : {},
...ui.integrity !== void 0 ? { integrity: ui.integrity } : {},
...ui.pin !== void 0 ? { pin: ui.pin } : {}
};
}
return out;
}
//#endregion
export { ATTACHMENT_FIELDS, SLOT_INPUT_FIELDS, applyDevOverlay, configInputToDescriptor, resolveApp, resolveApps, toConfigInput };
//# sourceMappingURL=resolve.mjs.map