From dea417fdd7b7c92fcfd34b4099fe925c4f8c39c6 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 14 Mar 2026 14:36:02 -0500 Subject: [PATCH] feat: Expose transport parameters (like `server` object) in request handlers --- bun.lock | 3 + jsr.json | 1 + package.json | 13 ++- src/__tests__/app.test.ts | 22 +++++- src/__tests__/client.test.ts | 3 +- src/__tests__/types.test.ts | 21 +++++ src/app.ts | 29 +++++-- .../__tests__/compile-fetch-function.test.ts | 79 +++++++++++++++++++ src/internal/compile-fetch-function.ts | 12 ++- src/internal/utils.ts | 22 ------ src/transports/bun-transport.ts | 22 ++++-- src/transports/deno-transport.ts | 27 ++++++- src/transports/fetch-transport.ts | 26 ++++++ src/types.ts | 38 +++++++-- 14 files changed, 264 insertions(+), 54 deletions(-) create mode 100644 src/transports/fetch-transport.ts diff --git a/bun.lock b/bun.lock index b526f8c..2968502 100644 --- a/bun.lock +++ b/bun.lock @@ -18,6 +18,7 @@ "@opentelemetry/sdk-trace-node": "^2.5.0", "@opentelemetry/semantic-conventions": "^1.39.0", "@types/bun": "latest", + "@types/deno": "^2.5.0", "@typescript/native-preview": "^7.0.0-dev.20260202.1", "changelogen": "^0.6.2", "cookie": "^1.1.1", @@ -427,6 +428,8 @@ "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + "@types/deno": ["@types/deno@2.5.0", "", {}, "sha512-g8JS38vmc0S87jKsFzre+0ZyMOUDHPVokEJymSCRlL57h6f/FdKPWBXgdFh3Z8Ees9sz11qt9VWELU9Y9ZkiVw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], diff --git a/jsr.json b/jsr.json index 5bb8779..ea374bb 100644 --- a/jsr.json +++ b/jsr.json @@ -15,6 +15,7 @@ "./schema": "./src/schema.ts", "./adapters/zod-schema-adapter": "./src/adapters/zod-schema-adapter.ts", "./transports/bun-transport": "./src/transports/bun-transport.ts", + "./transports/fetch-transport": "./src/transports/fetch-transport.ts", "./transports/deno-transport": "./src/transports/deno-transport.ts" } } diff --git a/package.json b/package.json index 43db19d..255ff66 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,10 @@ "types": "./dist/transports/bun-transport.d.mts", "default": "./dist/transports/bun-transport.mjs" }, + "./transports/fetch-transport": { + "types": "./dist/transports/fetch-transport.d.mts", + "default": "./dist/transports/fetch-transport.mjs" + }, "./transports/deno-transport": { "types": "./dist/transports/deno-transport.d.mts", "default": "./dist/transports/deno-transport.mjs" @@ -58,7 +62,7 @@ ], "scripts": { "dev": "bun test --watch", - "build": "tsdown src/{index,types,client,testing,schema,adapters/zod-schema-adapter,transports/bun-transport,transports/deno-transport}.ts", + "build": "tsdown src/{index,types,client,testing,schema,adapters/zod-schema-adapter,transports/bun-transport,transports/fetch-transport,transports/deno-transport}.ts", "bench": "bun run src/__tests__/bench.ts", "example": "bun --watch run example.ts", "example:prod": "NODE_ENV=production bun run example", @@ -80,6 +84,7 @@ "@opentelemetry/sdk-trace-node": "^2.5.0", "@opentelemetry/semantic-conventions": "^1.39.0", "@types/bun": "latest", + "@types/deno": "^2.5.0", "@typescript/native-preview": "^7.0.0-dev.20260202.1", "changelogen": "^0.6.2", "cookie": "^1.1.1", @@ -99,11 +104,15 @@ "zod": "^4.1.11" }, "peerDependencies": { - "@types/bun": "*" + "@types/bun": "*", + "@types/deno": "*" }, "peerDependenciesMeta": { "@types/bun": { "optional": true + }, + "@types/deno": { + "optional": true } } } diff --git a/src/__tests__/app.test.ts b/src/__tests__/app.test.ts index 14f37a6..3973761 100644 --- a/src/__tests__/app.test.ts +++ b/src/__tests__/app.test.ts @@ -6,7 +6,8 @@ import { zodSchemaAdapter } from "../adapters/zod-schema-adapter"; import { createApp } from "../app"; import { HttpStatus } from "../status"; import { createTestAppClient } from "../testing"; -import type { AnyDef, GetAppData } from "../types"; +import type { AnyDef, GetAppData, Transport } from "../types"; +import { createBunTransport } from "../transports/bun-transport"; // Silence console.error logs globalThis.console.error = mock(); @@ -466,6 +467,7 @@ describe("App", () => { "/": AnyDef; }; }; + transport: Transport; }>(); expect(actual).not.toMatchObject({ a: "A" }); }); @@ -489,6 +491,7 @@ describe("App", () => { "/": AnyDef; }; }; + transport: Transport; }>(); expect(actual).toMatchObject({ a: "A" }); }); @@ -608,4 +611,21 @@ describe("App", () => { }); }); }); + + describe("transports", () => { + it("should include transport decorations", async () => { + const expected = Symbol("server") as any; + + let actual: Bun.Server; + const app = createApp({ + schemaAdapter: zodSchemaAdapter, + transport: createBunTransport(), + }).get("/", (ctx) => { + actual = ctx.server; + }); + await app.build()(new Request("http://localhost:3000"), expected); + + expect(actual!).toBe(expected); + }); + }); }); diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index fa5b315..2e54de9 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, mock } from "bun:test"; import { createApp } from "../app"; import { createTestAppClient } from "../testing"; -import type { App } from "../types"; +import type { App, Transport } from "../types"; import type { GetClientRoutes } from "../client"; import { expectTypeOf } from "expect-type"; import { z } from "zod/v4"; @@ -154,6 +154,7 @@ describe("Client", () => { "/images": {}; }; }; + transport: Transport; }>; type Expected = { GET: { diff --git a/src/__tests__/types.test.ts b/src/__tests__/types.test.ts index 376c99a..0720f80 100644 --- a/src/__tests__/types.test.ts +++ b/src/__tests__/types.test.ts @@ -5,6 +5,7 @@ import { z } from "zod/v4"; import type { StandardSchemaV1 } from "@standard-schema/spec"; import { ErrorResponse } from "../schema"; import type { HttpStatus } from "../status"; +import type { BunTransport } from "../transports/bun-transport"; describe("Types", () => { describe("MergeRoutes", () => { @@ -86,6 +87,7 @@ describe("Types", () => { routes: {}; exported: false; ctx: { a: "A" }; + transport: t.Transport; }; type B = { routes: { GET: { "/b": {} } }; @@ -95,6 +97,7 @@ describe("Types", () => { exported: A["exported"]; ctx: { a: "A" }; routes: { GET: { "/b": {} } }; + transport: t.Transport; }; type Actual = t.MergeAppData; @@ -108,18 +111,21 @@ describe("Types", () => { exported: false; ctx: { a: "A" }; routes: { GET: { "/a": {} } }; + transport: t.Transport; }; type B = { prefix: "/test"; exported: true; ctx: { b: "B" }; routes: { GET: { "/b": {} } }; + transport: BunTransport; }; type Expected = { prefix: "/test"; exported: true; ctx: { a: "A"; b: "B" }; routes: { GET: { "/a": {}; "/b": {} } }; + transport: t.Transport; }; type Actual = t.MergeAppData; @@ -389,6 +395,7 @@ describe("Types", () => { [p in Path]: Def; }; }; + transport: t.Transport; }; type Expected = { params: Record; @@ -414,6 +421,7 @@ describe("Types", () => { [p in Path]: Def; }; }; + transport: t.Transport; }; type Expected = Ctx; @@ -434,6 +442,7 @@ describe("Types", () => { [p in Path]: Def; }; }; + transport: t.Transport; }; type Expected = { route: Path; @@ -464,6 +473,7 @@ describe("Types", () => { [p in Path]: Def; }; }; + transport: t.Transport; }; type Expected = { route: Path; @@ -487,6 +497,7 @@ describe("Types", () => { routes: { GET: { "/test": { body: z.ZodObject<{ id: z.ZodString }> } }; }; + transport: t.Transport; }>; type Expected = MyApp; @@ -503,6 +514,7 @@ describe("Types", () => { routes: { GET: { "/test": { body: z.ZodObject<{ id: z.ZodString }> } }; }; + transport: t.Transport; }>; type Expected = t.App<{ prefix: ""; @@ -511,6 +523,7 @@ describe("Types", () => { routes: { GET: { "/api/test": { body: z.ZodObject<{ id: z.ZodString }> } }; }; + transport: t.Transport; }>; type Actual = t.ApplyAppPrefix; @@ -541,6 +554,7 @@ describe("Types", () => { }; }; }; + transport: t.Transport; }; type Expected = { ctx: AppData["ctx"]; @@ -555,6 +569,7 @@ describe("Types", () => { "/api/users": AppData["routes"]["POST"]["/users"]; }; }; + transport: t.Transport; }; type Actual = t.ApplyAppDataPrefix; @@ -570,6 +585,7 @@ describe("Types", () => { exported: false; prefix: "/api"; routes: {}; + transport: t.Transport; }; type ChildAppData = { ctx: { b: "b" }; @@ -578,12 +594,14 @@ describe("Types", () => { routes: { GET: { "/users": t.AnyDef }; }; + transport: BunTransport; }; type Expected = { ctx: { a: "a" }; exported: false; prefix: "/api"; routes: ChildAppData["routes"]; + transport: t.Transport; }; type Actual = t.UseAppData; @@ -597,6 +615,7 @@ describe("Types", () => { exported: false; prefix: "/api"; routes: {}; + transport: t.Transport; }; type ChildAppData = { ctx: { b: "b" }; @@ -605,6 +624,7 @@ describe("Types", () => { routes: { GET: { "/users": t.AnyDef }; }; + transport: BunTransport; }; type Expected = { ctx: { a: "a"; b: "b" }; @@ -613,6 +633,7 @@ describe("Types", () => { routes: { GET: { "/users": t.AnyDef }; }; + transport: t.Transport; }; type Actual = t.UseAppData; diff --git a/src/app.ts b/src/app.ts index 6b4da1b..4f316ae 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,9 +3,9 @@ import { addRoute, createRouter } from "rou3"; import { compileRouter } from "rou3/compiler"; import { compileFetchFunction } from "./internal/compile-fetch-function"; import { compileRouteHandler } from "./internal/compile-route-handler"; -import { detectTransport } from "./internal/utils"; import { buildOpenApiDocs, buildScalarHtml } from "./open-api"; import type { + AnyTransport, App, BasePath, BasePrefix, @@ -18,6 +18,7 @@ import type { ServerSideFetch, Transport, } from "./types"; +import { createFetchTransport } from "./transports/fetch-transport"; let appIdInc = 0; const nextAppId = () => `app-${appIdInc++}`; @@ -47,18 +48,22 @@ const nextHookId = (appId: string) => `${appId}/hook-${_hookIdInc++}`; * // Or serve the app yourself * const fetch = app.build(); * Bun.serve({ fetch, ... }); - * Deno.serve({ fetch, ... }); + * Deno.serve({ ... }, fetch); * ``` * * @param options Configure application behavior. */ -export function createApp( - options?: CreateAppOptions, +export function createApp< + TPrefix extends BasePrefix = "", + TTransport extends AnyTransport = Transport, +>( + options?: CreateAppOptions, ): App<{ ctx: {}; exported: false; prefix: TPrefix; routes: {}; + transport: TTransport; }> { const appId = nextAppId(); @@ -66,6 +71,10 @@ export function createApp( const hooks: App["~zeta"]["hooks"] = {}; const routes: App["~zeta"]["routes"] = {}; + let _transport: AnyTransport; + const getTransport = (): AnyTransport => + (_transport ??= options?.transport ?? createFetchTransport()); + const addRoutesEntry = (method: string, route: string, data: RouterData) => { routes[method] ??= {}; if (routes[method][route]) { @@ -132,7 +141,8 @@ export function createApp( } const getRoute = compileRouter(router); - return compileFetchFunction({ getRoute, hooks, origin }); + const transport = getTransport(); + return compileFetchFunction({ getRoute, hooks, origin, transport }); }, getOpenApiSpec: () => { @@ -148,7 +158,7 @@ export function createApp( }, listen: (port, cb) => { - const transport = options?.transport ?? detectTransport(); + const transport = getTransport(); transport.listen(port, app.build(), cb); return app; }, @@ -352,7 +362,10 @@ export function createApp( /** * Configure how the app is created. */ -export type CreateAppOptions = { +export type CreateAppOptions< + TPrefix extends BasePrefix = "", + TTransport extends AnyTransport = Transport, +> = { /** * The origin to use when constructing URLs. * @default "http://localhost" @@ -397,7 +410,7 @@ export type CreateAppOptions = { * }); * ``` */ - transport?: Transport; + transport?: TTransport; /** * Where the OpenAPI JSON docs is hosted. diff --git a/src/internal/__tests__/compile-fetch-function.test.ts b/src/internal/__tests__/compile-fetch-function.test.ts index 72ec0e6..57e33c5 100644 --- a/src/internal/__tests__/compile-fetch-function.test.ts +++ b/src/internal/__tests__/compile-fetch-function.test.ts @@ -1,11 +1,18 @@ import { describe, it, expect } from "bun:test"; import { compileFetchFunction } from "../compile-fetch-function"; +import type { Transport } from "../../types"; process.env.NODE_ENV = "production"; const origin = "http://localhost"; const getRoute = () => undefined; +const transport: Transport = { + listen: () => { + throw Error("Not implemented"); + }, +}; + describe("compileFetchFunction", () => { describe("when there are no hooks", () => { it("should be a simple function", () => { @@ -13,6 +20,7 @@ describe("compileFetchFunction", () => { origin, getRoute, hooks: {}, + transport, }); expect(actual.toString()).toMatchInlineSnapshot(` @@ -73,6 +81,7 @@ describe("compileFetchFunction", () => { { id: "", applyTo: "global", callback: () => void 0 }, ], }, + transport, }); expect(actual.toString()).toMatchInlineSnapshot(` @@ -141,12 +150,14 @@ describe("compileFetchFunction", () => { { id: "", applyTo: "global", callback: () => void 0 }, ], }, + transport, }); expect(actual.toString()).toMatchInlineSnapshot(` "(request) => { const path = utils.getRawPathname(request); const ctx = new utils.Context(request, path, utils.origin); + let handlerReturnedPromise = false; try { @@ -214,6 +225,7 @@ describe("compileFetchFunction", () => { { id: "", applyTo: "global", callback: () => void 0 }, ], }, + transport, }); expect(actual.toString()).toMatchInlineSnapshot(` @@ -269,4 +281,71 @@ describe("compileFetchFunction", () => { `); }); }); + + describe("when the transport includes decoration", () => { + const decoratedTransport: Transport = { + listen: () => { + throw Error("Not implemented"); + }, + decorate: () => { + throw Error("Not implemented"); + }, + }; + + it("should include a call to transport.decorate", async () => { + const actual = compileFetchFunction({ + origin, + getRoute, + hooks: {}, + transport: decoratedTransport, + }); + + expect(actual.toString()).toMatchInlineSnapshot(` + "(request, ...args) => { + const path = utils.getRawPathname(request); + const ctx = new utils.Context(request, path, utils.origin); + utils.transport.decorate(ctx, request, ...args); + + try { + const matchedRoute = utils.getRoute(request.method, path); + if (matchedRoute == null) { + throw new utils.NotFoundHttpError(undefined, { + method: request.method, + path, + }); + } else { + ctx.matchedRoute = matchedRoute; + } + + ctx.response = matchedRoute.data.compiledHandler(request, ctx); + if (typeof ctx.response.then !== utils.FUNCTION) return ctx.response; + + return ctx.response.catch(error => { + const status = + error instanceof utils.HttpError + ? error.status + : utils.HttpStatus.InternalServerError; + return ( + ctx.response = Response.json( + utils.serializeErrorResponse(error), + { status, headers: ctx.set.headers }, + ) + ); + }); + } catch (error) { + const status = + error instanceof utils.HttpError + ? error.status + : utils.HttpStatus.InternalServerError; + return ( + ctx.response = Response.json( + utils.serializeErrorResponse(error), + { status, headers: ctx.set.headers }, + ) + ); + } + }" + `); + }); + }); }); diff --git a/src/internal/compile-fetch-function.ts b/src/internal/compile-fetch-function.ts index 970ac7b..0f9ed90 100644 --- a/src/internal/compile-fetch-function.ts +++ b/src/internal/compile-fetch-function.ts @@ -1,7 +1,12 @@ import type { MatchedRoute } from "rou3"; import { HttpError, NotFoundHttpError } from "../errors"; import { HttpStatus } from "../status"; -import type { LifeCycleHooks, RouterData, ServerSideFetch } from "../types"; +import type { + AnyTransport, + LifeCycleHooks, + RouterData, + ServerSideFetch, +} from "../types"; import { Context } from "./context"; import { cleanupCompiledWhitespace, @@ -16,9 +21,10 @@ export function compileFetchFunction(options: CompileOptions): ServerSideFetch { const onGlobalErrorCount = options.hooks.onGlobalError?.length; const js = ` -return (request) => { +return (request${options.transport?.decorate ? ", ...args" : ""}) => { const path = utils.getRawPathname(request); const ctx = new utils.Context(request, path, utils.origin); + ${options.transport?.decorate ? `utils.transport.decorate(ctx, request, ...args);` : ""} ${onGlobalAfterResponseCount ? "let handlerReturnedPromise = false;" : ""} try { @@ -62,6 +68,7 @@ ${compileErrorResponse(2)} HttpError, HttpStatus, serializeErrorResponse, + transport: options.transport, }); } @@ -163,4 +170,5 @@ type CompileOptions = { path: string, ) => MatchedRoute | undefined; origin: string; + transport: AnyTransport; }; diff --git a/src/internal/utils.ts b/src/internal/utils.ts index 4921827..b58d0bb 100644 --- a/src/internal/utils.ts +++ b/src/internal/utils.ts @@ -3,15 +3,12 @@ import type { MatchedRoute } from "rou3"; import { HttpError } from "../errors"; import type { ErrorResponse } from "../schema"; import { HttpStatus } from "../status"; -import { createBunTransport } from "../transports/bun-transport"; -import { createDenoTransport } from "../transports/deno-transport"; import type { App, LifeCycleHook, MaybePromise, RouterData, StatusResult, - Transport, } from "../types"; export function validateSchema( @@ -159,25 +156,6 @@ export function isStatusResult(result: any): result is StatusResult { return IsStatusResult in result; } -export function detectTransport(): Transport { - // @ts-ignore: Bun types may not be available - if (typeof Bun !== "undefined") return createBunTransport(); - // @ts-ignore: Deno types may not be available - if (typeof Deno !== "undefined") return createDenoTransport(); - - throw Error(`Cannot automatically detect which transport to use. You must specify a transport in your top-level app: - - --- - import { createBunTransport } from '@aklinker1/zeta/transports/bun-transport'; - - const app = createApp({ - transport: createBunTransport(), - }) - - app.listen(); - ---`); -} - export function cleanupCompiledWhitespace(code: string): string { return ( code diff --git a/src/transports/bun-transport.ts b/src/transports/bun-transport.ts index 7970c13..e7c71fe 100644 --- a/src/transports/bun-transport.ts +++ b/src/transports/bun-transport.ts @@ -1,17 +1,27 @@ import type { Transport } from "../types"; -// @ts-ignore: Bun types may not be available -import type { ServeFunctionOptions } from "@types/bun"; + +export type BunTransport = Transport<[request: Request, server: Bun.Server]>; + +declare module "../types" { + interface RequestContext { + server: Bun.Server; + } +} export function createBunTransport( - options: Omit, "fetch" | "port">, -): Transport { - const listen: Transport["listen"] = (port, fetch, cb) => { - // @ts-ignore: Bun types may not be available + options?: Omit, "fetch" | "port">, +): BunTransport { + const listen: BunTransport["listen"] = (port, fetch, cb) => { Bun.serve({ ...options, port, fetch }); if (cb) setTimeout(cb, 0); }; + const decorate: BunTransport["decorate"] = (ctx, _request, server) => { + ctx.server = server; + }; + return { listen, + decorate, }; } diff --git a/src/transports/deno-transport.ts b/src/transports/deno-transport.ts index 24b008e..7688fe5 100644 --- a/src/transports/deno-transport.ts +++ b/src/transports/deno-transport.ts @@ -1,13 +1,32 @@ import type { Transport } from "../types"; -export function createDenoTransport(): Transport { - const listen: Transport["listen"] = (port, fetch, cb) => { - // @ts-ignore: Deno types may not be available - Deno.serve({ port, fetch }); +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; + } +} + +export function createDenoTransport( + options?: Omit, +): DenoTransport { + const listen: DenoTransport["listen"] = (port, fetch, cb) => { + Deno.serve({ ...options, port }, fetch); if (cb) setTimeout(cb, 0); }; + const decorate: DenoTransport["decorate"] = (ctx, _request, server) => { + ctx.server = server; + }; + return { listen, + decorate, }; } + +type ServeOptions = Parameters[0]; diff --git a/src/transports/fetch-transport.ts b/src/transports/fetch-transport.ts new file mode 100644 index 0000000..966d6b2 --- /dev/null +++ b/src/transports/fetch-transport.ts @@ -0,0 +1,26 @@ +import type { Transport } from "../types"; + +export function createFetchTransport(): Transport { + const listen: Transport["listen"] = (port, fetch, cb) => { + if (globalThis.Bun) Bun.serve({ port, fetch }); + else if (globalThis.Deno) Deno.serve({ port }, fetch); + else + throw Error(`Cannot automatically detect which transport to use. You must specify a transport in your top-level app: + + --- + import { createBunTransport } from '@aklinker1/zeta/transports/bun-transport'; + + const app = createApp({ + transport: createBunTransport(), + }) + + app.listen(); + ---`); + + if (cb) setTimeout(cb, 0); + }; + + return { + listen, + }; +} diff --git a/src/types.ts b/src/types.ts index b17c7f7..b5b7d94 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,7 +56,7 @@ export interface App { /** * Merge and simplify all the app routes into a single fetch function. */ - build: () => ServerSideFetch; + build: () => ServerSideFetch; /** * Returns your application's OpenAPI spec. You do not need to listen to a @@ -587,6 +587,7 @@ export type AppData = { prefix: BasePrefix; ctx: BaseCtx; routes: BaseRoutes; + transport: AnyTransport; }; /** @@ -597,6 +598,7 @@ export type DefaultAppData = { prefix: ""; ctx: {}; routes: {}; + transport: AnyTransport; }; /** @@ -651,16 +653,19 @@ export type BasePath = `/${string}`; // CONTEXT OBJECTS // -/** - * `ctx` type used in the `onGlobalRequest` hook. - */ -export type OnGlobalRequestContext = TCtx & { +export interface RequestContext { request: Request; url: URL; path: string; method: string; set: Setter; -}; +} + +/** + * `ctx` type used in the `onGlobalRequest` hook. + */ +export type OnGlobalRequestContext = TCtx & + RequestContext; /** * `ctx` type used in the `onTransform` hook. @@ -801,6 +806,7 @@ export type MergeApp = * - `exported`: The second app's exported status overrides the first if * present. * - `routes`: See `MergeRoutes` for details. + * - `transport`: Use the original transport, it cannot be overridden */ export type MergeAppData< T1 extends AppData, @@ -814,6 +820,7 @@ export type MergeAppData< routes: T2["routes"] extends BaseRoutes ? Simplify> : T1["routes"]; + transport: T1["transport"]; }>; /** @@ -856,6 +863,7 @@ export type ApplyAppDataPrefix< >; } : TAppData["routes"]; + transport: TAppData["transport"]; }; // @@ -1029,10 +1037,21 @@ export interface SchemaAdapter { // TRANSPORTS // -export interface Transport { +export interface Transport< + TFetchArgs extends [request: Request, ...params: any[]] = [request: Request], +> { + /** + * Actually bind the server to a local port for hosting. + */ listen: (port: number, fetch: ServerSideFetch, cb?: () => void) => void; + /** + * Callback where the ctx object can be modified based on the args provided to the fetch function. + */ + decorate?: (ctx: any, ...fetchArgs: TFetchArgs) => void; } +export type AnyTransport = Transport<[request: Request, ...params: any[]]>; + // // SETTER // @@ -1070,7 +1089,10 @@ export type MaybePromise = Promise | T; /** * A function that, given a request, returns a response. This type is compliant with WinterCG. */ -export type ServerSideFetch = (request: Request) => MaybePromise; +export type ServerSideFetch = + TTransport extends Transport + ? (...args: Params) => MaybePromise + : never; /** * Apply a string prefix to all the keys of an object.