UNPKG

everything-dev

Version:

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

1 lines • 12.6 kB
{"version":3,"file":"merge.mjs","names":[],"sources":["../../../src/services/resolution/merge.ts"],"sourcesContent":["import { createDefu } from \"defu\";\nimport type { BosConfigInput, ExtendsConfig } from \"../../types\";\n\nexport const BOS_CONFIG_ORDER = [\n \"extends\",\n \"account\",\n \"domain\",\n \"title\",\n \"description\",\n \"testnet\",\n \"staging\",\n \"repository\",\n \"publish\",\n \"platform\",\n \"ci\",\n \"app\",\n \"plugins\",\n \"rolledBackFrom\",\n] as const;\n\nexport type BosConfigFieldName = (typeof BOS_CONFIG_ORDER)[number];\n\nexport type BosEnv = \"development\" | \"production\" | \"staging\";\n\nexport interface ResolvedConfigMeta {\n env: BosEnv;\n resolvedAt: string;\n extendsChain: string[];\n source?: string;\n}\n\nconst ARRAY_UNION_KEYS = new Set([\"secrets\", \"connectSrc\"]);\n\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction unionArrays(a: unknown, b: unknown): unknown[] | undefined {\n const aArr = Array.isArray(a) ? a : [];\n const bArr = Array.isArray(b) ? b : [];\n if (aArr.length === 0 && bArr.length === 0) return undefined;\n const seen = new Set<string>();\n const result: unknown[] = [];\n for (const item of [...aArr, ...bArr]) {\n if (typeof item === \"string\") {\n if (seen.has(item)) continue;\n seen.add(item);\n }\n result.push(item);\n }\n return result;\n}\n\nfunction cleanNullSentinels(obj: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (value === null || value === undefined) continue;\n if (isPlainObject(value)) {\n const cleaned = cleanNullSentinels(value);\n if (Object.keys(cleaned).length > 0) {\n out[key] = cleaned;\n }\n } else {\n out[key] = value;\n }\n }\n return out;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst bosConfigMerger = createDefu((obj: any, key: any, value: any): boolean | undefined => {\n if (key === \"pin\") {\n // the pin is an atomic unit — a child pin replaces the parent's whole\n // (per-key defu merging would half-mix manifest/integrity across runtimes)\n obj[key] = value;\n return true;\n }\n if (obj[key] === null) return true;\n if (value === null) {\n obj[key] = null;\n return true;\n }\n if (Array.isArray(obj[key]) && Array.isArray(value)) {\n if (ARRAY_UNION_KEYS.has(key)) {\n obj[key] = unionArrays(obj[key], value);\n } else {\n obj[key] = value;\n }\n return true;\n }\n return false;\n});\n\nexport function resolveExtendsRef(\n extendsField: string | ExtendsConfig | undefined,\n env: BosEnv,\n): string | undefined {\n if (!extendsField) return undefined;\n if (typeof extendsField === \"string\") return extendsField;\n return extendsField[env] ?? extendsField.production ?? Object.values(extendsField).find(Boolean);\n}\n\nexport function mergeBosConfigWithExtends(\n parent: BosConfigInput,\n child: BosConfigInput,\n): BosConfigInput {\n const merged = bosConfigMerger(child, parent) as BosConfigInput;\n\n if (child.plugins !== undefined && isPlainObject(child.plugins)) {\n (merged as Record<string, unknown>).plugins = cleanNullSentinels(\n child.plugins as Record<string, unknown>,\n );\n }\n\n const mergedRecord = merged as Record<string, unknown>;\n\n if (isPlainObject(mergedRecord.app)) {\n for (const entryVal of Object.values(mergedRecord.app as Record<string, unknown>)) {\n if (!isPlainObject(entryVal)) continue;\n for (const secretKey of ARRAY_UNION_KEYS) {\n if (Array.isArray(entryVal[secretKey])) {\n entryVal[secretKey] =\n (unionArrays(entryVal[secretKey] as unknown[], []) as string[] | undefined)?.filter(\n Boolean,\n ) ?? entryVal[secretKey];\n }\n }\n }\n }\n\n if (isPlainObject(mergedRecord.plugins)) {\n for (const pluginVal of Object.values(mergedRecord.plugins as Record<string, unknown>)) {\n if (!isPlainObject(pluginVal)) continue;\n for (const secretKey of ARRAY_UNION_KEYS) {\n if (Array.isArray(pluginVal[secretKey])) {\n pluginVal[secretKey] =\n (unionArrays(pluginVal[secretKey] as unknown[], []) as string[] | undefined)?.filter(\n Boolean,\n ) ?? pluginVal[secretKey];\n }\n }\n }\n }\n\n return rebuildOrderedConfig(mergedRecord) as BosConfigInput;\n}\n\nexport function mergeBosConfigWithTemplate(\n local: BosConfigInput,\n template: BosConfigInput,\n): BosConfigInput {\n const merged = mergeJsonValuesPreservingLocalOrder(local, template) as BosConfigInput;\n return rebuildOrderedConfig(merged as Record<string, unknown>) as BosConfigInput;\n}\n\nfunction mergeJsonValuesPreservingLocalOrder(local: unknown, template: unknown): unknown {\n if (isPlainObject(local) && isPlainObject(template)) {\n const merged: Record<string, unknown> = {};\n for (const key of Object.keys(local)) {\n merged[key] = mergeJsonValuesPreservingLocalOrder(\n local[key],\n (template as Record<string, unknown>)[key],\n );\n }\n for (const key of Object.keys(template as Record<string, unknown>)) {\n if (!(key in merged)) {\n merged[key] = (template as Record<string, unknown>)[key];\n }\n }\n return merged;\n }\n return local ?? template;\n}\n\nexport function rebuildOrderedConfig<T extends Record<string, unknown>>(config: T): T {\n const ordered: Record<string, unknown> = {};\n\n for (const key of BOS_CONFIG_ORDER) {\n if (key in config) {\n ordered[key] = config[key];\n }\n }\n\n for (const key of Object.keys(config)) {\n if (!BOS_CONFIG_ORDER.includes(key as BosConfigFieldName)) {\n ordered[key] = config[key];\n }\n }\n\n return ordered as T;\n}\n\n/**\n * Deep child-wins merge used to overlay a freshly authored config on top of\n * the currently published one — authored keys win, published-only pipeline\n * state (bundle URLs, integrity) is preserved. Unlike the extends merge,\n * plugin entries merge per-key so published URLs survive authored edits;\n * removing a slot from the authored config therefore requires a deploy,\n * which regenerates the payload from scratch.\n */\nexport function mergeAuthoredOverPublished(\n published: BosConfigInput,\n authored: BosConfigInput,\n): BosConfigInput {\n const merged = mergeJsonValuesPreservingLocalOrder(authored, published) as Record<\n string,\n unknown\n >;\n return rebuildOrderedConfig(merged) as BosConfigInput;\n}\n\nexport { bosConfigMerger };\n\nconst isLocalRef = (value: unknown): boolean =>\n typeof value === \"string\" && value.startsWith(\"local:\");\n\n/** A config entry (app slot or plugin attachment) with its workspace-relative\n * `local:` development targets removed — sub-slot `ui` included. */\nfunction stripLocalTargetsFromEntry(entry: Record<string, unknown>): Record<string, unknown> {\n const next = { ...entry };\n if (isLocalRef(next.development)) delete next.development;\n const ui = next.ui as Record<string, unknown> | undefined;\n if (ui && typeof ui === \"object\" && isLocalRef(ui.development)) {\n const strippedUi = { ...ui };\n delete strippedUi.development;\n next.ui = strippedUi;\n }\n return next;\n}\n\n/**\n * Publish canonicalization (#584): a `local:` development target is\n * workspace-relative and meaningful only in the repo that declares the\n * workspace — a child inheriting one cannot spawn it. The authoring surface\n * (the parent's own `bos.app.ts`) keeps them for the parent's own dev; the\n * published payload carries only consumable targets, so `extends` never\n * hands a child a `local:` ref. The runtime's production fallback\n * (`resolveDevelopmentTarget`) resolves the slot from its published URL.\n */\nexport function stripLocalDevelopmentTargets<T extends object>(config: T): T {\n const out = { ...config } as Record<string, unknown>;\n const app = out.app as Record<string, Record<string, unknown>> | undefined;\n if (app && typeof app === \"object\") {\n const nextApp: Record<string, unknown> = { ...app };\n for (const key of [\"host\", \"ui\", \"api\", \"auth\"] as const) {\n const entry = app[key];\n if (entry && typeof entry === \"object\") nextApp[key] = stripLocalTargetsFromEntry(entry);\n }\n out.app = nextApp;\n }\n const plugins = out.plugins as Record<string, unknown> | undefined;\n if (plugins && typeof plugins === \"object\") {\n const nextPlugins: Record<string, unknown> = { ...plugins };\n for (const [id, raw] of Object.entries(plugins)) {\n if (raw && typeof raw === \"object\" && !Array.isArray(raw)) {\n nextPlugins[id] = stripLocalTargetsFromEntry(raw as Record<string, unknown>);\n }\n }\n out.plugins = nextPlugins;\n }\n return out as T;\n}\n"],"mappings":"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"}