UNPKG

every-plugin

Version:
249 lines (217 loc) 8.07 kB
import type { PluginInfo } from "./utils"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", "Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization", }; const applyCorsHeaders = (res: any) => { Object.entries(corsHeaders).forEach(([key, value]) => { res.setHeader(key, value); }); }; const normalizePrefix = (prefix?: string): string => { if (!prefix) return ""; const cleaned = prefix.replace(/^\/+|\/+$/g, ""); return cleaned ? `/${cleaned}` : ""; }; export function setupPluginMiddleware( devServer: any, pluginInfo: PluginInfo, devConfig: any, port: number, ) { const rpcPrefix = normalizePrefix(devConfig?.prefix); const handlers: { rpc: any; api: any } = { rpc: null, api: null }; let cleanup: (() => Promise<void>) | null = null; const performCleanup = async () => { if (cleanup) { await cleanup(); cleanup = null; } }; (async () => { await performCleanup(); try { const { createPluginRuntime } = await import("every-plugin"); const { RPCHandler } = await import("@orpc/server/fetch"); const { OpenAPIHandler } = await import("@orpc/openapi/fetch"); const { OpenAPIReferencePlugin } = await import("@orpc/openapi/plugins"); const { ZodToJsonSchemaConverter } = await import("@orpc/zod/zod4"); const { onError } = await import("every-plugin/orpc"); const { formatORPCError } = await import("every-plugin/errors"); const pluginId = devConfig?.pluginId || pluginInfo.normalizedName; const runtime = createPluginRuntime({ registry: { [pluginId]: { remote: `http://localhost:${port}/remoteEntry.js`, }, }, }); const defaultConfig = { variables: {}, secrets: {} }; // @ts-expect-error we don't know the plugin id const loaded = await runtime.usePlugin(pluginId, (devConfig?.config ?? defaultConfig) as any); cleanup = async () => { if (runtime) await runtime.shutdown(); handlers.rpc = null; handlers.api = null; if (devServer.app.locals.handlers) { devServer.app.locals.handlers = null; } }; handlers.rpc = new RPCHandler(loaded.router, { interceptors: [ onError((error: any) => { formatORPCError(error); }), ], }); handlers.api = new OpenAPIHandler(loaded.router, { plugins: [ new OpenAPIReferencePlugin({ schemaConverters: [new ZodToJsonSchemaConverter()], }), ], interceptors: [ onError((error: any) => { formatORPCError(error); }), ], }); console.log(`╭─────────────────────────────────────────────`); console.log(`│ ✅ Plugin dev server ready: `); console.log(`├─────────────────────────────────────────────`); console.log(`│ 📡 RPC: http://localhost:${port}/api/rpc${rpcPrefix}`); console.log(`│ 📖 Docs: http://localhost:${port}/api`); console.log(`│ 💚 Health: http://localhost:${port}/`); console.log(`╰─────────────────────────────────────────────`); devServer.app.locals.handlers = handlers; if (devServer.server) { devServer.server.once("close", async () => { await performCleanup(); }); } } catch (error) { console.error("❌ Failed to load plugin:", error); await performCleanup(); } })().catch((err) => { console.error("❌ Plugin dev server fatal error:", err); }); process.once("SIGINT", async () => { const timeout = setTimeout(() => process.exit(0), 3000); await performCleanup(); clearTimeout(timeout); }); process.once("SIGTERM", async () => { const timeout = setTimeout(() => process.exit(0), 3000); await performCleanup(); clearTimeout(timeout); }); devServer.app.options("*", (_req: any, res: any) => { applyCorsHeaders(res); res.status(200).end(); }); devServer.app.get("/", (_req: any, res: any) => { applyCorsHeaders(res); res.json({ ok: true, plugin: pluginInfo.normalizedName, version: pluginInfo.version, status: devServer.app.locals.handlers?.rpc ? "ready" : "loading", endpoints: { health: "/", docs: "/api", rpc: `/api/rpc${rpcPrefix}`, }, }); }); devServer.app.get("/health", (_req: any, res: any) => { applyCorsHeaders(res); res.status(200).send("OK"); }); const buildDevContext = (_req: any, webRequest: Request) => { const rawClone = webRequest.method === "GET" || webRequest.method === "HEAD" ? null : webRequest.clone(); let cachedRawBody: string | null = null; return { reqHeaders: webRequest.headers, getRawBody: async (): Promise<string> => { if (cachedRawBody !== null) return cachedRawBody; if (!rawClone) { cachedRawBody = ""; return cachedRawBody; } cachedRawBody = await rawClone.text(); return cachedRawBody; }, }; }; const handleApiRequest = async (req: any, res: any) => { applyCorsHeaders(res); const apiHandler = devServer.app.locals.handlers?.api; if (!apiHandler) { return res.status(503).json({ error: "Plugin still loading..." }); } try { const url = `http://${req.headers.host}${req.url}`; const webRequest = new Request(url, { method: req.method, headers: req.headers, body: req.method !== "GET" && req.method !== "HEAD" ? req : undefined, duplex: req.method !== "GET" && req.method !== "HEAD" ? "half" : undefined, } as RequestInit); const result = await apiHandler.handle(webRequest, { prefix: "/api", context: buildDevContext(req, webRequest), }); if (result.response) { res.status(result.response.status); result.response.headers.forEach((value: string, key: string) => { res.setHeader(key, value); }); const text = await result.response.text(); res.send(text); } else { res.status(404).send("Not Found"); } } catch (error) { console.error("OpenAPI error:", error); res.status(500).json({ error: (error as Error).message }); } }; devServer.app.all(`/api/rpc${rpcPrefix}/*`, async (req: any, res: any) => { applyCorsHeaders(res); const rpcHandler = devServer.app.locals.handlers?.rpc; if (!rpcHandler) { return res.status(503).json({ error: "Plugin still loading..." }); } try { const url = `http://${req.headers.host}${req.url}`; const webRequest = new Request(url, { method: req.method, headers: req.headers, body: req.method !== "GET" && req.method !== "HEAD" ? req : undefined, duplex: req.method !== "GET" && req.method !== "HEAD" ? "half" : undefined, } as RequestInit); const result = await rpcHandler.handle(webRequest, { prefix: `/api/rpc${rpcPrefix}`, context: buildDevContext(req, webRequest), }); if (result.response) { res.status(result.response.status); result.response.headers.forEach((value: string, key: string) => { res.setHeader(key, value); }); const text = await result.response.text(); res.send(text); } else { res.status(404).send("Not Found"); } } catch (error) { console.error("RPC error:", error); res.status(500).json({ error: (error as Error).message }); } }); devServer.app.all("/api", handleApiRequest); devServer.app.all("/api/*", handleApiRequest); }