UNPKG

everything-dev

Version:

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

578 lines (576 loc) • 24.8 kB
import { buildAuthExportStub, buildAuthTypesGenContent } from "./auth-types-gen.mjs"; import { fetchJsonOrNull, fetchResponse } from "./http-client.mjs"; import { isAuthMirrorPluginEntry } from "./service-descriptor.mjs"; import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { dirname, join, relative } from "node:path"; import { Context, Effect, Layer, ManagedRuntime, Schema } from "effect"; import { createHash } from "node:crypto"; //#region src/api-contract.ts var ApiManifestFetchError = class extends Schema.TaggedError()("ApiManifestFetchError", { url: Schema.String, message: Schema.String }) {}; var ApiManifestFormatError = class extends Schema.TaggedError()("ApiManifestFormatError", { url: Schema.String, message: Schema.String }) {}; var MissingContractTypesError = class extends Schema.TaggedError()("MissingContractTypesError", { pluginName: Schema.String, message: Schema.String }) {}; var ContractTypesFetchError = class extends Schema.TaggedError()("ContractTypesFetchError", { url: Schema.String, message: Schema.String }) {}; var ContractTypesChecksumError = class extends Schema.TaggedError()("ContractTypesChecksumError", { url: Schema.String, message: Schema.String }) {}; var AuthExportFetchError = class extends Schema.TaggedError()("AuthExportFetchError", { url: Schema.String, message: Schema.String }) {}; var ApiContractResolver = class ApiContractResolver extends Context.Service()("everything-dev/api-contract/ApiContractResolver") { static layer = Layer.effect(ApiContractResolver, Effect.sync(() => { const manifest = Effect.fn("ApiContractResolver.manifest")(function* (apiBaseUrl) { const url = getApiPluginManifestUrl(apiBaseUrl); const fetched = yield* Effect.tryPromise({ try: () => fetchJsonOrNull(url, { retries: 2 }), catch: () => new ApiManifestFetchError({ url, message: `Failed to fetch API plugin manifest from ${url}` }) }); if (!fetched) return yield* new ApiManifestFetchError({ url, message: `Failed to fetch API plugin manifest from ${url}` }); if (fetched.schemaVersion !== 1 || fetched.kind !== "every-plugin/manifest") return yield* new ApiManifestFormatError({ url, message: "Unsupported API plugin manifest format" }); return fetched; }); const writeContractTypes = Effect.fn("ApiContractResolver.writeContractTypes")(function* (opts) { const contractResponse = yield* Effect.tryPromise({ try: () => fetchResponse(opts.contractUrl), catch: () => new ContractTypesFetchError({ url: opts.contractUrl, message: `Failed to fetch contract types from ${opts.contractUrl}` }) }); if (!contractResponse.ok) return yield* new ContractTypesFetchError({ url: opts.contractUrl, message: `Failed to fetch contract types from ${opts.contractUrl}: ${contractResponse.status} ${contractResponse.statusText}` }); const contractTypes = yield* Effect.tryPromise({ try: () => contractResponse.text(), catch: () => new ContractTypesFetchError({ url: opts.contractUrl, message: `Failed to fetch contract types from ${opts.contractUrl}` }) }); if (opts.manifest.contract?.types.sha256 && opts.manifest.contract.types.sha256 !== sha256(contractTypes)) return yield* new ContractTypesChecksumError({ url: opts.contractUrl, message: "Fetched contract types failed checksum verification" }); const generatedPath = join(opts.runtimeDir, opts.generatedSubdir, "contract.d.ts"); yield* Effect.sync(() => { mkdirSync(dirname(generatedPath), { recursive: true }); writeFileIfChanged(generatedPath, contractTypes); }); return { key: opts.name, importName: `${sanitizeIdentifier(opts.name)}Contract`, sourceFilePath: generatedPath, generatedPath }; }); const contractSource = Effect.fn("ApiContractResolver.contractSource")(function* (opts) { const fetchedManifest = yield* manifest(opts.baseUrl); if (!fetchedManifest.contract) return yield* new MissingContractTypesError({ pluginName: fetchedManifest.plugin.name, message: `Plugin manifest for ${fetchedManifest.plugin.name} does not advertise contract types` }); const contractUrl = `${trimTrailingSlash(opts.baseUrl)}/${fetchedManifest.contract.types.path.replace(/^\.\//, "")}`; return yield* writeContractTypes({ contractUrl, manifest: fetchedManifest, runtimeDir: opts.runtimeDir, generatedSubdir: opts.generatedSubdir, name: opts.name }); }); const authExportTypes = Effect.fn("ApiContractResolver.authExportTypes")(function* (opts) { const entry = (opts.manifest.additionalExports ?? []).find((candidate) => candidate.path.includes("auth-export") || candidate.path.endsWith("auth-export.d.ts")); if (!entry) return null; const exportUrl = `${trimTrailingSlash(opts.baseUrl)}/${entry.path.replace(/^\.\//, "")}`; const response = yield* Effect.tryPromise({ try: () => fetchResponse(exportUrl), catch: () => new AuthExportFetchError({ url: exportUrl, message: `Failed to fetch auth export types from ${exportUrl}` }) }); if (!response.ok) return yield* new AuthExportFetchError({ url: exportUrl, message: `Failed to fetch auth export types from ${exportUrl}: ${response.status}` }); const content = yield* Effect.tryPromise({ try: () => response.text(), catch: () => new AuthExportFetchError({ url: exportUrl, message: `Failed to fetch auth export types from ${exportUrl}` }) }); if (entry.sha256 && entry.sha256 !== sha256(content)) return yield* new AuthExportFetchError({ url: exportUrl, message: `Auth export types checksum mismatch for ${exportUrl}` }); const generatedPath = join(opts.runtimeDir, "auth", "auth-export.d.ts"); yield* Effect.sync(() => { mkdirSync(dirname(generatedPath), { recursive: true }); writeFileIfChanged(generatedPath, content); }); return generatedPath; }); return ApiContractResolver.of({ manifest, contractSource, authExportTypes }); })); }; let resolverRuntime = null; function getResolver() { if (!resolverRuntime) resolverRuntime = ManagedRuntime.make(ApiContractResolver.layer); return resolverRuntime; } 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 fetchApiPluginManifest(apiBaseUrl) { return getResolver().runPromise(Effect.gen(function* () { return yield* (yield* ApiContractResolver).manifest(apiBaseUrl); })).catch((error) => { throw new Error(error.message); }); } function localApiContractSource(configDir) { const sourcePath = join(configDir, "api", "src", "contract.ts"); if (!existsSync(sourcePath)) throw new Error(`local api contract not found at api/src/contract.ts — restore the file or configure a reachable api URL`); return { key: "api", importName: "BaseApiContract", sourceFilePath: sourcePath }; } function localAuthContractSource(configDir) { const sourcePath = join(configDir, "plugins", "auth", "src", "contract.ts"); if (!existsSync(sourcePath)) throw new Error(`local auth contract not found at plugins/auth/src/contract.ts — restore the file or configure a reachable auth URL`); return { key: "auth", importName: "authContract", sourceFilePath: sourcePath }; } async function remoteContractSource(opts) { return getResolver().runPromise(Effect.gen(function* () { return yield* (yield* ApiContractResolver).contractSource({ baseUrl: opts.baseUrl, runtimeDir: opts.runtimeDir, name: opts.name, generatedSubdir: opts.generatedSubdir }); })).catch((error) => { throw new Error(error.message); }); } async function fetchAuthExportTypes(opts) { return getResolver().runPromise(Effect.gen(function* () { return yield* (yield* ApiContractResolver).authExportTypes({ baseUrl: opts.baseUrl, runtimeDir: opts.runtimeDir, manifest: opts.manifest }); })).catch((error) => { console.warn(`[API Contract] ${error.message}`); return null; }); } 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 writePluginClientGen(opts) { const pluginSrcDir = join(opts.localPath, "src"); if (!existsSync(pluginSrcDir)) return; const targetPath = join(pluginSrcDir, "lib", "plugins-client.gen.ts"); const lines = []; for (const source of opts.depSources) { const importPath = toImportPath(targetPath, source.sourceFilePath); lines.push(`import type { ContractType as ${source.importName} } from "${importPath}";`); } lines.push("import type { RouterContractClient, RouterContract } from \"@orpc/contract\";"); lines.push("import type { ContractedRouter } from \"@orpc/server\";"); lines.push("type PluginClientEntry<C extends RouterContract> = {\n client: (context?: Record<string, unknown>) => RouterContractClient<C>;\n router: ContractedRouter<C, any>;\n};"); lines.push(""); if (opts.depSources.length === 0) lines.push("export type PluginsClient = Record<string, never>;"); else { lines.push("export type PluginsClient = {"); for (const source of opts.depSources) { const key = /^[$A-Z_][0-9A-Z_$]*$/i.test(source.key) ? source.key : JSON.stringify(source.key); lines.push(` ${key}: PluginClientEntry<${source.importName}>;`); } lines.push("};"); } mkdirSync(dirname(targetPath), { recursive: true }); writeFileIfChanged(targetPath, `${lines.join("\n")}\n`); } function writeGeneratedFiles(opts) { const hasLocalApiWorkspace = existsSync(join(opts.configDir, "api", "src")); 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`); if (hasLocalApiWorkspace) { const pluginsClientPath = join(opts.configDir, "api", "src", "lib", "plugins-types.gen.ts"); const pluginsClientLines = []; const allPluginSources = [...pluginSources]; if (opts.authSource) allPluginSources.push({ ...opts.authSource, key: "auth" }); const apiDepSources = opts.apiDependsOn?.length ? allPluginSources.filter((s) => opts.apiDependsOn.includes(s.key)) : allPluginSources; const unresolvedDepKeys = opts.apiDependsOn?.filter((depKey) => !allPluginSources.some((s) => s.key === depKey)) ?? []; for (const source of apiDepSources) { const importPath = toImportPath(pluginsClientPath, source.sourceFilePath); pluginsClientLines.push(`import type { ContractType as ${source.importName} } from "${importPath}";`); } pluginsClientLines.push("import type { RouterContractClient, RouterContract } from \"@orpc/contract\";"); pluginsClientLines.push("import type { ContractedRouter } from \"@orpc/server\";"); pluginsClientLines.push("type PluginClientEntry<C extends RouterContract> = {\n client: (context?: Record<string, unknown>) => RouterContractClient<C>;\n router: ContractedRouter<C, any>;\n};"); pluginsClientLines.push(""); if (apiDepSources.length === 0 && unresolvedDepKeys.length === 0) pluginsClientLines.push("export type PluginsClient = Record<string, never>;"); else { pluginsClientLines.push("export type PluginsClient = {"); for (const source of apiDepSources) { const key = /^[$A-Z_][0-9A-Z_$]*$/i.test(source.key) ? source.key : JSON.stringify(source.key); pluginsClientLines.push(` ${key}: PluginClientEntry<${source.importName}>;`); } for (const key of unresolvedDepKeys) { const keyStr = /^[$A-Z_][0-9A-Z_$]*$/i.test(key) ? key : JSON.stringify(key); pluginsClientLines.push(` ${keyStr}?: PluginClientEntry<RouterContract>;`); } pluginsClientLines.push("};"); } mkdirSync(dirname(pluginsClientPath), { recursive: true }); writeFileIfChanged(pluginsClientPath, `${pluginsClientLines.join("\n")}\n`); } const allSourcesForLookup = [...pluginSources]; if (opts.authSource) allSourcesForLookup.push({ ...opts.authSource, key: "auth" }); for (const pluginKey of opts.pluginKeys) { const localPath = opts.pluginLocalPaths?.[pluginKey]; if (!localPath) continue; const depSources = (opts.pluginDependsOn?.[pluginKey] ?? []).map((depKey) => allSourcesForLookup.find((s) => s.key === depKey)).filter((s) => Boolean(s)); writePluginClientGen({ configDir: opts.configDir, pluginKey, depSources, localPath }); } if (opts.authLocalPath) { const depSources = (opts.authDependsOn ?? []).map((depKey) => allSourcesForLookup.find((s) => s.key === depKey)).filter((s) => Boolean(s)); writePluginClientGen({ configDir: opts.configDir, pluginKey: "auth", depSources, localPath: opts.authLocalPath }); } 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")); for (const key of opts.pluginKeys) { const localPath = opts.pluginLocalPaths?.[key]; if (!localPath) continue; const pluginSrcDir = join(localPath, "src"); if (existsSync(pluginSrcDir)) authTypeTargets.push(join(pluginSrcDir, "lib", "auth-types.gen.ts")); } if (opts.authExportPath) for (const authTypesPath of authTypeTargets) { const exportImportPath = toImportPath(authTypesPath, opts.authExportPath); const contractImportPath = toImportPath(authTypesPath, join(dirname(opts.authExportPath), "contract.d.ts")); mkdirSync(dirname(authTypesPath), { recursive: true }); writeFileIfChanged(authTypesPath, buildAuthTypesGenContent(exportImportPath, contractImportPath)); } else if (opts.authSource) { const generatedAuthExportPath = join(opts.configDir, ".bos", "generated", "auth", "auth-export.d.ts"); mkdirSync(dirname(generatedAuthExportPath), { recursive: true }); if (!existsSync(generatedAuthExportPath)) writeFileIfChanged(generatedAuthExportPath, buildAuthExportStub()); for (const authTypesPath of authTypeTargets) { const exportImportPath = toImportPath(authTypesPath, generatedAuthExportPath); const contractImportPath = toImportPath(authTypesPath, join(opts.configDir, ".bos", "generated", "auth", "contract.d.ts")); mkdirSync(dirname(authTypesPath), { recursive: true }); writeFileIfChanged(authTypesPath, buildAuthTypesGenContent(exportImportPath, contractImportPath)); } } const localPluginKeys = new Set(Object.keys(opts.pluginLocalPaths ?? {})); if (opts.authLocalPath) localPluginKeys.add("auth"); const pluginsRoot = join(opts.configDir, "plugins"); if (existsSync(pluginsRoot)) for (const entry of readdirSync(pluginsRoot, { withFileTypes: true })) { if (!entry.isDirectory() || entry.name.startsWith("_") || localPluginKeys.has(entry.name)) continue; for (const genName of ["plugins-client.gen.ts", "auth-types.gen.ts"]) { const genPath = join(pluginsRoot, entry.name, "src", "lib", genName); if (existsSync(genPath)) rmSync(genPath); } } const generatedPluginsRoot = join(opts.configDir, ".bos", "generated", "plugins"); if (existsSync(generatedPluginsRoot)) { const configuredKeys = new Set(opts.sources.map((source) => source.key)); for (const entry of readdirSync(generatedPluginsRoot, { withFileTypes: true })) { if (!entry.isDirectory() || configuredKeys.has(entry.name)) continue; rmSync(join(generatedPluginsRoot, entry.name), { recursive: true, force: true }); } } return uiContractPath; } async function syncApiContractBridge(opts) { const runtimeDir = join(opts.configDir, ".bos", "generated"); const isAuthMirrorEntry = (key, plugin) => isAuthMirrorPluginEntry(opts.runtimeConfig.auth, key, plugin); const pluginEntries = Object.entries(opts.runtimeConfig.plugins ?? {}).filter(([key, plugin]) => !isAuthMirrorEntry(key, plugin)).sort(([a], [b]) => a.localeCompare(b)); const sources = []; const status = []; let manifest = null; let generatedPath = null; let authSource = null; let authExportPath = null; const excludedPluginKeys = /* @__PURE__ */ new Set(); try { const baseSource = await resolveContractSource({ configDir: opts.configDir, runtimeDir, key: "api", source: opts.runtimeConfig.api, baseUrl: opts.apiBaseUrl, generatedSubdir: "api" }); sources.push(baseSource); status.push({ key: "api", source: opts.runtimeConfig.api.source, url: opts.runtimeConfig.api.source !== "local" ? opts.apiBaseUrl : void 0, localPath: opts.runtimeConfig.api.source === "local" ? opts.runtimeConfig.api.localPath : void 0 }); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.warn(`[API Contract] Failed to resolve api contract: ${message}`); status.push({ key: "api", source: "failed", url: opts.apiBaseUrl || void 0, error: message }); } if (opts.runtimeConfig.auth) try { 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); status.push({ key: "auth", source: opts.runtimeConfig.auth.source, url: opts.runtimeConfig.auth.source !== "local" ? opts.runtimeConfig.auth.url : void 0, localPath: opts.runtimeConfig.auth.source === "local" ? opts.runtimeConfig.auth.localPath : void 0 }); 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; } } } catch (error) { const message = error instanceof Error ? error.message : String(error); console.warn(`[API Contract] Failed to resolve auth contract: ${message}`); status.push({ key: "auth", source: "failed", url: opts.runtimeConfig.auth.url || void 0, error: message }); } 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)`); status.push({ key, source: "skipped" }); excludedPluginKeys.add(key); } const resolvablePlugins = pluginEntries.filter(([key]) => !excludedPluginKeys.has(key)); (await Promise.allSettled(resolvablePlugins.map(async ([key, plugin]) => { return { key, source: await resolveContractSource({ configDir: opts.configDir, runtimeDir, key, source: plugin, baseUrl: plugin.url, generatedSubdir: `plugins/${key}` }), plugin }; }))).forEach((result, index) => { const [key, plugin] = resolvablePlugins[index]; if (result.status === "fulfilled") { sources.push(result.value.source); status.push({ key, source: plugin.source, url: plugin.source !== "local" ? plugin.url : void 0, localPath: plugin.source === "local" ? plugin.localPath : void 0 }); if (result.value.source.generatedPath) generatedPath = result.value.source.generatedPath; } else { const message = result.reason instanceof Error ? result.reason.message : String(result.reason); console.warn(`[API Contract] Failed to resolve plugin "${key}": ${message}`); status.push({ key, source: "failed", url: plugin.url || void 0, error: message }); excludedPluginKeys.add(key); } }); const apiStatus = status.find((s) => s.key === "api"); if (apiStatus?.source === "failed") throw new Error(`Cannot generate contract types without api contract: ${apiStatus.error ?? "unknown error"}`); const allPluginKeys = pluginEntries.filter(([key]) => !excludedPluginKeys.has(key)).map(([key]) => key); const pluginDependsOn = {}; const pluginLocalPaths = {}; for (const [key, plugin] of pluginEntries) { if (excludedPluginKeys.has(key)) continue; if (plugin.dependsOn?.length) pluginDependsOn[key] = plugin.dependsOn; if (plugin.localPath) pluginLocalPaths[key] = plugin.localPath; } writeGeneratedFiles({ configDir: opts.configDir, sources, pluginKeys: allPluginKeys, authSource, authExportPath, apiDependsOn: opts.runtimeConfig.api.dependsOn, pluginDependsOn, pluginLocalPaths, authLocalPath: opts.runtimeConfig.auth?.source === "local" ? opts.runtimeConfig.auth.localPath : void 0, authDependsOn: opts.runtimeConfig.auth?.dependsOn }); 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, status }; } //#endregion export { ApiContractResolver, ApiManifestFetchError, ApiManifestFormatError, AuthExportFetchError, ContractTypesChecksumError, ContractTypesFetchError, MissingContractTypesError, fetchApiPluginManifest, syncApiContractBridge, writeGeneratedFiles }; //# sourceMappingURL=api-contract.mjs.map