From 25d70340044cb51f0eb07ab9f428cb53cba7636e Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 14 Mar 2026 16:05:08 -0500 Subject: [PATCH 1/2] fix: Replace module augmentation with helper functions --- docs/content/server/transports.md | 33 +++++++++++++++--- src/__tests__/app.test.ts | 13 ++++--- src/transports/bun-transport.ts | 57 +++++++++++++++++++++++++----- src/transports/deno-transport.ts | 58 ++++++++++++++++++++++++++----- tsconfig.json | 4 +-- 5 files changed, 137 insertions(+), 28 deletions(-) diff --git a/docs/content/server/transports.md b/docs/content/server/transports.md index d59e20c..3594e80 100644 --- a/docs/content/server/transports.md +++ b/docs/content/server/transports.md @@ -6,7 +6,7 @@ weight: 6 By default, Zeta uses `createFetchTransport`. It detects your runtime and provides a minimal transport that supports Bun and Deno. -## Runtime-specific Decorations +## Runtime-specific API Access The default fetch transport not provide access to runtime-specific APIs. For example, in Bun, you use the `server` arg to setup websockets or get the request IP address: @@ -19,21 +19,44 @@ Bun.serve({ }); ``` -To access the same object, you need to provide a custom `transport` then transport-specific APIs will be available in the request context: +To access the same object, you need to provide a custom `transport` then transport-specific APIs, then use the transports helper functions, likes `getBunServer`: ```ts import { createApp } from "@aklinker1/zeta"; -import { createBunTransport } from "@aklinker1/zeta/transports/bun-transport"; +import { + createBunTransport, + getBunServer, +} from "@aklinker1/zeta/transports/bun-transport"; const app = createApp({ transport: createBunTransport(), -}).get(({ request, server }) => { +}).get(({ request }) => { + const server = getBunServer(request); const ip = server.requestIP(request); // ... }); ``` -You only have to add the transport once, to your top-level app, and any runtime-specific decorations will be made available in all child-apps as well! +Alternatively, you can use the plugin to decorate your context directly: + +```ts +import { createApp } from "@aklinker1/zeta"; +import { + createBunTransport, + bunServerPlugin, +} from "@aklinker1/zeta/transports/bun-transport"; + +const app = createApp({ + transport: createBunTransport(), +}) + .use(bunServerPlugin) + .get(({ server }) => { + const ip = server.requestIP(request); + // ... + }); +``` + +You only have to add the transport once to your top-level app, even if you're using these utils in child-apps. ## Transport Options diff --git a/src/__tests__/app.test.ts b/src/__tests__/app.test.ts index 3973761..bae8ab1 100644 --- a/src/__tests__/app.test.ts +++ b/src/__tests__/app.test.ts @@ -7,7 +7,10 @@ import { createApp } from "../app"; import { HttpStatus } from "../status"; import { createTestAppClient } from "../testing"; import type { AnyDef, GetAppData, Transport } from "../types"; -import { createBunTransport } from "../transports/bun-transport"; +import { + bunServerPlugin, + createBunTransport, +} from "../transports/bun-transport"; // Silence console.error logs globalThis.console.error = mock(); @@ -620,9 +623,11 @@ describe("App", () => { const app = createApp({ schemaAdapter: zodSchemaAdapter, transport: createBunTransport(), - }).get("/", (ctx) => { - actual = ctx.server; - }); + }) + .use(bunServerPlugin) + .get("/", ({ server }) => { + actual = server; + }); await app.build()(new Request("http://localhost:3000"), expected); expect(actual!).toBe(expected); diff --git a/src/transports/bun-transport.ts b/src/transports/bun-transport.ts index e7c71fe..eb50d73 100644 --- a/src/transports/bun-transport.ts +++ b/src/transports/bun-transport.ts @@ -1,12 +1,9 @@ -import type { Transport } from "../types"; +import type { RequestContext, Transport } from "../types"; +import { createApp } from "../app"; -export type BunTransport = Transport<[request: Request, server: Bun.Server]>; +const SERVER_KEY = Symbol("bun-transport.server"); -declare module "../types" { - interface RequestContext { - server: Bun.Server; - } -} +export type BunTransport = Transport<[request: Request, server: Bun.Server]>; export function createBunTransport( options?: Omit, "fetch" | "port">, @@ -17,7 +14,7 @@ export function createBunTransport( }; const decorate: BunTransport["decorate"] = (ctx, _request, server) => { - ctx.server = server; + ctx[SERVER_KEY] = server; }; return { @@ -25,3 +22,47 @@ export function createBunTransport( decorate, }; } + +/** + * Given the request context, return Bun's `server` object. Throws an error if the bun transport is not provided on the top-level app. + * + * @example + * ```ts + * const app = createApp({ + * transport: createBunTransport(), + * }).get("/", (ctx) => { + * const server = getBunServer(ctx); + * }) + * ``` + * + * @see `bunServerPlugin` to add the `server` object to request context directly. + */ +export function getBunServer(ctx: RequestContext): Bun.Server { + const server = (ctx as any)[SERVER_KEY]; + if (!server) + throw Error( + "Bun server not found. Did you forget to provide the bun transport?", + ); + + return server; +} + +/** + * Plugin that decorates Bun's `server` object in the request context. + * + * @example + * ```ts + * const app = createApp({ + * transport: createBunTransport(), + * }) + * .use(bunServerPlugin) + * .get("/", ({ server }) => { + * // ... + * }) + * ``` + * + * @see `getBunServer` for a simple function to return the server + */ +export const bunServerPlugin = createApp() + .onTransform((ctx) => ({ server: getBunServer(ctx) })) + .export(); diff --git a/src/transports/deno-transport.ts b/src/transports/deno-transport.ts index 7688fe5..ac6471b 100644 --- a/src/transports/deno-transport.ts +++ b/src/transports/deno-transport.ts @@ -1,15 +1,13 @@ -import type { Transport } from "../types"; +import { createApp } from "../app"; +import type { RequestContext, Transport } from "../types"; + +const SERVER_KEY = Symbol("deno-transport.server"); export type DenoTransport = Transport< [request: Request, server: Deno.HttpServer] >; -declare module "../types" { - interface RequestContext { - // @ts-expect-error: Ignore conflict with bun transport, only one will be imported in production. - server: Deno.HttpServer; - } -} +type ServeOptions = Parameters[0]; export function createDenoTransport( options?: Omit, @@ -20,7 +18,7 @@ export function createDenoTransport( }; const decorate: DenoTransport["decorate"] = (ctx, _request, server) => { - ctx.server = server; + ctx[SERVER_KEY] = server; }; return { @@ -29,4 +27,46 @@ export function createDenoTransport( }; } -type ServeOptions = Parameters[0]; +/** + * Given the request context, return Deno's `server` object. Throws an error if the Deno transport is not provided on the top-level app. + * + * @example + * ```ts + * const app = createApp({ + * transport: createDenoTransport(), + * }).get("/", (ctx) => { + * const server = getDenoServer(ctx); + * }) + * ``` + * + * @see `denoServerPlugin` to add the `server` object to request context directly. + */ +export function getDenoServer(ctx: RequestContext): Deno.HttpServer { + const server = (ctx as any)[SERVER_KEY]; + if (!server) + throw Error( + "Deno server not found. Did you forget to provide the deno transport?", + ); + + return server; +} + +/** + * Plugin that decorates Deno's `server` object in the request context. + * + * @example + * ```ts + * const app = createApp({ + * transport: createDenoTransport(), + * }) + * .use(denoServerPlugin) + * .get("/", ({ server }) => { + * // ... + * }) + * ``` + * + * @see `getDenoServer` for a simple function to return the server + */ +export const denoServerPlugin = createApp() + .onTransform((ctx) => ({ server: getDenoServer(ctx) })) + .export(); diff --git a/tsconfig.json b/tsconfig.json index 3892f01..3ef51ab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ // Best practices "strict": true, "skipLibCheck": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, }, - "exclude": ["node_modules", ".git", "dist"] + "exclude": ["./node_modules/**", "./.git/**", "./dist/**"], } From 67bb8ab2ca3f57bda9192c6ccf4213c11bb80963 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 14 Mar 2026 16:06:38 -0500 Subject: [PATCH 2/2] Fix formatting --- tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 3ef51ab..bb4449f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ // Best practices "strict": true, "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, + "noFallthroughCasesInSwitch": true }, - "exclude": ["./node_modules/**", "./.git/**", "./dist/**"], + "exclude": ["./node_modules/**", "./.git/**", "./dist/**"] }