everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
162 lines (160 loc) • 6.08 kB
JavaScript
import { createDefu } from "defu";
//#region src/services/resolution/merge.ts
const BOS_CONFIG_ORDER = [
"extends",
"account",
"domain",
"title",
"description",
"testnet",
"staging",
"repository",
"publish",
"platform",
"ci",
"app",
"plugins",
"rolledBackFrom"
];
const ARRAY_UNION_KEYS = /* @__PURE__ */ new Set(["secrets", "connectSrc"]);
function isPlainObject(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function unionArrays(a, b) {
const aArr = Array.isArray(a) ? a : [];
const bArr = Array.isArray(b) ? b : [];
if (aArr.length === 0 && bArr.length === 0) return void 0;
const seen = /* @__PURE__ */ new Set();
const result = [];
for (const item of [...aArr, ...bArr]) {
if (typeof item === "string") {
if (seen.has(item)) continue;
seen.add(item);
}
result.push(item);
}
return result;
}
function cleanNullSentinels(obj) {
const out = {};
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === void 0) continue;
if (isPlainObject(value)) {
const cleaned = cleanNullSentinels(value);
if (Object.keys(cleaned).length > 0) out[key] = cleaned;
} else out[key] = value;
}
return out;
}
const bosConfigMerger = createDefu((obj, key, value) => {
if (key === "pin") {
obj[key] = value;
return true;
}
if (obj[key] === null) return true;
if (value === null) {
obj[key] = null;
return true;
}
if (Array.isArray(obj[key]) && Array.isArray(value)) {
if (ARRAY_UNION_KEYS.has(key)) obj[key] = unionArrays(obj[key], value);
else obj[key] = value;
return true;
}
return false;
});
function resolveExtendsRef(extendsField, env) {
if (!extendsField) return void 0;
if (typeof extendsField === "string") return extendsField;
return extendsField[env] ?? extendsField.production ?? Object.values(extendsField).find(Boolean);
}
function mergeBosConfigWithExtends(parent, child) {
const merged = bosConfigMerger(child, parent);
if (child.plugins !== void 0 && isPlainObject(child.plugins)) merged.plugins = cleanNullSentinels(child.plugins);
const mergedRecord = merged;
if (isPlainObject(mergedRecord.app)) for (const entryVal of Object.values(mergedRecord.app)) {
if (!isPlainObject(entryVal)) continue;
for (const secretKey of ARRAY_UNION_KEYS) if (Array.isArray(entryVal[secretKey])) entryVal[secretKey] = unionArrays(entryVal[secretKey], [])?.filter(Boolean) ?? entryVal[secretKey];
}
if (isPlainObject(mergedRecord.plugins)) for (const pluginVal of Object.values(mergedRecord.plugins)) {
if (!isPlainObject(pluginVal)) continue;
for (const secretKey of ARRAY_UNION_KEYS) if (Array.isArray(pluginVal[secretKey])) pluginVal[secretKey] = unionArrays(pluginVal[secretKey], [])?.filter(Boolean) ?? pluginVal[secretKey];
}
return rebuildOrderedConfig(mergedRecord);
}
function mergeJsonValuesPreservingLocalOrder(local, template) {
if (isPlainObject(local) && isPlainObject(template)) {
const merged = {};
for (const key of Object.keys(local)) merged[key] = mergeJsonValuesPreservingLocalOrder(local[key], template[key]);
for (const key of Object.keys(template)) if (!(key in merged)) merged[key] = template[key];
return merged;
}
return local ?? template;
}
function rebuildOrderedConfig(config) {
const ordered = {};
for (const key of BOS_CONFIG_ORDER) if (key in config) ordered[key] = config[key];
for (const key of Object.keys(config)) if (!BOS_CONFIG_ORDER.includes(key)) ordered[key] = config[key];
return ordered;
}
/**
* Deep child-wins merge used to overlay a freshly authored config on top of
* the currently published one — authored keys win, published-only pipeline
* state (bundle URLs, integrity) is preserved. Unlike the extends merge,
* plugin entries merge per-key so published URLs survive authored edits;
* removing a slot from the authored config therefore requires a deploy,
* which regenerates the payload from scratch.
*/
function mergeAuthoredOverPublished(published, authored) {
return rebuildOrderedConfig(mergeJsonValuesPreservingLocalOrder(authored, published));
}
const isLocalRef = (value) => typeof value === "string" && value.startsWith("local:");
/** A config entry (app slot or plugin attachment) with its workspace-relative
* `local:` development targets removed — sub-slot `ui` included. */
function stripLocalTargetsFromEntry(entry) {
const next = { ...entry };
if (isLocalRef(next.development)) delete next.development;
const ui = next.ui;
if (ui && typeof ui === "object" && isLocalRef(ui.development)) {
const strippedUi = { ...ui };
delete strippedUi.development;
next.ui = strippedUi;
}
return next;
}
/**
* Publish canonicalization (#584): a `local:` development target is
* workspace-relative and meaningful only in the repo that declares the
* workspace — a child inheriting one cannot spawn it. The authoring surface
* (the parent's own `bos.app.ts`) keeps them for the parent's own dev; the
* published payload carries only consumable targets, so `extends` never
* hands a child a `local:` ref. The runtime's production fallback
* (`resolveDevelopmentTarget`) resolves the slot from its published URL.
*/
function stripLocalDevelopmentTargets(config) {
const out = { ...config };
const app = out.app;
if (app && typeof app === "object") {
const nextApp = { ...app };
for (const key of [
"host",
"ui",
"api",
"auth"
]) {
const entry = app[key];
if (entry && typeof entry === "object") nextApp[key] = stripLocalTargetsFromEntry(entry);
}
out.app = nextApp;
}
const plugins = out.plugins;
if (plugins && typeof plugins === "object") {
const nextPlugins = { ...plugins };
for (const [id, raw] of Object.entries(plugins)) if (raw && typeof raw === "object" && !Array.isArray(raw)) nextPlugins[id] = stripLocalTargetsFromEntry(raw);
out.plugins = nextPlugins;
}
return out;
}
//#endregion
export { BOS_CONFIG_ORDER, bosConfigMerger, isPlainObject, mergeAuthoredOverPublished, mergeBosConfigWithExtends, rebuildOrderedConfig, resolveExtendsRef, stripLocalDevelopmentTargets };
//# sourceMappingURL=merge.mjs.map