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/**"]
}