UNPKG

everything-dev

Version:

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

487 lines (485 loc) • 24 kB
const require_merge = require('./merge.cjs'); const require_http_client = require('../../utils/http-client.cjs'); const require_load = require('./load.cjs'); const require_process_env = require('../../env/process-env.cjs'); const require_network = require('../../utils/network.cjs'); const require_api_contract = require('../codegen/api-contract.cjs'); const require_services_deploy_version_manifest = require('../deploy/version-manifest.cjs'); const require_services_registry_dag = require('../registry/dag.cjs'); let node_fs = require("node:fs"); let node_path = require("node:path"); let effect = require("effect"); let every_plugin_ui_manifest_contract = require("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) require_load.emitConfigWarning(`[Config] Resolving "${target}" from ${extendsRef}`); else require_load.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 || !(0, node_fs.existsSync)(devTarget.localPath))) { if (production && target) require_load.emitConfigWarning(`[Config] Could not load local target for "${target}", using production`); return resolveRuntimeTarget(production, baseDir, "remote"); } return devTarget; } const buildRuntimeConfigEffect = effect.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 ? require_merge.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:${require_process_env.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.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, require_load.getEntryAssociatedUi(authConfig), baseDir, authName, options?.authSource) }; }, catch: (cause) => new require_load.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: require_network.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(require_process_env.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.Effect.tryPromise({ try: async () => { try { if (result.api.source !== "remote" || !result.api.url) return; const manifest = await require_api_contract.fetchApiPluginManifest(result.api.url); const manifestPluginEntries = []; if (manifest.plugins?.length) { for (const node of require_services_registry_dag.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 (!require_process_env.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 require_load.ConfigExtendsError({ message: String(cause), cause }) }); yield* effect.Effect.tryPromise({ try: () => deriveVersionManifestFields(result, config, env), catch: (cause) => new require_load.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 require_services_deploy_version_manifest.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?.[every_plugin_ui_manifest_contract.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", require_load.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`, require_load.getEntryAssociatedUi(pluginConfig), plugin.ui, (r) => applyUi(plugin.ui, r)); } } async function buildRuntimeConfig(config, baseDir, env, options) { return effect.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.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.Effect.forEach(entries, ({ pluginId, normalized }) => effect.Effect.gen(function* () { const resolvedReference = yield* effect.Effect.tryPromise({ try: () => require_load.resolveComposableReference(normalized, baseDir, env, `plugins.${pluginId}`), catch: (cause) => new require_load.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") require_load.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.Effect.tryPromise({ try: () => resolveRemotePluginRuntimeName(pluginRuntime.url, pluginRuntime.name), catch: (cause) => new require_load.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.Effect.runPromise(resolveRuntimePluginsEffect(plugins, baseDir, env, remotePlugins)); } async function resolveRemotePluginRuntimeName(baseUrl, fallback) { const manifest = await require_http_client.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 require_load.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 ? require_merge.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 = (0, node_path.join)(localPath, "package.json"); const pkgPath = (0, node_fs.existsSync)(uiPkgPath) ? uiPkgPath : (0, node_path.join)((0, node_path.dirname)(localPath), "package.json"); try { const pkg = JSON.parse((0, node_fs.readFileSync)(pkgPath, "utf-8")); if (typeof pkg.name === "string" && pkg.name.length > 0) return (0, every_plugin_ui_manifest_contract.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 = (0, node_path.join)(localPath, "package.json"); const packageJson = JSON.parse((0, node_fs.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 (require_merge.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 (!require_merge.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(require_load.LOCAL_PREFIX.length).trim(); if (!localTarget) throw new require_load.ConfigExtendsError({ message: `Invalid local development target: ${value}` }); const localPath = (0, node_path.resolve)(baseDir, localTarget); if (!(0, node_fs.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(require_load.LOCAL_PREFIX.length).trim(); return localTarget ? (0, node_path.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 exports.buildRuntimeConfig = buildRuntimeConfig; exports.buildRuntimeConfigEffect = buildRuntimeConfigEffect; exports.buildRuntimePluginsForConfig = buildRuntimePluginsForConfig; exports.getHostDevelopmentPort = getHostDevelopmentPort; exports.isLocalDevelopmentTarget = isLocalDevelopmentTarget; exports.parsePort = parsePort; exports.resolveDevelopmentHostUrl = resolveDevelopmentHostUrl; exports.resolveLocalDevelopmentPath = resolveLocalDevelopmentPath; exports.resolvePluginRuntimeName = resolvePluginRuntimeName; exports.resolveUiRuntimeName = resolveUiRuntimeName; //# sourceMappingURL=runtime.cjs.map