From f5bbd7d32b97c4652b5cdf89bb13ab9058fc3970 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Fri, 3 Apr 2026 05:40:47 -0500 Subject: [PATCH] Add web adapter This commit introduces a new web adapter, allowing applications to be served in a web environment. The web adapter provides functionality to create and manage application instances, handle API requests, and serve the application via a fetch handler. It also includes support for integrating with admin functionalities when admin options are provided. --- app/build.ts | 5 +++ app/package.json | 6 +++ app/src/adapter/web/index.ts | 1 + app/src/adapter/web/web.adapter.spec.ts | 53 +++++++++++++++++++++++++ app/src/adapter/web/web.adapter.ts | 47 ++++++++++++++++++++++ 5 files changed, 112 insertions(+) create mode 100644 app/src/adapter/web/index.ts create mode 100644 app/src/adapter/web/web.adapter.spec.ts create mode 100644 app/src/adapter/web/web.adapter.ts diff --git a/app/build.ts b/app/build.ts index 92f63356..ede37e93 100644 --- a/app/build.ts +++ b/app/build.ts @@ -340,6 +340,11 @@ async function buildAdapters() { platform: "node", }), + tsup.build({ + ...baseConfig("web"), + platform: "neutral", + }), + tsup.build({ ...baseConfig("node"), platform: "node", diff --git a/app/package.json b/app/package.json index e9428f06..e8ded283 100644 --- a/app/package.json +++ b/app/package.json @@ -278,6 +278,11 @@ "import": "./dist/adapter/tanstack-start/index.js", "require": "./dist/adapter/tanstack-start/index.js" }, + "./adapter/web": { + "types": "./dist/types/adapter/web/index.d.ts", + "import": "./dist/adapter/web/index.js", + "require": "./dist/adapter/web/index.js" + }, "./dist/main.css": "./dist/ui/main.css", "./dist/styles.css": "./dist/ui/styles.css", "./dist/manifest.json": "./dist/static/.vite/manifest.json", @@ -298,6 +303,7 @@ "adapter/node": ["./dist/types/adapter/node/index.d.ts"], "adapter/sveltekit": ["./dist/types/adapter/sveltekit/index.d.ts"], "adapter/tanstack-start": ["./dist/types/adapter/tanstack-start/index.d.ts"], + "adapter/web": ["./dist/types/adapter/web/index.d.ts"], "adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"] } }, diff --git a/app/src/adapter/web/index.ts b/app/src/adapter/web/index.ts new file mode 100644 index 00000000..c536188d --- /dev/null +++ b/app/src/adapter/web/index.ts @@ -0,0 +1 @@ +export * from "./web.adapter"; diff --git a/app/src/adapter/web/web.adapter.spec.ts b/app/src/adapter/web/web.adapter.spec.ts new file mode 100644 index 00000000..c7928b43 --- /dev/null +++ b/app/src/adapter/web/web.adapter.spec.ts @@ -0,0 +1,53 @@ +import { afterAll, beforeAll, describe, test, expect } from "bun:test"; +import { createBknd } from "./web.adapter"; +import type { WebBkndConfig } from "./web.adapter"; +import { disableConsoleLog, enableConsoleLog } from "core/utils"; +import { adapterTestSuite } from "adapter/adapter-test-suite"; +import { bunTestRunner } from "adapter/bun/test"; + +beforeAll(disableConsoleLog); +afterAll(enableConsoleLog); + +describe("web adapter via createBknd", () => { + adapterTestSuite(bunTestRunner, { + makeApp: (config, args) => createBknd(config, args).getApp(), + makeHandler: (config, args) => createBknd(config ?? {}, args).serve(), + }); + + test("caches app instance", async () => { + const bknd = createBknd({ connection: { url: ":memory:" } }); + const app1 = await bknd.getApp(); + const app2 = await bknd.getApp(); + expect(app1).toBe(app2); + }); + + test("getApi returns api", async () => { + const bknd = createBknd({ connection: { url: ":memory:" } }); + const api = await bknd.getApi(); + expect(api).toBeDefined(); + }); + + test("serve returns a fetch handler", async () => { + const bknd = createBknd({ connection: { url: ":memory:" } }); + const handler = bknd.serve(); + const res = await handler(new Request("http://localhost:3000/api/system/config")); + expect(res.status).toBe(200); + }); + + test("uses createRuntimeApp when adminOptions provided", async () => { + const bknd = createBknd({ + connection: { url: ":memory:" }, + adminOptions: { adminBasepath: "/admin" }, + }); + const app = await bknd.getApp(); + expect(app).toBeDefined(); + expect(app.isBuilt()).toBe(true); + }); + + test("uses createFrameworkApp when no adminOptions", async () => { + const bknd = createBknd({ connection: { url: ":memory:" } }); + const app = await bknd.getApp(); + expect(app).toBeDefined(); + expect(app.isBuilt()).toBe(true); + }); +}); diff --git a/app/src/adapter/web/web.adapter.ts b/app/src/adapter/web/web.adapter.ts new file mode 100644 index 00000000..64d653b9 --- /dev/null +++ b/app/src/adapter/web/web.adapter.ts @@ -0,0 +1,47 @@ +import { + createFrameworkApp, + createRuntimeApp, + type FrameworkBkndConfig, +} from "bknd/adapter"; +import type { AdminControllerOptions } from "modules/server/AdminController"; +import type { App } from "App"; + +export type WebBkndConfig = FrameworkBkndConfig & { + adminOptions?: AdminControllerOptions | false; +}; + +export function createBknd(config: WebBkndConfig, args?: Env) { + let appPromise: Promise | undefined; + + const { adminOptions, ...frameworkConfig } = config; + + async function getApp() { + if (!appPromise) { + if (adminOptions != null) { + appPromise = createRuntimeApp({ ...frameworkConfig, adminOptions }, args); + } else { + appPromise = createFrameworkApp(frameworkConfig, args); + } + } + return appPromise; + } + + async function getApi(opts?: { headers?: Headers; verify?: boolean }) { + const app = await getApp(); + if (opts?.verify) { + const api = app.getApi({ headers: opts.headers }); + await api.verifyAuth(); + return api; + } + return app.getApi(); + } + + function serve() { + return async (req: Request) => { + const app = await getApp(); + return app.fetch(req); + }; + } + + return { getApp, getApi, serve }; +}