everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
331 lines (329 loc) • 11.2 kB
JavaScript
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