UNPKG

everything-dev

Version:

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

478 lines (476 loc) • 23 kB
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