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