UNPKG

everything-dev

Version:

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

868 lines (866 loc) • 39.9 kB
import { getProcessEnv } from "./env/process-env.mjs"; import { fetchJsonOrNull } from "./http-client.mjs"; import { fetchApiPluginManifest } from "./api-contract.mjs"; import { manifestPluginsToNodes } from "./dag.mjs"; import { BOS_CONFIG_ORDER, bosConfigMerger, isPlainObject, mergeBosConfigWithExtends, rebuildOrderedConfig, resolveExtendsRef } from "./merge.mjs"; import { BosConfigSchema } from "./types.mjs"; import { AppDescriptorSchema } from "./descriptor/schema.mjs"; import { resolveApp, toConfigInput } from "./descriptor/resolve.mjs"; import { fetchBosConfigFromFastKv } from "./fastkv.mjs"; import { getNetworkIdForAccount } from "./network.mjs"; import { resolveSlotVersion } from "./version-manifest-resolve.mjs"; import { AsyncLocalStorage } from "node:async_hooks"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, isAbsolute, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { Effect, Schema } from "effect"; import { sanitizeContainerName } from "every-plugin/ui/manifest/contract"; //#region src/config.ts const warnOutsideEffect = (...args) => { console.warn(...args); }; const LOCAL_PREFIX = "local:"; const DEFAULT_HOST_PORT = 3e3; const RESOLVED_CONFIG_FILENAME = "bos.resolved-config.json"; const MISSING_CONFIG_MESSAGE = "No config found (bos.app.ts or bos.config.json)"; const DEV_OVERLAY_FILENAME = "bos.dev.ts"; const warningCapture = new AsyncLocalStorage(); /** * @internal — runs `fn` with config warnings captured into `sink` instead of * the console. Plumbing for the resolution session; not part of the public * config surface. */ function runWithConfigWarningSink(sink, fn) { return warningCapture.run({ sink, previous: warningCapture.getStore() }, fn); } function emitConfigWarning(message) { const capture = warningCapture.getStore(); if (capture) capture.sink.push(message); else console.warn(message); } const configPathCache = /* @__PURE__ */ new Map(); /** * @internal — clears the findConfigPath memo. Plumbing for tests and the * resolution session; not part of the public config surface. */ function resetConfigPathCache() { configPathCache.clear(); } /** * Reads the local authored config without resolving the extends chain or * touching the network — the build-surface factories (generated rsbuild * configs) consume it for their APP_NAME/APP_ACCOUNT defines. */ async function readAuthoredConfigInput(cwd) { const configPath = findConfigPath(cwd); if (!configPath || configPath.startsWith("bos://")) return null; return readConfigInput(configPath, dirname(configPath)); } /** * The authored config entry inside exactly `dir` — no upward walk. Unlike * `findConfigPath` this never escapes the given directory, for write targets * and nested-local-path resolution where a parent's config must not be * picked up. Prefers the authored `bos.app.ts` descriptor (the canonical * form) over a legacy `bos.config.json`. */ function localConfigEntryPath(dir) { const appPath = join(dir, "bos.app.ts"); if (existsSync(appPath)) return appPath; const jsonPath = join(dir, "bos.config.json"); return existsSync(jsonPath) ? jsonPath : null; } /** `localConfigEntryPath` + the form-aware read (`BosConfigInput`). */ async function readLocalAuthoredConfigInput(dir) { const entry = localConfigEntryPath(dir); if (!entry) return null; return readConfigInput(entry, dirname(entry)); } /** * The authored `bos.app.ts` descriptor is the canonical form — when both * forms coexist (e.g. a generated `bos.config.json` write-back beside the * authored descriptor), the authored file wins. JSON-form-only children are * unaffected: with no `bos.app.ts`, the JSON file is the entry. */ function findConfigPath(cwd) { const cacheKey = resolve(cwd ?? process.cwd()); const cached = configPathCache.get(cacheKey); if (cached !== void 0) return cached; let dir = cacheKey; while (true) { const appPath = join(dir, "bos.app.ts"); if (existsSync(appPath)) { configPathCache.set(cacheKey, appPath); return appPath; } const jsonPath = join(dir, "bos.config.json"); if (existsSync(jsonPath)) { configPathCache.set(cacheKey, jsonPath); return jsonPath; } const parent = dirname(dir); if (parent === dir) break; dir = parent; } configPathCache.set(cacheKey, null); return null; } function isAppDescriptorPath(configPath) { return configPath.endsWith(".app.ts") || configPath.endsWith(".app.js"); } /** * Materialize an authored bos.app.ts descriptor into the authoring-shape * `BosConfigInput` the config pipeline consumes. Import-extends is honored * two ways: the parent descriptor value can be imported directly (an * inlined parent, resolved through `resolveApp`), or `extends` can name a * published config (`bos://…`) / a local file, which flows into the JSON * extends chain unchanged. */ async function loadAppDescriptorConfig(resolvedPath) { return parseAppDescriptorModule(await import( /* webpackIgnore: true */ pathToFileURL(resolvedPath).href ), resolvedPath); } /** * @internal — parses an already-imported app descriptor module into the * authoring-shape `BosConfigInput`. Plumbing for the resolution session's * injectable module loader; not part of the public config surface. */ function parseAppDescriptorModule(mod, sourcePath) { if (!mod.default) throw new Error(`${sourcePath ?? "App descriptor module"} must default-export an App() descriptor`); const descriptor = AppDescriptorSchema.parse(mod.default); const registry = {}; for (const value of Object.values(mod)) { const parsed = AppDescriptorSchema.safeParse(value); if (parsed.success) registry[parsed.data.name] = parsed.data; } const extendsRef = descriptor.extends; if (typeof extendsRef === "string" && registry[extendsRef] !== void 0 || extendsRef !== void 0 && typeof extendsRef !== "string") return resolveApp(descriptor.name, registry); const input = toConfigInput(descriptor); if (extendsRef !== void 0) input.extends = extendsRef; return input; } /** * @internal — validates an already-imported `bos.dev.ts` module as a * `Partial<AppDescriptor>` dev overlay. Plumbing for the resolution session's * injectable module loader; not part of the public config surface. */ function parseDevOverlayModule(mod, sourcePath) { const source = sourcePath ?? "bos.dev.ts"; if (!mod?.default) throw new DevOverlayError({ path: source, message: `${source} must default-export a dev overlay (Partial<AppDescriptor>)` }); const parsed = AppDescriptorSchema.partial().safeParse(mod.default); if (!parsed.success) throw new DevOverlayError({ path: source, message: `${source} is not a valid dev overlay: ${parsed.error.message}`, cause: parsed.error }); return parsed.data; } var ConfigLoadError = class extends Schema.TaggedError()("ConfigLoadError", { path: Schema.String, message: Schema.String, cause: Schema.optional(Schema.Unknown) }) {}; var CircularExtendsError = class extends Schema.TaggedError()("CircularExtendsError", { chain: Schema.Array(Schema.String), message: Schema.String }) {}; var ConfigNotfoundError = class extends Schema.TaggedError()("ConfigNotfoundError", { message: Schema.String }) {}; var ConfigExtendsError = class extends Schema.TaggedError()("ConfigExtendsError", { message: Schema.String, cause: Schema.optional(Schema.Unknown) }) {}; var ConfigVersionManifestError = class extends Schema.TaggedError()("ConfigVersionManifestError", { message: Schema.String, cause: Schema.optional(Schema.Unknown) }) {}; var DevOverlayError = class extends Schema.TaggedError()("DevOverlayError", { path: Schema.String, message: Schema.String, cause: Schema.optional(Schema.Unknown) }) {}; function defaultConfigEnv() { return process.env.NODE_ENV === "production" ? "production" : "development"; } function parseRuntimeOverrideTargets(value) { if (!value) return []; return [...new Set(value.split(",").map((entry) => entry.trim()).filter(Boolean))].map((entry) => { if (entry === "ui" || entry === "api" || entry === "plugins") return entry; if (entry.startsWith("plugins.") && entry.length > 8) return entry; throw new ConfigExtendsError({ message: `Invalid runtime override target: ${entry}` }); }); } function isRuntimeOverrideAllowed(allowedTargets, target) { if (allowedTargets.includes(target)) return true; if (target.startsWith("plugins.")) return allowedTargets.includes("plugins.*"); return false; } async function buildRuntimePluginsForConfig(config, baseDir, env, remotePlugins) { const plugins = await resolveRuntimePlugins(config.plugins ?? {}, baseDir, env, remotePlugins); return Object.keys(plugins).length > 0 ? plugins : void 0; } function getEntryAssociatedUi(entry) { const flat = isPlainObject(entry.ui) ? entry.ui : void 0; const nested = (() => { if (!isPlainObject(entry.app)) return void 0; const app = entry.app; return isPlainObject(app.ui) ? app.ui : void 0; })(); const ui = flat ?? nested; if (!ui) return void 0; if ("shared" in ui) throw new ConfigExtendsError({ message: "app.ui.shared is no longer supported. Move shared deps to app.api.shared, app.auth.shared, or plugins.*.shared." }); return ui; } function mergeAssociatedUi(parentUi, childUi) { if (!parentUi) return childUi ? { ...childUi } : void 0; if (!childUi) return { ...parentUi }; return bosConfigMerger({ ...childUi }, parentUi); } function withAssociatedUi(entry, associatedUi) { if (!associatedUi) return entry; const app = isPlainObject(entry.app) ? { ...entry.app } : {}; app.ui = mergeAssociatedUi(getEntryAssociatedUi(entry), associatedUi); return { ...entry, app }; } async function resolveConfigComposableEntries(config, baseDir, env) { const [resolvedApi, resolvedAuth] = await Promise.all([resolveComposableReference(config.app.api, baseDir, env, "app.api"), config.app.auth ? resolveComposableReference(config.app.auth, baseDir, env, "app.auth") : Promise.resolve(void 0)]); const resolvedPlugins = config.plugins ? Object.fromEntries(await Promise.all(Object.entries(config.plugins).map(async ([pluginId, pluginValue]) => { const resolvedPlugin = await resolveComposableReference(asComposableEntry(pluginValue), baseDir, env, `plugins.${pluginId}`); return [pluginId, withAssociatedUi(resolvedPlugin.entry, resolvedPlugin.associatedUi)]; }))) : void 0; return { ...config, app: { ...config.app, api: resolvedApi.entry, auth: resolvedAuth?.entry }, plugins: resolvedPlugins }; } function getResolvedConfigPath(configDir) { return join(configDir, ".bos", RESOLVED_CONFIG_FILENAME); } function loadGeneratedResolvedConfig(configDir) { const resolvedPath = getResolvedConfigPath(configDir); if (!existsSync(resolvedPath)) return null; try { const raw = JSON.parse(readFileSync(resolvedPath, "utf-8")); if (!isPlainObject(raw)) return null; const { _resolved, ...configData } = raw; return BosConfigSchema.parse(configData); } catch { return null; } } function writeResolvedConfig(configDir, config, env, extendsChain, source) { const resolvedPath = getResolvedConfigPath(configDir); const resolvedDir = dirname(resolvedPath); if (!existsSync(resolvedDir)) mkdirSync(resolvedDir, { recursive: true }); const ordered = rebuildOrderedConfig(config); const output = { _resolved: { env, resolvedAt: (/* @__PURE__ */ new Date()).toISOString(), extendsChain: extendsChain ?? [], ...source ? { source } : {} }, ...ordered }; const content = `${JSON.stringify(output, null, 2)}\n`; try { if (readFileSync(resolvedPath, "utf-8") === content) return; } catch {} writeFileSync(resolvedPath, content); } function resolveBosConfigPath(configDir) { const resolvedPath = getResolvedConfigPath(configDir); if (existsSync(resolvedPath)) return resolvedPath; return join(configDir, "bos.config.json"); } async function readBosConfigForBuild(configDir) { const resolvedPath = getResolvedConfigPath(configDir); if (existsSync(resolvedPath)) try { const raw = JSON.parse(readFileSync(resolvedPath, "utf-8")); if (isPlainObject(raw)) { const { _resolved, ...configData } = raw; return configData; } } catch (e) { console.warn(`[Config] Failed to parse _resolved.json, falling back to the authored config: ${String(e)}`); } const authored = await readLocalAuthoredConfigInput(configDir); if (authored) return authored; const bosConfigPath = join(configDir, "bos.config.json"); return JSON.parse(readFileSync(bosConfigPath, "utf-8")); } /** * @internal — splits an extends ref into its config path and optional * `#target` suffix. Plumbing for the resolution session; not part of the * public config surface. */ function parseExtendsTarget(ref) { const hashIndex = ref.indexOf("#"); if (hashIndex === -1) return { configPath: ref }; const configPath = ref.slice(0, hashIndex); const targetPath = ref.slice(hashIndex + 1); return { configPath, targetPath: targetPath.length > 0 ? targetPath : void 0 }; } /** * @internal — resolves the base directory a config path extends from. * Plumbing for the resolution session; not part of the public config surface. */ function getConfigBaseDir(configPath, baseDir) { if (configPath.startsWith("bos://")) return baseDir; return dirname(isAbsolute(configPath) ? configPath : resolve(baseDir, configPath)); } function asComposableEntry(value) { if (value === void 0) return {}; if (typeof value === "string") return { extends: value }; if (!isPlainObject(value)) throw new ConfigExtendsError({ message: `Expected config entry object, received ${typeof value}` }); return value; } function getTargetedEntry(config, targetPath) { if (targetPath.startsWith("app.")) { const entryKey = targetPath.slice(4); return asComposableEntry(config.app?.[entryKey]); } if (targetPath.startsWith("plugins.")) { const pluginId = targetPath.slice(8); if (pluginId.length === 0) throw new ConfigExtendsError({ message: `Invalid plugin target path: ${targetPath}` }); return asComposableEntry(config.plugins?.[pluginId]); } throw new ConfigExtendsError({ message: `Unsupported extends target path: ${targetPath}` }); } function getAssociatedUi(config, _targetPath) { if (!isPlainObject(config.app?.ui)) return; const ui = config.app.ui; if ("shared" in ui) throw new ConfigExtendsError({ message: "app.ui.shared is no longer supported. Move shared deps to app.api.shared, app.auth.shared, or plugins.*.shared." }); return ui; } function applyNullOverrides(target, source) { for (const [key, value] of Object.entries(source)) if (value === null) target[key] = null; else if (isPlainObject(value) && isPlainObject(target[key])) applyNullOverrides(target[key], value); } function mergeComposableEntries(parent, child) { const merged = bosConfigMerger({ ...child }, parent); applyNullOverrides(merged, child); return merged; } function stripUnsafeLocalDevelopment(entry, allowLocalPaths) { if (!entry || allowLocalPaths) return entry; if (typeof entry.development === "string" && entry.development.startsWith(LOCAL_PREFIX)) { const { development: _ignored, ...rest } = entry; return rest; } return entry; } async function resolveComposableReference(source, baseDir, env, defaultTargetPath) { let resolvedEntry = {}; let providerBaseDir = baseDir; let targetPath = defaultTargetPath; let associatedUi = getEntryAssociatedUi(source); let allowLocalPaths = false; let extendsError; const extendsRef = source.extends ? resolveExtendsRef(source.extends, env) : void 0; if (extendsRef) { const { resolveMergedConfigInput } = await import("./resolution/session.mjs"); const parsed = parseExtendsTarget(extendsRef); targetPath = parsed.targetPath ?? defaultTargetPath; const extendsBaseDir = getConfigBaseDir(parsed.configPath, baseDir); try { const extendedConfig = await resolveMergedConfigInput(parsed.configPath, { baseDir: extendsBaseDir, env }); resolvedEntry = mergeComposableEntries(resolvedEntry, getTargetedEntry(extendedConfig, targetPath)); providerBaseDir = extendsBaseDir; associatedUi = mergeAssociatedUi(associatedUi, getAssociatedUi(extendedConfig, targetPath)); } catch (error) { extendsError = error; } } const localDevelopment = typeof source.development === "string" && source.development.startsWith(LOCAL_PREFIX) ? source.development : void 0; const localDevelopmentPath = localDevelopment ? resolve(baseDir, localDevelopment.slice(6).trim()) : void 0; const hasUsableLocalDevelopment = Boolean(localDevelopmentPath && existsSync(localDevelopmentPath)); if (localDevelopmentPath) { const { resolveMergedConfigInput } = await import("./resolution/session.mjs"); const localPath = localDevelopmentPath; const localConfigPath = localConfigEntryPath(localPath); if (localConfigPath) { const localConfig = await resolveMergedConfigInput(localConfigPath, { baseDir: localPath, env }); resolvedEntry = mergeComposableEntries(resolvedEntry, getTargetedEntry(localConfig, targetPath)); providerBaseDir = localPath; associatedUi = mergeAssociatedUi(associatedUi, getAssociatedUi(localConfig, targetPath)); allowLocalPaths = true; } } const sourceOverrides = { ...source }; if (allowLocalPaths && localDevelopment) delete sourceOverrides.development; resolvedEntry = mergeComposableEntries(resolvedEntry, sourceOverrides); associatedUi = mergeAssociatedUi(associatedUi, getEntryAssociatedUi(sourceOverrides)); if (extendsError && !allowLocalPaths && !hasUsableLocalDevelopment) throw extendsError; return { entry: stripUnsafeLocalDevelopment(resolvedEntry, allowLocalPaths || Boolean(localDevelopment)), providerBaseDir, targetPath, associatedUi: stripUnsafeLocalDevelopment(associatedUi, allowLocalPaths || Boolean(localDevelopment)) }; } function resolveDevelopmentTarget(development, production, baseDir, forceSource, target, extendsRef) { if (forceSource === "remote") return resolveRuntimeTarget(production, baseDir, "remote"); if (!development) { if (production && target) { if (extendsRef) emitConfigWarning(`[Config] Resolving "${target}" from ${extendsRef}`); else emitConfigWarning(`[Config] No development target for "${target}", using production`); } return resolveRuntimeTarget(production, baseDir, "remote"); } const devTarget = resolveRuntimeTarget(development, baseDir); if (devTarget.source === "local" && (!devTarget.localPath || !existsSync(devTarget.localPath))) { if (production && target) emitConfigWarning(`[Config] Could not load local target for "${target}", using production`); return resolveRuntimeTarget(production, baseDir, "remote"); } return devTarget; } const buildRuntimeConfigEffect = Effect.fn("buildRuntimeConfig")(function* (config, baseDir, env, options) { const uiConfig = config.app.ui; const apiConfig = config.app.api; const authConfig = config.app.auth; const uiRuntime = env === "development" ? resolveDevelopmentTarget(uiConfig.development, uiConfig.production, baseDir, options?.uiSource, "app.ui") : resolveRuntimeTarget(uiConfig.production, baseDir, "remote"); const apiRuntime = env === "development" ? resolveDevelopmentTarget(apiConfig.development, apiConfig.production, baseDir, options?.apiSource, "app.api") : resolveRuntimeTarget(apiConfig.production, baseDir, "remote"); const authExtendsRef = authConfig?.extends ? resolveExtendsRef(authConfig.extends, env) : void 0; const authRuntime = authConfig ? env === "development" ? resolveDevelopmentTarget(authConfig.development, authConfig.production, baseDir, options?.authSource, "app.auth", authExtendsRef) : resolveRuntimeTarget(authConfig.production, baseDir, "remote") : void 0; const hostConfig = config.app.host; const hostRuntime = env === "development" ? resolveDevelopmentTarget(hostConfig.development, hostConfig.production, baseDir, options?.hostSource, "app.host") : resolveRuntimeTarget(hostConfig.production, baseDir, "remote"); const hostListeningUrl = env === "development" ? resolveDevelopmentHostUrl(hostConfig.development) : `http://localhost:${getProcessEnv("PORT") ?? DEFAULT_HOST_PORT}`; const hostIsRemote = hostRuntime.source === "remote"; const uiIsRemote = uiRuntime.source === "remote"; const apiIsRemote = apiRuntime.source === "remote"; const resolvedApiName = resolvePluginRuntimeName(apiConfig.name, apiRuntime.localPath, "api"); const authEntry = yield* Effect.tryPromise({ try: async () => { if (!authConfig || !authRuntime) return void 0; if (!authRuntime.localPath && !authRuntime.url) return void 0; let authName = resolvePluginRuntimeName(authConfig.name, authRuntime.localPath, "auth"); if (authRuntime.source === "remote" && authRuntime.url && !authRuntime.localPath && typeof authConfig.name !== "string") authName = await resolveRemotePluginRuntimeName(authRuntime.url, authName); return { name: authName, extendsRef: authExtendsRef, url: authRuntime.url, entry: authRuntime.url ? `${authRuntime.url}/mf-manifest.json` : "/mf-manifest.json", localPath: authRuntime.localPath, port: authRuntime.port, source: authRuntime.source, proxy: authConfig.proxy, variables: authConfig.variables, secrets: authConfig.secrets, integrity: authRuntime.source === "remote" ? authConfig.integrity : void 0, shared: authConfig.shared, ui: buildRuntimeUiConfig("auth", env, getEntryAssociatedUi(authConfig), baseDir, authName, options?.authSource) }; }, catch: (cause) => new ConfigExtendsError({ message: String(cause), cause }) }); const runtimePlugins = { ...options?.plugins && Object.keys(options.plugins).length > 0 ? options.plugins : void 0 }; if (authEntry && !runtimePlugins.auth) runtimePlugins.auth = authEntry; const result = { env, account: config.account, domain: config.domain, title: config.title, description: config.description, networkId: getNetworkIdForAccount(config.account), repository: config.repository, host: { name: "host", url: hostListeningUrl, entry: `${hostListeningUrl}/mf-manifest.json`, localPath: hostRuntime.localPath, port: env === "development" ? parsePort(hostListeningUrl) : Number(getProcessEnv("PORT")) || DEFAULT_HOST_PORT, secrets: hostConfig.secrets, integrity: hostIsRemote ? hostConfig.integrity : void 0, source: hostRuntime.source, remoteUrl: hostIsRemote ? hostRuntime.url : void 0 }, ui: { name: resolvePluginRuntimeName(uiConfig.name, uiRuntime.localPath, "ui"), url: uiRuntime.url, entry: uiRuntime.url ? `${uiRuntime.url}/mf-manifest.json` : "/mf-manifest.json", publicUrl: typeof uiConfig.publicUrl === "string" ? uiConfig.publicUrl : void 0, localPath: uiRuntime.localPath, port: uiRuntime.port, ssrUrl: uiIsRemote ? uiConfig.ssr : void 0, ssrIntegrity: uiIsRemote ? uiConfig.ssrIntegrity : void 0, integrity: uiIsRemote ? uiConfig.integrity : void 0, source: uiRuntime.source }, api: { name: resolvedApiName, url: apiRuntime.url, entry: apiRuntime.url ? `${apiRuntime.url}/mf-manifest.json` : "/mf-manifest.json", localPath: apiRuntime.localPath, port: apiRuntime.port, source: apiRuntime.source, proxy: options?.proxy ?? apiConfig.proxy, variables: apiConfig.variables, secrets: apiConfig.secrets, integrity: apiIsRemote ? apiConfig.integrity : void 0, shared: apiConfig.shared, dependsOn: apiConfig.dependsOn ? normalizeStringArray(apiConfig.dependsOn) : void 0 }, auth: authEntry, plugins: Object.keys(runtimePlugins).length > 0 ? runtimePlugins : void 0 }; yield* Effect.tryPromise({ try: async () => { try { if (result.api.source !== "remote" || !result.api.url) return; const manifest = await fetchApiPluginManifest(result.api.url); const manifestPluginEntries = []; if (manifest.plugins?.length) { for (const node of manifestPluginsToNodes(manifest.plugins)) if (!result.plugins?.[node.key]) { manifestPluginEntries.push({ key: node.key, config: { name: node.name, url: node.url, entry: node.entry, source: "remote", dependsOn: node.dependsOn, secrets: node.secrets, variables: node.variables } }); if (node.secrets) { for (const secretName of node.secrets) if (!getProcessEnv(secretName)) warnOutsideEffect(`[Config] Plugin "${node.key}" (discovered from manifest) expects secret "${secretName}" but it is not set in the environment.`); } } } if (manifest.dependsOn?.length) { const existing = new Set(result.api.dependsOn ?? []); for (const dep of manifest.dependsOn) if (!existing.has(dep)) result.api.dependsOn = [...result.api.dependsOn ?? [], dep]; } if (manifestPluginEntries.length > 0) { if (!result.plugins) result.plugins = {}; for (const { key, config: pluginConfig } of manifestPluginEntries) if (!result.plugins[key]) result.plugins[key] = pluginConfig; } } catch (error) { const message = error instanceof Error ? error.message : String(error); warnOutsideEffect(`[Config] Failed to fetch API plugin manifest for discovery: ${message}`); } }, catch: (cause) => new ConfigExtendsError({ message: String(cause), cause }) }); yield* Effect.tryPromise({ try: () => deriveVersionManifestFields(result, config, env), catch: (cause) => new ConfigVersionManifestError({ message: String(cause), cause }) }); return result; }); /** * Version-manifest derivation (atomic-deploys 04): for every remote slot, * resolve its `pin` (fetch + SRI-verify the pinned manifest, process-cached * per pin) and stamp the derived entry-level fields — `entryUrl` (hashed * container entry), the browser-manifest `entry`, and entry-level * `integrity`/`ssrIntegrity`/`ssrEntryUrl` — so consumers load immutable * hashed bytes without knowing the pointer indirection. * * Hard break: outside development every remote slot MUST pin a version * manifest — an unpinned remote slot fails resolution loudly (the deploy * train pins every workspace it uploads; a manifest-less slot means a * pre-atomic-deploy config). */ async function deriveVersionManifestFields(result, config, env) { if (env === "development") return; const derive = async (label, slotConfig, slot, apply) => { if (!slot || slot.source !== "remote" || !slot.url) return; const slotWithProduction = slotConfig; if (!slotWithProduction || slotWithProduction.production === void 0) return; const rawPin = slotConfig?.pin; const pin = rawPin && typeof rawPin.manifest === "string" && rawPin.manifest && typeof rawPin.integrity === "string" && rawPin.integrity ? { manifest: rawPin.manifest, integrity: rawPin.integrity } : void 0; if (!pin) throw new Error(`slot "${label}" (${slot.url}) pins no version manifest — remote slots must pin a version manifest outside development (pre-atomic-deploy config; redeploy required)`); apply(await resolveSlotVersion({ base: slot.remoteUrl ?? slot.url, pin })); }; const applyUi = (ui, r) => { if (!ui) return; ui.entryUrl = r.entryUrl; ui.entry = r.browserManifestUrl ?? ui.entry; ui.browserManifestUrl = r.browserManifestUrl; ui.integrity = r.entryIntegrity; ui.ssrEntryUrl = r.ssrEntryUrl; ui.ssrIntegrity = r.ssrIntegrity ?? ui.ssrIntegrity; }; await derive("app.host", config.app.host, result.host, (r) => { result.host.entryUrl = r.entryUrl; result.host.integrity = r.entryIntegrity; }); await derive("app.ui", config.app.ui, result.ui, (r) => applyUi(result.ui, r)); await derive("app.api", config.app.api, result.api, (r) => { result.api.entryUrl = r.entryUrl; result.api.integrity = r.entryIntegrity; }); if (result.auth) { await derive("app.auth", config.app.auth, result.auth, (r) => { result.auth.entryUrl = r.entryUrl; result.auth.integrity = r.entryIntegrity; }); await derive("app.auth.ui", getEntryAssociatedUi(config.app.auth), result.auth.ui, (r) => applyUi(result.auth.ui, r)); } for (const [key, plugin] of Object.entries(result.plugins ?? {})) { let pluginConfig = config.plugins?.[key]; const isAuthMirror = key === "auth" && result.auth && (plugin === result.auth || plugin.url === result.auth.url); if ((!pluginConfig || typeof pluginConfig === "string") && isAuthMirror) pluginConfig = config.app.auth; if (!pluginConfig || typeof pluginConfig === "string") continue; await derive(`plugins.${key}`, pluginConfig, plugin, (r) => { plugin.entryUrl = r.entryUrl; plugin.integrity = r.entryIntegrity; }); await derive(`plugins.${key}.ui`, getEntryAssociatedUi(pluginConfig), plugin.ui, (r) => applyUi(plugin.ui, r)); } } async function buildRuntimeConfig(config, baseDir, env, options) { return Effect.runPromise(buildRuntimeConfigEffect(config, baseDir, env, options)); } async function readConfigInput(configPath, baseDir) { if (configPath.startsWith("bos://")) return fetchBosConfigFromFastKv(configPath); const resolvedPath = isAbsolute(configPath) ? configPath : resolve(baseDir, configPath); if (isAppDescriptorPath(resolvedPath)) return loadAppDescriptorConfig(resolvedPath); return JSON.parse(readFileSync(resolvedPath, "utf-8")); } function normalizePluginEntry(raw) { if (raw === null || raw === false) return raw; if (typeof raw === "string") return { extends: raw }; return raw; } const resolveRuntimePluginsEffect = Effect.fn("resolveRuntimePlugins")(function* (plugins, baseDir, env, remotePlugins) { const entries = Object.entries(plugins).map(([pluginId, rawInput]) => ({ pluginId, normalized: normalizePluginEntry(rawInput) })).filter((entry) => entry.normalized !== null && entry.normalized !== false); const resolved = yield* Effect.forEach(entries, ({ pluginId, normalized }) => Effect.gen(function* () { const resolvedReference = yield* Effect.tryPromise({ try: () => resolveComposableReference(normalized, baseDir, env, `plugins.${pluginId}`), catch: (cause) => new ConfigExtendsError({ message: String(cause), cause }) }); const forceSource = remotePlugins === void 0 ? void 0 : remotePlugins.length === 0 || remotePlugins.includes(pluginId) ? "remote" : void 0; const pluginRuntime = buildRuntimePluginConfig(pluginId, env, resolvedReference, forceSource); if (!pluginRuntime.localPath && !pluginRuntime.url) { if (forceSource === "remote") emitConfigWarning(`[Config] Plugin "${pluginId}" has no production URL in bos.config.json and cannot be resolved as remote. Add a "production" field or remove it from --remote-plugins.`); return null; } if (pluginRuntime.source === "remote" && pluginRuntime.url && !pluginRuntime.localPath && typeof resolvedReference.entry.name !== "string") pluginRuntime.name = yield* Effect.tryPromise({ try: () => resolveRemotePluginRuntimeName(pluginRuntime.url, pluginRuntime.name), catch: (cause) => new ConfigExtendsError({ message: String(cause), cause }) }); return [pluginId, pluginRuntime]; }), { concurrency: "unbounded" }); const out = {}; for (const entry of resolved) if (entry) out[entry[0]] = entry[1]; return out; }); function resolveRuntimePlugins(plugins, baseDir, env, remotePlugins) { return Effect.runPromise(resolveRuntimePluginsEffect(plugins, baseDir, env, remotePlugins)); } async function resolveRemotePluginRuntimeName(baseUrl, fallback) { const manifest = await fetchJsonOrNull(`${baseUrl.replace(/\/$/, "")}/plugin.manifest.json`, { retries: 2 }); return typeof manifest?.plugin?.name === "string" && manifest.plugin.name.length > 0 ? manifest.plugin.name : fallback; } function buildRuntimePluginConfig(pluginId, env, resolved, forceSource) { const source = resolved.entry; const development = typeof source.development === "string" ? source.development : void 0; const production = typeof source.production === "string" ? source.production : void 0; if (production?.startsWith("bos://")) throw new ConfigExtendsError({ message: `Plugin "${pluginId}" has unsupported production target "${production}". Use extends: "bos://account/domain" for plugin configs or a CDN URL for production.` }); const pluginExtendsRef = source.extends ? resolveExtendsRef(source.extends, env) : void 0; const runtimeTarget = env === "development" ? resolveDevelopmentTarget(development, production, resolved.providerBaseDir, forceSource, `plugins.${pluginId}`, pluginExtendsRef) : resolveRuntimeTarget(production, resolved.providerBaseDir, "remote"); const apiName = resolvePluginRuntimeName(source.name, runtimeTarget.localPath, pluginId); const ui = buildRuntimeUiConfig(pluginId, env, resolved.associatedUi, resolved.providerBaseDir, apiName, forceSource); const routes = source.routes; return { name: apiName, url: runtimeTarget.url, entry: runtimeTarget.url ? `${runtimeTarget.url.replace(/\/$/, "")}/mf-manifest.json` : "/mf-manifest.json", source: runtimeTarget.source, extendsRef: pluginExtendsRef, localPath: runtimeTarget.localPath, port: runtimeTarget.port, proxy: typeof source.proxy === "string" ? source.proxy : void 0, variables: normalizeJsonRecord(source.variables), secrets: normalizeStringArray(source.secrets), shared: source.shared, connectSrc: normalizeStringArray(source.connectSrc), integrity: runtimeTarget.source === "remote" ? source.integrity : void 0, ui, routes, dependsOn: source.dependsOn ? normalizeStringArray(source.dependsOn) : void 0 }; } /** * Resolve a plugin entry's associated ui surface (`plugins.<id>.ui` / legacy * `plugins.<id>.app.ui`) into the runtime ui config. Extracted so app-slot * entries (app.auth) resolve their ui the same way. */ function buildRuntimeUiConfig(pluginId, env, uiConfig, providerBaseDir, apiName, forceSource) { const uiDevelopment = typeof uiConfig?.development === "string" ? uiConfig.development : void 0; const uiProduction = typeof uiConfig?.production === "string" ? uiConfig.production : void 0; const uiRuntime = uiConfig && (uiDevelopment || uiProduction) ? env === "development" ? resolveDevelopmentTarget(uiDevelopment, uiProduction, providerBaseDir, forceSource, `plugins.${pluginId}.ui`) : resolveRuntimeTarget(uiProduction, providerBaseDir, "remote") : void 0; if (!uiRuntime) return void 0; if (uiRuntime.source === "remote" && !uiRuntime.url) return void 0; return { name: resolveUiRuntimeName(uiConfig, uiRuntime.localPath, apiName), url: uiRuntime.url, entry: uiRuntime.url ? `${uiRuntime.url.replace(/\/$/, "")}/mf-manifest.json` : "/mf-manifest.json", source: uiRuntime.source, publicUrl: typeof uiConfig?.publicUrl === "string" ? uiConfig.publicUrl : void 0, localPath: uiRuntime.localPath, port: uiRuntime.port, integrity: uiRuntime.source === "remote" && typeof uiConfig?.integrity === "string" ? uiConfig.integrity : void 0, ssrUrl: uiRuntime.source === "remote" && typeof uiConfig?.ssr === "string" ? uiConfig.ssr : uiRuntime.source === "local" ? uiRuntime.url : void 0, ssrIntegrity: uiRuntime.source === "remote" && typeof uiConfig?.ssrIntegrity === "string" ? uiConfig.ssrIntegrity : void 0 }; } /** * MF container identity must match the build: folder-form plugin ui sources * build under the sanitized plugin workspace package name (the generated * rsbuild config imports the plugin's package.json), so derive the same * value here — an authored `ui.name` is only a fallback, and can never * override what the container actually registers under. */ function resolveUiRuntimeName(uiConfig, localPath, apiName) { if (localPath) { const uiPkgPath = join(localPath, "package.json"); const pkgPath = existsSync(uiPkgPath) ? uiPkgPath : join(dirname(localPath), "package.json"); try { const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); if (typeof pkg.name === "string" && pkg.name.length > 0) return sanitizeContainerName(pkg.name); } catch (e) { console.warn(`[Config] Could not read package.json at ${pkgPath}: ${String(e)}`); } } return typeof uiConfig?.name === "string" ? uiConfig.name : `${apiName}-ui`; } function resolvePluginRuntimeName(explicitName, localPath, fallback) { if (explicitName) return explicitName; if (!localPath) return fallback; try { const packageJsonPath = join(localPath, "package.json"); const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); if (typeof packageJson.name === "string" && packageJson.name.length > 0) return packageJson.name; } catch (e) { console.warn(`[Config] Could not read package.json at ${localPath}: ${String(e)}`); } return fallback; } function normalizeJsonValue(value) { if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value; if (Array.isArray(value)) return value.map((item) => normalizeJsonValue(item)).filter((item) => item !== void 0); if (isPlainObject(value)) { const out = {}; for (const [key, raw] of Object.entries(value)) { const normalized = normalizeJsonValue(raw); if (normalized !== void 0) out[key] = normalized; } return out; } } function normalizeJsonRecord(value) { if (!isPlainObject(value)) return void 0; const out = {}; for (const [key, raw] of Object.entries(value)) { const normalized = normalizeJsonValue(raw); if (normalized !== void 0) out[key] = normalized; } return Object.keys(out).length > 0 ? out : void 0; } function normalizeStringArray(value) { if (!Array.isArray(value)) return void 0; const out = value.filter((item) => typeof item === "string" && item.length > 0); return out.length > 0 ? out : void 0; } function resolveRuntimeTarget(value, baseDir, defaultSource = "remote") { if (!value) return { source: defaultSource, url: "" }; if (value.startsWith(LOCAL_PREFIX)) { const localTarget = value?.slice(6).trim(); if (!localTarget) throw new ConfigExtendsError({ message: `Invalid local development target: ${value}` }); const localPath = resolve(baseDir, localTarget); if (!existsSync(localPath)) return { source: "local", url: "" }; return { source: "local", url: "", localPath }; } return { source: defaultSource, url: value.replace(/\/$/, ""), port: parsePort(value) }; } function isLocalDevelopmentTarget(value) { return typeof value === "string" && value.startsWith(LOCAL_PREFIX); } function resolveLocalDevelopmentPath(value, baseDir) { if (!isLocalDevelopmentTarget(value)) return null; const localTarget = value.slice(6).trim(); return localTarget ? resolve(baseDir, localTarget) : null; } function resolveDevelopmentHostUrl(value) { if (!value || isLocalDevelopmentTarget(value)) return `http://localhost:${DEFAULT_HOST_PORT}`; return value.replace(/\/$/, ""); } function getHostDevelopmentPort(value) { return parsePort(resolveDevelopmentHostUrl(value)); } function parsePort(url) { try { const parsed = new URL(url); return parsed.port ? parseInt(parsed.port, 10) : parsed.protocol === "https:" ? 443 : 80; } catch { return 3e3; } } //#endregion export { BOS_CONFIG_ORDER, BosConfigSchema, CircularExtendsError, ConfigExtendsError, ConfigLoadError, ConfigNotfoundError, ConfigVersionManifestError, DEV_OVERLAY_FILENAME, DevOverlayError, MISSING_CONFIG_MESSAGE, buildRuntimeConfig, buildRuntimeConfigEffect, buildRuntimePluginsForConfig, defaultConfigEnv, findConfigPath, getConfigBaseDir, getHostDevelopmentPort, getResolvedConfigPath, isAppDescriptorPath, isLocalDevelopmentTarget, isRuntimeOverrideAllowed, loadAppDescriptorConfig, loadGeneratedResolvedConfig, localConfigEntryPath, mergeBosConfigWithExtends, parseAppDescriptorModule, parseDevOverlayModule, parseExtendsTarget, parsePort, parseRuntimeOverrideTargets, readAuthoredConfigInput, readBosConfigForBuild, readLocalAuthoredConfigInput, rebuildOrderedConfig, resetConfigPathCache, resolveBosConfigPath, resolveComposableReference, resolveConfigComposableEntries, resolveDevelopmentHostUrl, resolveExtendsRef, resolveLocalDevelopmentPath, resolvePluginRuntimeName, resolveUiRuntimeName, runWithConfigWarningSink, writeResolvedConfig }; //# sourceMappingURL=config.mjs.map