diff --git a/apps/dev-playground/client/vite.config.ts b/apps/dev-playground/client/vite.config.ts index 4599089..c9d75ff 100644 --- a/apps/dev-playground/client/vite.config.ts +++ b/apps/dev-playground/client/vite.config.ts @@ -1,5 +1,4 @@ import path from "node:path"; -import { appKitTypesPlugin } from "@databricks/app-kit"; import { tanstackRouter } from "@tanstack/router-plugin/vite"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; @@ -8,7 +7,6 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [ react(), - appKitTypesPlugin(), tanstackRouter({ target: "react", autoCodeSplitting: process.env.NODE_ENV !== "development", diff --git a/apps/dev-playground/server/reconnect-plugin.ts b/apps/dev-playground/server/reconnect-plugin.ts index 290d747..3c8d8b8 100644 --- a/apps/dev-playground/server/reconnect-plugin.ts +++ b/apps/dev-playground/server/reconnect-plugin.ts @@ -19,6 +19,7 @@ export class ReconnectPlugin extends Plugin { injectRoutes(router: IAppRouter): void { this.route(router, { + name: "reconnect", method: "get", path: "/", handler: async (_req, res) => { @@ -27,6 +28,7 @@ export class ReconnectPlugin extends Plugin { }); this.route(router, { + name: "stream", method: "get", path: "/stream", handler: async (req, res) => { diff --git a/apps/dev-playground/server/telemetry-example-plugin.ts b/apps/dev-playground/server/telemetry-example-plugin.ts index 271ef64..9f31b1a 100644 --- a/apps/dev-playground/server/telemetry-example-plugin.ts +++ b/apps/dev-playground/server/telemetry-example-plugin.ts @@ -41,118 +41,125 @@ class TelemetryExamples extends Plugin { } private registerTelemetryExampleRoutes(router: Router) { - router.post("/combined", async (req: Request, res: Response) => { - const startTime = Date.now(); - - return this.telemetry.startActiveSpan( - "combined-example", - { - attributes: { - "example.type": "combined", - "example.version": "v2", + this.route(router, { + name: "combined", + method: "post", + path: "/combined", + handler: async (req: Request, res: Response) => { + const startTime = Date.now(); + + return this.telemetry.startActiveSpan( + "combined-example", + { + attributes: { + "example.type": "combined", + "example.version": "v2", + }, }, - }, - async (span: Span) => { - try { - const userId = - req.body?.userId || req.query.userId || "demo-user-123"; - - this.telemetry.emit({ - severityNumber: SeverityNumber.INFO, - severityText: "INFO", - body: "Processing telemetry example request", - attributes: { - "user.id": userId, - "request.type": "combined-example", - }, - }); - - const result = await this.complexOperation(userId); - - const duration = Date.now() - startTime; - this.requestCounter.add(1, { status: "success" }); - this.durationHistogram.record(duration); - - this.telemetry.emit({ - severityNumber: SeverityNumber.INFO, - severityText: "INFO", - body: "Request completed successfully", - attributes: { - "user.id": userId, - "duration.ms": duration, - "result.fields": Object.keys(result).length, - }, - }); - - span.setStatus({ code: SpanStatusCode.OK }); - - res.json({ - success: true, - result, - duration_ms: duration, - tracing: { - hint: "Open Grafana at http://localhost:3000", - services: [ - "app-template (main service)", - "user-operations (complex operation)", - "auth-validation (user validation)", - "data-access (database operations - with cache!)", - "auth-service (permissions)", - "external-api (external HTTP calls)", - "data-processing (transformation)", - ], - expectedSpans: [ - "HTTP POST (SDK auto-instrumentation)", - "combined-example (custom tracer: custom-telemetry-example)", - " └─ complex-operation (custom tracer: user-operations)", - " ├─ validate-user (100ms, custom tracer: auth-validation)", - " ├─ fetch-user-data (200ms first call / cached on repeat, custom tracer: data-access) [parallel]", - " │ └─ cache.hit attribute set by SDK (false on first call, true on repeat)", - " ├─ fetch-external-resource (custom tracer: external-api) [parallel]", - " │ └─ HTTP GET https://example.com (SDK auto-instrumentation)", - " ├─ fetch-permissions (150ms, custom tracer: auth-service) [parallel]", - " └─ transform-data (80ms, custom tracer: data-processing)", - ], - }, - metrics: { - recorded: ["app.requests.total", "app.request.duration"], - }, - logs: { - emitted: [ - "Starting complex operation workflow", - "Data fetching completed successfully", - "Data transformation completed", - "Permissions retrieved", - "External API call completed", - ], - }, - }); - } catch (error) { - span.recordException(error as Error); - span.setStatus({ code: SpanStatusCode.ERROR }); - this.requestCounter.add(1, { status: "error" }); - - this.telemetry.emit({ - severityNumber: SeverityNumber.ERROR, - severityText: "ERROR", - body: error instanceof Error ? error.message : "Unknown error", - attributes: { - "error.type": error instanceof Error ? error.name : "Unknown", - "error.stack": error instanceof Error ? error.stack : undefined, - "request.path": req.path, - }, - }); - - res.status(500).json({ - error: true, - message: error instanceof Error ? error.message : "Unknown error", - }); - } finally { - span.end(); - } - }, - { name: "custom-telemetry-example" }, - ); + async (span: Span) => { + try { + const userId = + req.body?.userId || req.query.userId || "demo-user-123"; + + this.telemetry.emit({ + severityNumber: SeverityNumber.INFO, + severityText: "INFO", + body: "Processing telemetry example request", + attributes: { + "user.id": userId, + "request.type": "combined-example", + }, + }); + + const result = await this.complexOperation(userId); + + const duration = Date.now() - startTime; + this.requestCounter.add(1, { status: "success" }); + this.durationHistogram.record(duration); + + this.telemetry.emit({ + severityNumber: SeverityNumber.INFO, + severityText: "INFO", + body: "Request completed successfully", + attributes: { + "user.id": userId, + "duration.ms": duration, + "result.fields": Object.keys(result).length, + }, + }); + + span.setStatus({ code: SpanStatusCode.OK }); + + res.json({ + success: true, + result, + duration_ms: duration, + tracing: { + hint: "Open Grafana at http://localhost:3000", + services: [ + "app-template (main service)", + "user-operations (complex operation)", + "auth-validation (user validation)", + "data-access (database operations - with cache!)", + "auth-service (permissions)", + "external-api (external HTTP calls)", + "data-processing (transformation)", + ], + expectedSpans: [ + "HTTP POST (SDK auto-instrumentation)", + "combined-example (custom tracer: custom-telemetry-example)", + " └─ complex-operation (custom tracer: user-operations)", + " ├─ validate-user (100ms, custom tracer: auth-validation)", + " ├─ fetch-user-data (200ms first call / cached on repeat, custom tracer: data-access) [parallel]", + " │ └─ cache.hit attribute set by SDK (false on first call, true on repeat)", + " ├─ fetch-external-resource (custom tracer: external-api) [parallel]", + " │ └─ HTTP GET https://example.com (SDK auto-instrumentation)", + " ├─ fetch-permissions (150ms, custom tracer: auth-service) [parallel]", + " └─ transform-data (80ms, custom tracer: data-processing)", + ], + }, + metrics: { + recorded: ["app.requests.total", "app.request.duration"], + }, + logs: { + emitted: [ + "Starting complex operation workflow", + "Data fetching completed successfully", + "Data transformation completed", + "Permissions retrieved", + "External API call completed", + ], + }, + }); + } catch (error) { + span.recordException(error as Error); + span.setStatus({ code: SpanStatusCode.ERROR }); + this.requestCounter.add(1, { status: "error" }); + + this.telemetry.emit({ + severityNumber: SeverityNumber.ERROR, + severityText: "ERROR", + body: error instanceof Error ? error.message : "Unknown error", + attributes: { + "error.type": error instanceof Error ? error.name : "Unknown", + "error.stack": + error instanceof Error ? error.stack : undefined, + "request.path": req.path, + }, + }); + + res.status(500).json({ + error: true, + message: + error instanceof Error ? error.message : "Unknown error", + }); + } finally { + span.end(); + } + }, + { name: "custom-telemetry-example" }, + ); + }, }); } diff --git a/packages/app-kit/src/analytics/analytics.ts b/packages/app-kit/src/analytics/analytics.ts index b700ff3..81d4b12 100644 --- a/packages/app-kit/src/analytics/analytics.ts +++ b/packages/app-kit/src/analytics/analytics.ts @@ -42,6 +42,7 @@ export class AnalyticsPlugin extends Plugin { injectRoutes(router: IAppRouter) { this.route(router, { + name: "arrow", method: "get", path: "/arrow-result/:jobId", handler: async (req: Request, res: Response) => { @@ -50,6 +51,7 @@ export class AnalyticsPlugin extends Plugin { }); this.route(router, { + name: "arrowAsUser", method: "get", path: "/users/me/arrow-result/:jobId", handler: async (req: Request, res: Response) => { @@ -58,6 +60,7 @@ export class AnalyticsPlugin extends Plugin { }); this.route(router, { + name: "queryAsUser", method: "post", path: "/users/me/query/:query_key", handler: async (req: Request, res: Response) => { @@ -66,6 +69,7 @@ export class AnalyticsPlugin extends Plugin { }); this.route(router, { + name: "query", method: "post", path: "/query/:query_key", handler: async (req: Request, res: Response) => { diff --git a/packages/app-kit/src/core/tests/databricks.test.ts b/packages/app-kit/src/core/tests/databricks.test.ts index 09d7772..00e1a5b 100644 --- a/packages/app-kit/src/core/tests/databricks.test.ts +++ b/packages/app-kit/src/core/tests/databricks.test.ts @@ -53,6 +53,10 @@ class CoreTestPlugin implements BasePlugin { asUser() { return this; } + + getEndpoints() { + return {}; + } } class NormalTestPlugin implements BasePlugin { @@ -80,6 +84,10 @@ class NormalTestPlugin implements BasePlugin { asUser() { return this; } + + getEndpoints() { + return {}; + } } class DeferredTestPlugin implements BasePlugin { @@ -109,6 +117,10 @@ class DeferredTestPlugin implements BasePlugin { asUser(): any { return this; } + + getEndpoints() { + return {}; + } } class SlowSetupPlugin implements BasePlugin { @@ -133,6 +145,10 @@ class SlowSetupPlugin implements BasePlugin { asUser(): any { return this; } + + getEndpoints() { + return {}; + } } class FailingPlugin implements BasePlugin { @@ -152,6 +168,10 @@ class FailingPlugin implements BasePlugin { asUser(): any { return this; } + + getEndpoints() { + return {}; + } } describe("AppKit", () => { diff --git a/packages/app-kit/src/plugin/plugin.ts b/packages/app-kit/src/plugin/plugin.ts index df61c3b..975d894 100644 --- a/packages/app-kit/src/plugin/plugin.ts +++ b/packages/app-kit/src/plugin/plugin.ts @@ -3,6 +3,7 @@ import type { BasePlugin, BasePluginConfig, IAppResponse, + PluginEndpointMap, PluginExecuteConfig, PluginExecutionSettings, PluginPhase, @@ -44,6 +45,9 @@ export abstract class Plugin< /** If the plugin requires the Databricks client to be set in the request context */ requiresDatabricksClient = false; + /** Registered endpoints for this plugin */ + private registeredEndpoints: PluginEndpointMap = {}; + static phase: PluginPhase = "normal"; name: string; @@ -68,6 +72,10 @@ export abstract class Plugin< async setup() {} + getEndpoints(): PluginEndpointMap { + return this.registeredEndpoints; + } + abortActiveOperations(): void { this.streamManager.abortAll(); } @@ -154,13 +162,19 @@ export abstract class Plugin< } } - // TResponse is used for type generation + protected registerEndpoint(name: string, path: string): void { + this.registeredEndpoints[name] = path; + } + protected route<_TResponse>( router: express.Router, config: RouteConfig, ): void { - const { method, path, handler } = config; + const { name, method, path, handler } = config; + router[method](path, handler); + + this.registerEndpoint(name, `/api/${this.name}${path}`); } // build execution options by merging defaults, plugin config, and user overrides diff --git a/packages/app-kit/src/server/base-server.ts b/packages/app-kit/src/server/base-server.ts new file mode 100644 index 0000000..d3cb20b --- /dev/null +++ b/packages/app-kit/src/server/base-server.ts @@ -0,0 +1,27 @@ +import type express from "express"; +import { type PluginEndpoints, getConfigScript } from "./utils"; + +/** + * Base server for the App Kit. + * + * Abstract base class that provides common functionality for serving + * frontend applications. Subclasses implement specific serving strategies + * (Vite dev server, static file server, etc.). + */ +export abstract class BaseServer { + protected app: express.Application; + protected endpoints: PluginEndpoints; + + constructor(app: express.Application, endpoints: PluginEndpoints = {}) { + this.app = app; + this.endpoints = endpoints; + } + + abstract setup(): void | Promise; + + async close(): Promise {} + + protected getConfigScript(): string { + return getConfigScript(this.endpoints); + } +} diff --git a/packages/app-kit/src/server/index.ts b/packages/app-kit/src/server/index.ts index 88d1403..c786bec 100644 --- a/packages/app-kit/src/server/index.ts +++ b/packages/app-kit/src/server/index.ts @@ -10,7 +10,7 @@ import { databricksClientMiddleware } from "../utils"; import { RemoteTunnelController } from "./remote-tunnel/remote-tunnel-controller"; import { StaticServer } from "./static-server"; import type { ServerConfig } from "./types"; -import { getRoutes } from "./utils"; +import { type PluginEndpoints, getRoutes } from "./utils"; import { ViteDevServer } from "./vite-dev-server"; dotenv.config({ path: path.resolve(process.cwd(), "./.env") }); @@ -88,7 +88,7 @@ export class ServerPlugin extends Plugin { async start(): Promise { this.serverApplication.use(express.json()); - await this.extendRoutes(); + const endpoints = await this.extendRoutes(); for (const extension of this.serverExtensions) { extension(this.serverApplication); @@ -100,7 +100,7 @@ export class ServerPlugin extends Plugin { ); this.serverApplication.use(this.remoteTunnelController.middleware); - await this.setupFrontend(); + await this.setupFrontend(endpoints); const server = this.serverApplication.listen( this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port, @@ -165,13 +165,17 @@ export class ServerPlugin extends Plugin { * Setup the routes with the plugins. * * This method goes through all the plugins and injects the routes into the server application. + * Returns a map of plugin names to their registered named endpoints. */ - private async extendRoutes() { - if (!this.config.plugins) return; + private async extendRoutes(): Promise { + const endpoints: PluginEndpoints = {}; + + if (!this.config.plugins) return endpoints; this.serverApplication.get("/health", (_, res) => { res.status(200).json({ status: "ok" }); }); + this.registerEndpoint("health", "/health"); for (const plugin of Object.values(this.config.plugins)) { if (EXCLUDED_PLUGINS.includes(plugin.name)) continue; @@ -185,9 +189,15 @@ export class ServerPlugin extends Plugin { plugin.injectRoutes(router); - this.serverApplication.use(`/api/${plugin.name}`, router); + const basePath = `/api/${plugin.name}`; + this.serverApplication.use(basePath, router); + + // Collect named endpoints from the plugin + endpoints[plugin.name] = plugin.getEndpoints(); } } + + return endpoints; } /** @@ -196,7 +206,7 @@ export class ServerPlugin extends Plugin { * - Dev mode (no staticPath): Vite for HMR * - Production (no staticPath): Static files auto-detected */ - private async setupFrontend() { + private async setupFrontend(endpoints: PluginEndpoints) { const isDev = process.env.NODE_ENV === "development"; const hasExplicitStaticPath = this.config.staticPath !== undefined; @@ -205,6 +215,7 @@ export class ServerPlugin extends Plugin { const staticServer = new StaticServer( this.serverApplication, this.config.staticPath as string, + endpoints, ); staticServer.setup(); return; @@ -212,7 +223,7 @@ export class ServerPlugin extends Plugin { // auto-detection based on environment if (isDev) { - this.viteDevServer = new ViteDevServer(this.serverApplication); + this.viteDevServer = new ViteDevServer(this.serverApplication, endpoints); await this.viteDevServer.setup(); return; } @@ -220,7 +231,12 @@ export class ServerPlugin extends Plugin { // auto-detection based on static path const staticPath = ServerPlugin.findStaticPath(); if (staticPath) { - const staticServer = new StaticServer(this.serverApplication, staticPath); + const staticServer = new StaticServer( + this.serverApplication, + staticPath, + endpoints, + ); + staticServer.setup(); } } @@ -259,7 +275,9 @@ export class ServerPlugin extends Plugin { console.log("Remote tunnel: disabled (controller not initialized)"); } else { console.log( - `Remote tunnel: ${remoteServerController.isAllowedByEnv() ? "allowed" : "blocked"}; ${remoteServerController.isActive() ? "active" : "inactive"}`, + `Remote tunnel: ${ + remoteServerController.isAllowedByEnv() ? "allowed" : "blocked" + }; ${remoteServerController.isActive() ? "active" : "inactive"}`, ); } } diff --git a/packages/app-kit/src/server/remote-tunnel/remote-tunnel-manager.ts b/packages/app-kit/src/server/remote-tunnel/remote-tunnel-manager.ts index bf5c1ff..fdbc8aa 100644 --- a/packages/app-kit/src/server/remote-tunnel/remote-tunnel-manager.ts +++ b/packages/app-kit/src/server/remote-tunnel/remote-tunnel-manager.ts @@ -6,7 +6,11 @@ import { fileURLToPath } from "node:url"; import type express from "express"; import type { TunnelConnection } from "shared"; import { WebSocketServer } from "ws"; -import { generateTunnelIdFromEmail, getQueries, parseCookies } from "../utils"; +import { + generateTunnelIdFromEmail, + getConfigScript, + parseCookies, +} from "../utils"; import { REMOTE_TUNNEL_ASSET_PREFIXES } from "./gate"; const __filename = fileURLToPath(import.meta.url); @@ -162,16 +166,8 @@ export class RemoteTunnelManager { }); const indexPath = path.join(__dirname, "index.html"); - const configObject = this._configInjection(); - const configScript = ` - - `; - let html = fs.readFileSync(indexPath, "utf-8"); - - html = html.replace("", `${configScript}`); + html = html.replace("", `${getConfigScript()}`); res.send(html); }; @@ -251,17 +247,6 @@ export class RemoteTunnelManager { return res.status(200).send(html); } - private _configInjection() { - const configFolder = path.join(process.cwd(), "config"); - - const configObject = { - appName: process.env.DATABRICKS_APP_NAME || "", - queries: getQueries(configFolder), - }; - - return configObject; - } - setupWebSocket() { this.wss.on("connection", (ws, req) => { const email = req.headers["x-forwarded-email"] as string; diff --git a/packages/app-kit/src/server/static-server.ts b/packages/app-kit/src/server/static-server.ts index fe7faca..4af5059 100644 --- a/packages/app-kit/src/server/static-server.ts +++ b/packages/app-kit/src/server/static-server.ts @@ -1,7 +1,9 @@ import fs from "node:fs"; import path from "node:path"; -import express from "express"; -import { getQueries } from "./utils"; +import type express from "express"; +import expressStatic from "express"; +import { BaseServer } from "./base-server"; +import type { PluginEndpoints } from "./utils"; /** * Static server for the App Kit. @@ -11,23 +13,26 @@ import { getQueries } from "./utils"; * * @example * ```ts - * const staticServer = new StaticServer(app, staticPath); + * const staticServer = new StaticServer(app, staticPath, endpoints); * staticServer.setup(); * ``` */ -export class StaticServer { - private app: express.Application; +export class StaticServer extends BaseServer { private staticPath: string; - constructor(app: express.Application, staticPath: string) { - this.app = app; + constructor( + app: express.Application, + staticPath: string, + endpoints: PluginEndpoints = {}, + ) { + super(app, endpoints); this.staticPath = staticPath; } /** Setup the static server. */ setup() { this.app.use( - express.static(this.staticPath, { + expressStatic.static(this.staticPath, { index: false, }), ); @@ -50,21 +55,7 @@ export class StaticServer { } let html = fs.readFileSync(indexPath, "utf-8"); - const config = this.getRuntimeConfig(); - const configScript = ` - - `; - html = html.replace("", `${configScript}`); + html = html.replace("", `${this.getConfigScript()}`); res.send(html); } - - private getRuntimeConfig() { - const configFolder = path.join(process.cwd(), "config"); - return { - appName: process.env.DATABRICKS_APP_NAME || "", - queries: getQueries(configFolder), - }; - } } diff --git a/packages/app-kit/src/server/tests/server.test.ts b/packages/app-kit/src/server/tests/server.test.ts index 601d17b..a66d4cc 100644 --- a/packages/app-kit/src/server/tests/server.test.ts +++ b/packages/app-kit/src/server/tests/server.test.ts @@ -269,6 +269,7 @@ describe("ServerPlugin", () => { name: "needs-client", requiresDatabricksClient: true, injectRoutes, + getEndpoints: vi.fn().mockReturnValue({}), }, }; diff --git a/packages/app-kit/src/server/tests/static-server.test.ts b/packages/app-kit/src/server/tests/static-server.test.ts index df221dd..e340ded 100644 --- a/packages/app-kit/src/server/tests/static-server.test.ts +++ b/packages/app-kit/src/server/tests/static-server.test.ts @@ -22,14 +22,19 @@ vi.mock("express", () => ({ }, })); -// Mock getQueries +// Mock getQueries and getConfigScript vi.mock("../utils", () => ({ getQueries: vi.fn().mockReturnValue({ query1: "SELECT 1" }), + getConfigScript: vi.fn().mockReturnValue(` + + `), })); import fs from "node:fs"; import express from "express"; -import { getQueries } from "../utils"; +import { getConfigScript } from "../utils"; import { StaticServer } from "../static-server"; describe("StaticServer", () => { @@ -174,7 +179,7 @@ describe("StaticServer", () => { const handler = mockApp.get.mock.calls[0][1]; handler({ path: "/" }, mockRes, mockNext); - expect(getQueries).toHaveBeenCalled(); + expect(getConfigScript).toHaveBeenCalled(); const sentHtml = mockRes.send.mock.calls[0][0]; expect(sentHtml).toContain("query1"); }); diff --git a/packages/app-kit/src/server/utils.ts b/packages/app-kit/src/server/utils.ts index 23b857d..accdac2 100644 --- a/packages/app-kit/src/server/utils.ts +++ b/packages/app-kit/src/server/utils.ts @@ -86,3 +86,35 @@ export function getQueries(configFolder: string) { .map((f) => [path.basename(f, ".sql"), path.basename(f, ".sql")]), ); } + +import type { PluginEndpoints } from "shared"; + +export type { PluginEndpoints }; + +export interface RuntimeConfig { + appName: string; + queries: Record; + endpoints: PluginEndpoints; +} + +export function getRuntimeConfig( + endpoints: PluginEndpoints = {}, +): RuntimeConfig { + const configFolder = path.join(process.cwd(), "config"); + + return { + appName: process.env.DATABRICKS_APP_NAME || "", + queries: getQueries(configFolder), + endpoints, + }; +} + +export function getConfigScript(endpoints: PluginEndpoints = {}): string { + const config = getRuntimeConfig(endpoints); + + return ` + + `; +} diff --git a/packages/app-kit/src/server/vite-dev-server.ts b/packages/app-kit/src/server/vite-dev-server.ts index 181760d..5aaa853 100644 --- a/packages/app-kit/src/server/vite-dev-server.ts +++ b/packages/app-kit/src/server/vite-dev-server.ts @@ -3,6 +3,9 @@ import path from "node:path"; import type express from "express"; import type { ViteDevServer as ViteDevServerType } from "vite"; import { mergeConfigDedup } from "@/utils"; +import { BaseServer } from "./base-server"; +import type { PluginEndpoints } from "./utils"; +import { appKitTypesPlugin } from "../type-generator/vite-plugin"; /** * Vite dev server for the App Kit. @@ -12,16 +15,15 @@ import { mergeConfigDedup } from "@/utils"; * * @example * ```ts - * const viteDevServer = new ViteDevServer(app); + * const viteDevServer = new ViteDevServer(app, endpoints); * await viteDevServer.setup(); * ``` */ -export class ViteDevServer { - private app: express.Application; +export class ViteDevServer extends BaseServer { private vite: ViteDevServerType | null; - constructor(app: express.Application) { - this.app = app; + constructor(app: express.Application, endpoints: PluginEndpoints = {}) { + super(app, endpoints); this.vite = null; } @@ -62,8 +64,8 @@ export class ViteDevServer { ignored: ["**/node_modules/**", "!**/node_modules/@databricks/**"], }, }, - plugins: [react.default()], - appType: "spa", + plugins: [react.default(), appKitTypesPlugin()], + appType: "custom", }; const mergedConfigs = mergeConfigDedup(userConfig, coreConfig, mergeConfig); @@ -84,6 +86,7 @@ export class ViteDevServer { try { const indexPath = path.resolve(clientRoot, "index.html"); let html = fs.readFileSync(indexPath, "utf-8"); + html = html.replace("", `${this.getConfigScript()}`); html = await vite.transformIndexHtml(req.originalUrl, html); res.status(200).set({ "Content-Type": "text/html" }).end(html); } catch (e) { diff --git a/packages/shared/src/plugin.ts b/packages/shared/src/plugin.ts index eb5a889..b8bb05e 100644 --- a/packages/shared/src/plugin.ts +++ b/packages/shared/src/plugin.ts @@ -10,6 +10,8 @@ export interface BasePlugin { setup(): Promise; injectRoutes(router: express.Router): void; + + getEndpoints(): PluginEndpointMap; } export interface BasePluginConfig { @@ -96,11 +98,19 @@ export type IAppRequest = express.Request; export type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "head"; export type RouteConfig = { + /** Unique name for this endpoint (used for frontend access) */ + name: string; method: HttpMethod; path: string; handler: (req: IAppRequest, res: IAppResponse) => Promise; }; +/** Map of endpoint names to their full paths for a plugin */ +export type PluginEndpointMap = Record; + +/** Map of plugin names to their endpoint maps */ +export type PluginEndpoints = Record; + export interface QuerySchemas { [key: string]: unknown; }