everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
868 lines (866 loc) • 39.9 kB
JavaScript
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