everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
478 lines (476 loc) • 23 kB
JavaScript
import { isPlainObject, resolveExtendsRef } from "./merge.mjs";
import { fetchJsonOrNull } from "../../utils/http-client.mjs";
import { ConfigExtendsError, ConfigVersionManifestError, LOCAL_PREFIX, emitConfigWarning, getEntryAssociatedUi, resolveComposableReference } from "./load.mjs";
import { getProcessEnv } from "../../env/process-env.mjs";
import { getNetworkIdForAccount } from "../../utils/network.mjs";
import { fetchApiPluginManifest } from "../codegen/api-contract.mjs";
import { resolveSlotVersion } from "../deploy/version-manifest.mjs";
import { manifestPluginsToNodes } from "../registry/dag.mjs";
import { existsSync, readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { Effect } from "effect";
import { MANIFEST_FILENAME, sanitizeContainerName } from "every-plugin/ui/manifest/contract";
//#region src/services/resolution/runtime.ts
const warnOutsideEffect = (...args) => {
console.warn(...args);
};
const DEFAULT_HOST_PORT = 3e3;
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 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.
*
* Pinned remote slots derive in every environment, development included —
* hash-canonical bundles never serve the fixed-name dev entry, so a dev
* consumer pointing at remote production slots needs the same `entryUrl`
* stamps or its readiness probe 404s forever. Unpinned remote slots skip
* silently in development (a local-links resolution has nothing to pin);
* outside development they fail loud — the deploy train pins every
* workspace it uploads, so a manifest-less slot there is a
* pre-atomic-deploy config.
*/
async function deriveVersionManifestFields(result, config, env) {
const isDev = env === "development";
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) {
if (isDev) return;
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)`);
}
let resolved;
try {
resolved = await resolveSlotVersion({
base: slot.remoteUrl ?? slot.url,
pin
});
} catch (cause) {
if (isDev) {
warnOutsideEffect(`[Config] Version-manifest pin for "${label}" (${pin.manifest}) failed to resolve, continuing unpinned (development): ${cause instanceof Error ? cause.message : String(cause)}`);
return;
}
throw cause;
}
apply(resolved);
};
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;
ui.manifestIntegrity = r.files?.[MANIFEST_FILENAME] ?? ui.manifestIntegrity;
ui.sharedVersions = r.sharedVersions;
};
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));
}
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:")) {
const localTarget = value?.slice(LOCAL_PREFIX.length).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:");
}
function resolveLocalDevelopmentPath(value, baseDir) {
if (!isLocalDevelopmentTarget(value)) return null;
const localTarget = value.slice(LOCAL_PREFIX.length).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 { buildRuntimeConfig, buildRuntimeConfigEffect, buildRuntimePluginsForConfig, getHostDevelopmentPort, isLocalDevelopmentTarget, parsePort, resolveDevelopmentHostUrl, resolveLocalDevelopmentPath, resolvePluginRuntimeName, resolveUiRuntimeName };
//# sourceMappingURL=runtime.mjs.map