UNPKG

everything-dev

Version:

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

331 lines (329 loc) • 11.2 kB
import { mergeBosConfigWithExtends, resolveExtendsRef } from "../merge.mjs"; import { BosConfigSchema } from "../types.mjs"; import { applyDevOverlay } from "../descriptor/resolve.mjs"; import { fetchBosConfigFromFastKv } from "../fastkv.mjs"; import { CircularExtendsError, ConfigExtendsError, ConfigLoadError, ConfigNotfoundError, DEV_OVERLAY_FILENAME, buildRuntimeConfig, buildRuntimePluginsForConfig, defaultConfigEnv, findConfigPath, getConfigBaseDir, isAppDescriptorPath, loadAppDescriptorConfig, parseAppDescriptorModule, parseDevOverlayModule, parseExtendsTarget, resolveConfigComposableEntries, runWithConfigWarningSink } from "../config.mjs"; import { parseBosRef } from "../cli/utils/helpers.mjs"; import { readWorkspaceCatalog, resolveSourceDir } from "../cli/init.mjs"; import { existsSync, readFileSync } from "node:fs"; import { dirname, isAbsolute, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; //#region src/resolution/session.ts async function defaultImportModule(path) { return import( /* webpackIgnore: true */ pathToFileURL(path).href ); } function resolveIo(io) { return { fetchBosConfig: io?.fetchBosConfig ?? (async (bosUrl, registry) => fetchBosConfigFromFastKv(bosUrl, registry)), readFileOrNull: io?.readFileOrNull ?? (async (path) => existsSync(path) ? readFileSync(path, "utf-8") : null), importModule: io?.importModule ?? defaultImportModule }; } async function loadNodeConfig(ref, baseDir, io, registry) { if (ref.startsWith("bos://")) return await io.fetchBosConfig(ref, registry); const resolvedPath = isAbsolute(ref) ? ref : resolve(baseDir, ref); if (isAppDescriptorPath(resolvedPath)) return io.importModule === defaultImportModule ? loadAppDescriptorConfig(resolvedPath) : parseAppDescriptorModule(await io.importModule(resolvedPath), resolvedPath); const content = await io.readFileOrNull(resolvedPath); if (content === null) throw new ConfigLoadError({ path: resolvedPath, message: `Config file not found: ${resolvedPath}` }); return JSON.parse(content); } async function walkExtends(ref, baseDir, visited, chain, options) { if (visited.has(ref)) throw new CircularExtendsError({ chain: [...visited, ref], message: `Circular extends detected: ${[...visited, ref].join(" -> ")}` }); let config; let nodeBaseDir = baseDir; let sourceDir; if (ref.startsWith("bos://") && options.collectCatalogs) { const parsed = parseBosRef(ref); if (!parsed) throw new ConfigLoadError({ path: ref, message: `Invalid bos ref: ${ref}` }); const sourceResult = await resolveSourceDir({ extendsAccount: parsed.account, extendsGateway: parsed.gateway }); config = sourceResult.parentConfig; sourceDir = sourceResult.sourceDir || void 0; options.registerCleanup?.(sourceResult.cleanup); nodeBaseDir = sourceDir ?? baseDir; } else if (ref.startsWith("bos://")) config = await loadNodeConfig(ref, baseDir, options.io, options.registry); else { const resolvedPath = isAbsolute(ref) ? ref : resolve(baseDir, ref); nodeBaseDir = dirname(resolvedPath); config = await loadNodeConfig(ref, nodeBaseDir, options.io, options.registry); } chain.push(ref); const extendsRef = config.extends ? resolveExtendsRef(config.extends, options.env) : void 0; if (!extendsRef) { await options.visit?.({ ref, config, baseDir: nodeBaseDir, sourceDir, isLeaf: true }); return config; } const parsedParentRef = parseExtendsTarget(extendsRef); const parentBaseDir = getConfigBaseDir(parsedParentRef.configPath, nodeBaseDir); if (options.collectCatalogs && !canResolveExtendsRef(parsedParentRef.configPath, sourceDir ?? nodeBaseDir)) { await options.visit?.({ ref, config, baseDir: nodeBaseDir, sourceDir, isLeaf: true }); return config; } const nextVisited = new Set(visited); nextVisited.add(ref); const parent = await walkExtends(parsedParentRef.configPath, parentBaseDir, nextVisited, chain, options); await options.visit?.({ ref, config, baseDir: nodeBaseDir, sourceDir, isLeaf: false }); return mergeBosConfigWithExtends(parent, config); } function canResolveExtendsRef(parentRef, baseDir) { if (parentRef.startsWith("bos://")) return parseBosRef(parentRef) !== null; if (!baseDir) return false; return existsSync(resolve(baseDir, parentRef)); } function dispatchEntry(request, cwd) { if (request?.path) { const configPath = request.path; if (configPath.startsWith("bos://")) return { entry: configPath, baseDir: cwd, root: cwd, path: configPath, remote: true }; const baseDir = dirname(configPath); return { entry: configPath, baseDir, root: baseDir, path: configPath, remote: false }; } if (request?.bosUrl) return { entry: request.bosUrl, baseDir: cwd, root: cwd, path: null, remote: true }; if (request?.account) { if (!request.gateway) throw new ConfigExtendsError({ message: `Remote account "${request.account}" requires a gateway` }); return { entry: `bos://${request.account}/${request.gateway}`, baseDir: cwd, root: cwd, path: null, remote: true }; } if (request?.sourceDir) { const sourceRoot = resolve(request.sourceDir); const configPath = findConfigPath(sourceRoot); if (!configPath) return null; return { entry: configPath, baseDir: dirname(configPath), root: sourceRoot, path: configPath, remote: false }; } const configPath = findConfigPath(cwd); if (!configPath) return null; const baseDir = dirname(configPath); return { entry: configPath, baseDir, root: baseDir, path: configPath, remote: false }; } function isConfigNotFound(error) { return error instanceof Error && error.message.startsWith("No config found for "); } var ResolutionSession = class ResolutionSession { config; runtime; root; path; chain; rawConfig; catalog; repository; #env; #warnings; constructor(parts) { this.config = parts.config; this.runtime = parts.runtime; this.root = parts.root; this.path = parts.path; this.chain = parts.chain; this.rawConfig = parts.rawConfig; this.catalog = parts.catalog; this.repository = parts.repository; this.#env = parts.env; this.#warnings = parts.warnings; } static fromParts(parts) { return new ResolutionSession({ config: parts.config, runtime: parts.runtime, root: parts.root, path: parts.source?.path ?? null, chain: parts.source?.extended ?? [], rawConfig: null, catalog: null, repository: void 0, env: defaultConfigEnv(), warnings: [...parts.warnings ?? []] }); } get warnings() { const drained = [...this.#warnings]; this.#warnings.length = 0; return drained; } async buildRuntime(sources) { if (!this.config) throw new ConfigNotfoundError({ message: "No config loaded in this resolution session" }); const config = this.config; const env = sources?.env ?? this.#env; return runWithConfigWarningSink(this.#warnings, () => buildRuntimeConfig(config, this.root, env, { plugins: sources?.plugins ?? {}, uiSource: sources?.uiSource, apiSource: sources?.apiSource, authSource: sources?.authSource, hostSource: sources?.hostSource })); } }; /** * Opens a resolution session for the requested config source. In development, * a local leaf's `bos.dev.ts` overlay (beside the discovered entry config) is * merged child-wins over the extended input before parsing; overlays are never * read for `bos://` entries or in production/staging. */ async function openResolution(request, io) { const resolvedIo = resolveIo(io); const env = request?.env ?? defaultConfigEnv(); const runtimeEnv = env === "staging" ? "production" : env; const cwd = request?.cwd ?? process.cwd(); const collectCatalogs = request?.collectCatalogs === true; const warnings = []; const cleanups = []; try { const parts = await runWithConfigWarningSink(warnings, async () => { const dispatch = dispatchEntry(request, cwd); if (!dispatch) return null; const chain = []; const catalogs = []; let repository; let rawConfig; try { const mergedRaw = await walkExtends(dispatch.entry, dispatch.baseDir, /* @__PURE__ */ new Set(), chain, { env, registry: request?.registry, io: resolvedIo, collectCatalogs, registerCleanup: (fn) => cleanups.push(fn), visit: async (link) => { if (link.ref === dispatch.entry) rawConfig = link.config; if (!collectCatalogs) return; catalogs.push(link.sourceDir ? readWorkspaceCatalog(link.sourceDir) : {}); const repositoryValue = link.config.repository; if (typeof repositoryValue === "string") repository = repositoryValue; } }); let overlayMerged = mergedRaw; if (env === "development" && !dispatch.remote) { const overlayPath = join(dirname(dispatch.entry), DEV_OVERLAY_FILENAME); if (await resolvedIo.readFileOrNull(overlayPath) !== null) { const overlay = parseDevOverlayModule(await resolvedIo.importModule(overlayPath), overlayPath); overlayMerged = applyDevOverlay(mergedRaw, overlay); } } const config = await resolveConfigComposableEntries(BosConfigSchema.parse(overlayMerged), dispatch.baseDir, runtimeEnv); const pluginRuntime = await buildRuntimePluginsForConfig(config, dispatch.baseDir, runtimeEnv, request?.remotePlugins) ?? {}; return { config, runtime: await buildRuntimeConfig(config, dispatch.baseDir, runtimeEnv, { plugins: pluginRuntime }), root: dispatch.root, path: dispatch.path, chain, rawConfig: rawConfig ?? null, catalog: collectCatalogs ? Object.assign({}, ...[...catalogs].reverse()) : null, repository }; } catch (error) { if (dispatch.remote && isConfigNotFound(error)) return null; throw error; } }); if (!parts) return null; return new ResolutionSession({ ...parts, env: runtimeEnv, warnings }); } finally { for (const cleanup of [...cleanups].reverse()) await cleanup(); } } async function walkExtendsChain(entry, options) { const baseDir = entry.startsWith("bos://") ? process.cwd() : dirname(entry); const chain = []; const ownedCleanups = []; try { return { chain, config: await walkExtends(entry, baseDir, /* @__PURE__ */ new Set(), chain, { env: options.env ?? "development", registry: options.registry, io: resolveIo(options.io), collectCatalogs: options.collectCatalogs === true, visit: options.visit, registerCleanup: options.registerCleanup ?? ((fn) => ownedCleanups.push(fn)) }) }; } finally { for (const cleanup of [...ownedCleanups].reverse()) await cleanup(); } } /** * @internal — resolves the fully merged (parent-merged, un-parsed) * `BosConfigInput` for a config entry. Used by config.ts's * `resolveComposableReference` for plugin-entry extends resolution. */ async function resolveMergedConfigInput(entry, options) { return walkExtends(entry, options?.baseDir ?? process.cwd(), /* @__PURE__ */ new Set(), [], { env: options?.env ?? "development", registry: options?.registry, io: resolveIo(options?.io), collectCatalogs: false }); } //#endregion export { ResolutionSession, openResolution, resolveMergedConfigInput, walkExtendsChain }; //# sourceMappingURL=session.mjs.map