UNPKG

everything-dev

Version:

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

371 lines (369 loc) • 16.3 kB
import { bosConfigMerger, isPlainObject, resolveExtendsRef } from "./merge.mjs"; import { AppDescriptorSchema } from "../../descriptor/schema.mjs"; import { resolveApp, toConfigInput } from "../../descriptor/resolve.mjs"; import { fetchBosConfigFromFastKv } from "../registry/fastkv.mjs"; import { AsyncLocalStorage } from "node:async_hooks"; import { existsSync, readFileSync } from "node:fs"; import { dirname, isAbsolute, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { Schema } from "effect"; //#region src/services/resolution/load.ts const LOCAL_PREFIX = "local:"; 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); } /** * Evaluate an authored descriptor's source text in-process: descriptor-constructor * imports (`App`/`API`/`UI`/`Plugin` from `everything-dev/descriptor`) are * stubbed with pure-identity shims and the remaining module body is imported * from a data URL. Byte-deterministic — no bundler resolution of the * descriptor package and no module-cache interference (a tempdir child has no * node_modules, so a real dynamic import is both fragile and here served * stale, mangled transforms under vitest). */ async function loadInProcessDescriptor(source, label) { const body = source.split("\n").filter((line) => !/^\s*import\b/.test(line)).join("\n"); const shims = [ "const App = (input) => input;", "const API = (input) => input;", "const UI = (input) => input;", "const Plugin = (name) => ({ path: (dir, extra = {}) => ({ ...extra, name, path: dir }), extends: (ref, extra = {}) => ({ ...extra, name, extends: ref }) });" ].join("\n"); return parseAppDescriptorModule(await import( /* webpackIgnore: true */ `data:text/javascript;base64,${Buffer.from(`${shims}\n${body}`, "utf-8").toString("base64")}` ), label); } /** * @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 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 }; } /** * @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:")) { 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("./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:") ? 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("./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)) }; } 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")); } //#endregion export { CircularExtendsError, ConfigExtendsError, ConfigLoadError, ConfigNotfoundError, ConfigVersionManifestError, DEV_OVERLAY_FILENAME, DevOverlayError, LOCAL_PREFIX, MISSING_CONFIG_MESSAGE, defaultConfigEnv, emitConfigWarning, findConfigPath, getConfigBaseDir, getEntryAssociatedUi, isAppDescriptorPath, loadAppDescriptorConfig, loadInProcessDescriptor, localConfigEntryPath, parseAppDescriptorModule, parseDevOverlayModule, parseExtendsTarget, readAuthoredConfigInput, readLocalAuthoredConfigInput, resetConfigPathCache, resolveComposableReference, resolveConfigComposableEntries, runWithConfigWarningSink }; //# sourceMappingURL=load.mjs.map