everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
401 lines (399 loc) • 16.9 kB
JavaScript
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join, relative } from "node:path";
import { createHash } from "node:crypto";
//#region src/api-contract.ts
const REMOTE_FETCH_TIMEOUT_MS = 1e4;
function sha256(input) {
return createHash("sha256").update(input).digest("hex");
}
function trimTrailingSlash(input) {
return input.replace(/\/$/, "");
}
function sanitizeIdentifier(input) {
return input.replace(/[^A-Za-z0-9_]/g, "_").replace(/^[^A-Za-z_]+/, "_");
}
function toImportPath(fromFile, targetFile) {
const rel = relative(dirname(fromFile), targetFile).replace(/\\/g, "/");
return rel.startsWith(".") ? rel : `./${rel}`;
}
function writeFileIfChanged(filePath, content) {
try {
if (readFileSync(filePath, "utf8") === content) return false;
} catch {}
writeFileSync(filePath, content);
return true;
}
function getApiPluginManifestUrl(apiBaseUrl) {
return `${trimTrailingSlash(apiBaseUrl)}/plugin.manifest.json`;
}
async function fetchWithTimeout(url) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), REMOTE_FETCH_TIMEOUT_MS);
try {
return await fetch(url, { signal: controller.signal });
} catch (error) {
if (error instanceof Error && error.name === "AbortError") throw new Error(`Timed out fetching ${url} after ${REMOTE_FETCH_TIMEOUT_MS}ms`);
throw error;
} finally {
clearTimeout(timeout);
}
}
async function fetchApiPluginManifest(apiBaseUrl) {
const response = await fetchWithTimeout(getApiPluginManifestUrl(apiBaseUrl));
if (!response.ok) throw new Error(`Failed to fetch API plugin manifest: ${response.status} ${response.statusText}`);
const manifest = await response.json();
if (manifest.schemaVersion !== 1 || manifest.kind !== "every-plugin/manifest") throw new Error("Unsupported API plugin manifest format");
return manifest;
}
function localApiContractSource(configDir) {
return {
key: "api",
importName: "BaseApiContract",
sourceFilePath: join(configDir, "api", "src", "contract.ts")
};
}
function localAuthContractSource(configDir) {
return {
key: "auth",
importName: "authContract",
sourceFilePath: join(configDir, "plugins", "auth", "src", "contract.ts")
};
}
async function remoteContractSource(opts) {
const manifest = await fetchApiPluginManifest(opts.baseUrl);
if (!manifest.contract) throw new Error(`Plugin manifest for ${manifest.plugin.name} does not advertise contract types`);
const contractResponse = await fetchWithTimeout(`${trimTrailingSlash(opts.baseUrl)}/${manifest.contract.types.path.replace(/^\.\//, "")}`);
if (!contractResponse.ok) throw new Error(`Failed to fetch contract types: ${contractResponse.status} ${contractResponse.statusText}`);
const contractTypes = await contractResponse.text();
if (manifest.contract.types.sha256 && manifest.contract.types.sha256 !== sha256(contractTypes)) throw new Error("Fetched contract types failed checksum verification");
const generatedPath = join(opts.runtimeDir, opts.generatedSubdir, "contract.d.ts");
mkdirSync(dirname(generatedPath), { recursive: true });
writeFileIfChanged(generatedPath, contractTypes);
return {
key: opts.name,
importName: `${sanitizeIdentifier(opts.name)}Contract`,
sourceFilePath: generatedPath,
generatedPath
};
}
async function fetchAuthExportTypes(opts) {
if (!opts.manifest.additionalExports || opts.manifest.additionalExports.length === 0) return null;
const authExportEntry = opts.manifest.additionalExports.find((entry) => entry.path.includes("auth-export") || entry.path.endsWith("auth-export.d.ts"));
if (!authExportEntry) return null;
const response = await fetchWithTimeout(`${trimTrailingSlash(opts.baseUrl)}/${authExportEntry.path.replace(/^\.\//, "")}`);
if (!response.ok) {
console.warn(`[API Contract] Failed to fetch auth export types: ${response.status}`);
return null;
}
const content = await response.text();
if (authExportEntry.sha256 && authExportEntry.sha256 !== sha256(content)) {
console.warn("[API Contract] Auth export types checksum mismatch");
return null;
}
const generatedPath = join(opts.runtimeDir, "auth", "auth-export.d.ts");
mkdirSync(dirname(generatedPath), { recursive: true });
writeFileIfChanged(generatedPath, content);
return generatedPath;
}
function writeAuthTypesGen(targetPath, authExportPath) {
const exportImportPath = toImportPath(targetPath, authExportPath);
const content = [
`export type {`,
` Auth,`,
` AuthOrganizationContext,`,
` AuthOrganization,`,
` AuthOrganizationSummary,`,
` AuthOrganizationMember,`,
` AuthApiKey,`,
` AuthInvitation,`,
` GetActiveMemberInput,`,
` GetOrganizationInput,`,
` ListMembersInput,`,
` ListInvitationsInput,`,
` ListApiKeysInput,`,
` AuthServices,`,
` createAuthInstance,`,
`} from "${exportImportPath}";`,
`import type { InferOutput, ContractType as AuthContract } from "${toImportPath(targetPath, join(dirname(authExportPath), "contract.d.ts"))}";`,
`import type { Auth as BaseAuth } from "${exportImportPath}";`,
"",
"type RawAuthSession = InferOutput<\"getSession\">;",
"type RawAuthRequestContext = InferOutput<\"getContext\">;",
"type RawAuthActiveMember = InferOutput<\"getActiveMember\">;",
"",
"export type AuthSessionUser = NonNullable<RawAuthSession[\"user\"]> & {",
" role?: string | null;",
" isAnonymous?: boolean | null;",
" walletAddress?: string | null;",
" banned?: boolean | null;",
"};",
"export type AuthSessionData = NonNullable<RawAuthSession[\"session\"]> & {",
" activeOrganizationId?: string | null;",
"};",
"export type AuthSession = {",
" user: AuthSessionUser | null;",
" session: AuthSessionData | null;",
"};",
"export type AuthRequestContext = RawAuthRequestContext;",
"export type AuthActiveMember = RawAuthActiveMember;",
"export type AuthBaseSession = BaseAuth[\"$Infer\"][\"Session\"];",
"export type AuthContractType = AuthContract;",
""
].join("\n");
mkdirSync(dirname(targetPath), { recursive: true });
writeFileIfChanged(targetPath, content);
}
function writeFallbackAuthTypesGen(targetPath) {
const content = [
"import type { Auth } from \"better-auth\";",
"export type { Auth } from \"better-auth\";",
"export type AuthSession = Auth[\"$Infer\"][\"Session\"];",
"export type AuthSessionData = AuthSession;",
"export type AuthSessionUser = NonNullable<AuthSession[\"user\"]>;",
"export interface AuthOrganizationContext {",
" activeOrganizationId: string | null;",
" organization: { id: string; name: string; slug: string; logo?: string | null; metadata?: Record<string, unknown> } | null;",
" member: { id: string; role: string } | null;",
" isPersonal: boolean;",
" hasOrganization: boolean;",
"}",
"export interface AuthRequestContext {",
" user: AuthSessionUser | null;",
" userId: string | null;",
" isAuthenticated: boolean;",
" authMethod: \"session\" | \"apiKey\" | \"anonymous\" | \"none\";",
" near: {",
" primaryAccountId: string | null;",
" linkedAccounts: Array<{ accountId: string; network: string; publicKey: string; isPrimary: boolean }>;",
" hasNearAccount: boolean;",
" };",
" organization: AuthOrganizationContext;",
" organizations?: Array<{ id: string; role: string; name?: string; slug?: string }>;",
"}",
"export type AuthActiveMember = { id: string | null; role: string | null; organizationId: string | null };",
"export type AuthOrganization = {",
" id: string;",
" name: string;",
" slug: string;",
" logo?: string | null;",
" metadata?: Record<string, unknown> | null;",
" createdAt: Date;",
"};",
"export type AuthOrganizationSummary = NonNullable<AuthOrganizationContext[\"organization\"]>;",
"export type AuthOrganizationMember = NonNullable<AuthOrganizationContext[\"member\"]>;",
"export type AuthApiKey = {",
" id: string;",
" name: string | null;",
" prefix: string | null;",
" start: string | null;",
" expiresAt: Date | null;",
" createdAt: Date;",
" updatedAt: Date;",
" metadata: unknown | null;",
" permissions: Record<string, string[]> | null;",
"};",
"export type AuthInvitation = {",
" id: string;",
" organizationId: string;",
" email: string;",
" role: string | null;",
" status: string;",
" expiresAt: Date;",
" inviterId: string;",
"};",
"export type GetActiveMemberInput = { organizationId?: string };",
"export type GetOrganizationInput = { id: string };",
"export type ListMembersInput = { organizationId: string };",
"export type ListInvitationsInput = { organizationId: string };",
"export type ListApiKeysInput = { organizationId?: string };",
"export type createAuthInstance = never;",
"export interface AuthServices {",
" auth: Auth;",
" db: unknown;",
" driver: { close(): Promise<void> };",
" handler: (req: Request) => Promise<Response>;",
"}",
""
].join("\n");
mkdirSync(dirname(targetPath), { recursive: true });
writeFileIfChanged(targetPath, content);
}
async function resolveContractSource(opts) {
if (opts.key === "api") {
const localPath = opts.source && "localPath" in opts.source ? opts.source.localPath : void 0;
if (localPath != null && localPath !== "") return {
key: opts.key,
importName: "BaseApiContract",
sourceFilePath: join(localPath, "src", "contract.ts")
};
if (!opts.baseUrl) return localApiContractSource(opts.configDir);
}
if (opts.key === "auth" && opts.localSourceFactory) {
const localPath = opts.source && "localPath" in opts.source ? opts.source.localPath : void 0;
if (localPath != null && localPath !== "") return {
key: opts.key,
importName: "authContract",
sourceFilePath: join(localPath, "src", "contract.ts")
};
if (!opts.baseUrl) return opts.localSourceFactory(opts.configDir);
}
if (opts.source && "localPath" in opts.source && opts.source.localPath != null && opts.source.localPath !== "") return {
key: opts.key,
importName: `${sanitizeIdentifier(opts.key)}Contract`,
sourceFilePath: join(opts.source.localPath, "src", "contract.ts")
};
return remoteContractSource({
configDir: opts.configDir,
runtimeDir: opts.runtimeDir,
name: opts.key,
baseUrl: opts.baseUrl,
generatedSubdir: opts.generatedSubdir
});
}
function writeGeneratedFiles(opts) {
const baseSource = opts.sources.find((source) => source.key === "api");
const pluginSources = opts.pluginKeys.map((key) => opts.sources.find((entry) => entry.key === key)).filter((source) => Boolean(source));
if (!baseSource) throw new Error("API contract source is required to generate the aggregate contract");
const uiContractPath = join(opts.configDir, "ui", "src", "lib", "api-types.gen.ts");
const uiLines = [];
for (const source of opts.sources) {
const importPath = toImportPath(uiContractPath, source.sourceFilePath);
uiLines.push(`import type { ContractType as ${source.importName} } from "${importPath}";`);
}
uiLines.push("");
const compositeParts = [];
if (opts.authSource) compositeParts.push(`auth: ${opts.authSource.importName}`);
for (const source of pluginSources) {
const key = /^[$A-Z_][0-9A-Z_$]*$/i.test(source.key) ? source.key : JSON.stringify(source.key);
compositeParts.push(`${key}: ${source.importName}`);
}
if (compositeParts.length === 0) uiLines.push(`export type ApiContract = ${baseSource.importName};`);
else {
uiLines.push(`export type ApiContract = ${baseSource.importName} & {`);
for (const part of compositeParts) uiLines.push(` ${part};`);
uiLines.push("};");
}
mkdirSync(dirname(uiContractPath), { recursive: true });
writeFileIfChanged(uiContractPath, `${uiLines.join("\n")}\n`);
const pluginsClientPath = join(opts.configDir, "api", "src", "lib", "plugins-types.gen.ts");
const pluginsClientLines = [];
for (const source of pluginSources) {
const importPath = toImportPath(pluginsClientPath, source.sourceFilePath);
pluginsClientLines.push(`import type { ContractType as ${source.importName} } from "${importPath}";`);
}
if (opts.authSource) {
const authImportPath = toImportPath(pluginsClientPath, opts.authSource.sourceFilePath);
pluginsClientLines.push(`import type { ContractType as ${opts.authSource.importName} } from "${authImportPath}";`);
}
pluginsClientLines.push("import type { ContractRouterClient, AnyContractRouter } from \"@orpc/contract\";");
pluginsClientLines.push("type ClientFactory<C extends AnyContractRouter> = (context?: Record<string, unknown>) => ContractRouterClient<C>;");
pluginsClientLines.push("");
const allPluginSources = [...pluginSources];
if (opts.authSource) allPluginSources.push({
...opts.authSource,
key: "auth"
});
if (allPluginSources.length === 0) pluginsClientLines.push("export type PluginsClient = Record<string, never>;");
else {
pluginsClientLines.push("export type PluginsClient = {");
for (const source of allPluginSources) {
const key = /^[$A-Z_][0-9A-Z_$]*$/i.test(source.key) ? source.key : JSON.stringify(source.key);
pluginsClientLines.push(` ${key}: ClientFactory<${source.importName}>;`);
}
pluginsClientLines.push("};");
}
mkdirSync(dirname(pluginsClientPath), { recursive: true });
writeFileIfChanged(pluginsClientPath, `${pluginsClientLines.join("\n")}\n`);
const authTypeTargets = [join(opts.configDir, "ui", "src", "lib", "auth-types.gen.ts")];
const apiLibDir = join(opts.configDir, "api", "src", "lib");
if (existsSync(apiLibDir)) authTypeTargets.push(join(apiLibDir, "auth-types.gen.ts"));
const hostLibDir = join(opts.configDir, "host", "src", "lib");
if (existsSync(join(opts.configDir, "host", "src"))) authTypeTargets.push(join(hostLibDir, "auth-types.gen.ts"));
if (opts.authExportPath) for (const authTypesPath of authTypeTargets) writeAuthTypesGen(authTypesPath, opts.authExportPath);
else if (opts.authSource) for (const authTypesPath of authTypeTargets) writeFallbackAuthTypesGen(authTypesPath);
return uiContractPath;
}
async function syncApiContractBridge(opts) {
const runtimeDir = join(opts.configDir, ".bos", "generated");
const pluginEntries = Object.entries(opts.runtimeConfig.plugins ?? {}).sort(([a], [b]) => a.localeCompare(b));
const sources = [];
let manifest = null;
let generatedPath = null;
let authSource = null;
let authExportPath = null;
const baseSource = await resolveContractSource({
configDir: opts.configDir,
runtimeDir,
key: "api",
source: opts.runtimeConfig.api,
baseUrl: opts.apiBaseUrl,
generatedSubdir: "api"
});
sources.push(baseSource);
if (opts.runtimeConfig.auth) {
authSource = await resolveContractSource({
configDir: opts.configDir,
runtimeDir,
key: "auth",
source: opts.runtimeConfig.auth,
baseUrl: opts.runtimeConfig.auth.url,
generatedSubdir: "auth",
localSourceFactory: localAuthContractSource
});
sources.push(authSource);
if (authSource.generatedPath) generatedPath = authSource.generatedPath;
if (opts.runtimeConfig.auth.url && opts.runtimeConfig.auth.source !== "local") try {
const authManifest = await fetchApiPluginManifest(opts.runtimeConfig.auth.url);
const fetchedAuthExportPath = await fetchAuthExportTypes({
baseUrl: opts.runtimeConfig.auth.url,
runtimeDir,
manifest: authManifest
});
if (fetchedAuthExportPath) authExportPath = fetchedAuthExportPath;
} catch (error) {
console.warn(`[API Contract] Failed to fetch auth additional exports: ${error instanceof Error ? error.message : String(error)}`);
}
if (!authExportPath) {
const localAuthExport = join(opts.configDir, "plugins", "auth", "src", "auth-export.ts");
if (existsSync(localAuthExport)) authExportPath = localAuthExport;
else {
const generatedAuthExport = join(runtimeDir, "auth", "auth-export.d.ts");
if (existsSync(generatedAuthExport)) authExportPath = generatedAuthExport;
}
}
}
for (const [key, plugin] of pluginEntries) {
if (!plugin.url && !plugin.localPath) {
console.warn(`[API Contract] Skipping plugin "${key}" — no URL resolved (local path missing and no production URL configured)`);
continue;
}
const source = await resolveContractSource({
configDir: opts.configDir,
runtimeDir,
key,
source: plugin,
baseUrl: plugin.url,
generatedSubdir: `plugins/${key}`
});
sources.push(source);
if (source.generatedPath) generatedPath = source.generatedPath;
}
const allPluginKeys = pluginEntries.map(([key]) => key);
writeGeneratedFiles({
configDir: opts.configDir,
sources,
pluginKeys: allPluginKeys,
authSource,
authExportPath
});
if (opts.runtimeConfig.api.source !== "local") manifest = await fetchApiPluginManifest(opts.apiBaseUrl);
return {
bridgePath: join(opts.configDir, "ui", "src", "lib", "api-types.gen.ts"),
generatedPath,
manifest,
source: opts.runtimeConfig.api.source
};
}
//#endregion
export { syncApiContractBridge };
//# sourceMappingURL=api-contract.mjs.map