diff --git a/packages/adapter/adapter-bun/readme.md b/packages/adapter/adapter-bun/readme.md index 125d124e..756aa95f 100644 --- a/packages/adapter/adapter-bun/readme.md +++ b/packages/adapter/adapter-bun/readme.md @@ -29,6 +29,21 @@ You can leave out the `staticDir` option if you don't want to serve static asset The remaining options (`port`, `hostname` etc.) are passed to [Bun.serve](https://github.com/oven-sh/bun#bunserve---fast-http-server). +## `context.platform` + +```ts +export interface BunPlatformInfo { + /** Platform name */ + name: "bun"; + /** Bun server instance */ + server: Server; +} +``` + +## Environment variables + +The `ctx.env()` function is implemented using `process.env`. + ## Limitations Bun support is preliminary and Bun itself is in early development: diff --git a/packages/adapter/adapter-bun/src/index.ts b/packages/adapter/adapter-bun/src/index.ts index 59384f1c..1668901c 100644 --- a/packages/adapter/adapter-bun/src/index.ts +++ b/packages/adapter/adapter-bun/src/index.ts @@ -2,19 +2,24 @@ import fs from "node:fs"; import path from "node:path"; import type { AdapterRequestContext, HattipHandler } from "@hattip/core"; -import type { Serve as BunServeOptions } from "bun"; +import type { Serve, Server } from "bun"; -export type { BunServeOptions }; - -export type BunAdapterOptions = Omit & { +export type BunAdapterOptions = Omit & { staticDir?: string; trustProxy?: boolean; }; +export interface BunPlatformInfo { + /** Platform name */ + name: "bun"; + /** Bun server instance */ + server: Server; +} + export default function bunAdapter( - handler: HattipHandler, + handler: HattipHandler, options: BunAdapterOptions = {}, -): BunServeOptions { +): Serve { const { staticDir, trustProxy, ...remaingOptions } = options; let staticFiles: Set; @@ -42,7 +47,7 @@ export default function bunAdapter( } } - const context: AdapterRequestContext = { + const context: AdapterRequestContext = { request, // TODO: How to get the IP address when not behind a proxy? ip: trustProxy @@ -56,7 +61,7 @@ export default function bunAdapter( waitUntil() { // No op }, - platform: { name: "bun" }, + platform: { name: "bun", server: this }, env(variable: string) { return process.env[variable]; }, diff --git a/packages/adapter/adapter-cloudflare-workers/readme.md b/packages/adapter/adapter-cloudflare-workers/readme.md index ec9866b2..8fbe5a99 100644 --- a/packages/adapter/adapter-cloudflare-workers/readme.md +++ b/packages/adapter/adapter-cloudflare-workers/readme.md @@ -21,8 +21,22 @@ If you don't need to serve static files, you can import the adapter from `@hatti ## `context.platform` -This adapter's platform context contains `env` and `context` properties as passed to the worker's `fetch` function. +```ts +export interface CloudflareWorkersPlatformInfo { + /** Platform name */ + name: "cloudflare-workers"; + /** + * Bindings for secrets, environment variables, and other resources like + * KV namespaces etc. + */ + env: unknown; + /** + * Execution context + */ + context: ExecutionContext; +} +``` ## Environment variables -The `ctx.env()` function only returns bindings with a string value. Other bindings like KV or D1 will return `undefined`. You should use `ctx.platform.env` to access them instead. +The `ctx.env()` function only returns bindings with a string value. Such bindings correspond to [secrets and environment variables](https://developers.cloudflare.com/workers/platform/environment-variables). [Other bindings](https://developers.cloudflare.com/workers/configuration/bindings) like KV or D1 will return `undefined`. You should use `ctx.platform.env` to access them instead. diff --git a/packages/adapter/adapter-cloudflare-workers/src/index.ts b/packages/adapter/adapter-cloudflare-workers/src/index.ts index 3412d1ff..cc9abc37 100644 --- a/packages/adapter/adapter-cloudflare-workers/src/index.ts +++ b/packages/adapter/adapter-cloudflare-workers/src/index.ts @@ -9,13 +9,24 @@ import manifestText from "__STATIC_CONTENT_MANIFEST"; const manifest = JSON.parse(manifestText); export interface CloudflareWorkersPlatformInfo { + /** Platform name */ name: "cloudflare-workers"; + /** + * Bindings for secrets, environment variables, and other resources like + * KV namespaces etc. + * @see https://developers.cloudflare.com/workers/platform/environment-variables + * @see https://developers.cloudflare.com/workers/configuration/bindings + */ env: unknown; + /** + * Execution context + * @see https://developers.cloudflare.com/workers/runtime-apis/fetch-event/#parameters + */ context: ExecutionContext; } export default function cloudflareWorkersAdapter( - handler: HattipHandler, + handler: HattipHandler, ): ExportedHandlerFetchHandler { return async function fetchHandler(request, env, ctx) { if (request.method === "GET" || request.method === "HEAD") { diff --git a/packages/adapter/adapter-cloudflare-workers/src/no-static.ts b/packages/adapter/adapter-cloudflare-workers/src/no-static.ts index 62328f8a..218e999a 100644 --- a/packages/adapter/adapter-cloudflare-workers/src/no-static.ts +++ b/packages/adapter/adapter-cloudflare-workers/src/no-static.ts @@ -6,7 +6,7 @@ import type { CloudflareWorkersPlatformInfo } from "."; export type { CloudflareWorkersPlatformInfo }; export default function cloudflareWorkersAdapter( - handler: HattipHandler, + handler: HattipHandler, ): ExportedHandlerFetchHandler { return async function fetchHandler(request, env, ctx) { const context: AdapterRequestContext = { diff --git a/packages/adapter/adapter-fastly/readme.md b/packages/adapter/adapter-fastly/readme.md index 6e6b9180..3f274422 100644 --- a/packages/adapter/adapter-fastly/readme.md +++ b/packages/adapter/adapter-fastly/readme.md @@ -35,7 +35,14 @@ export default fastlyAdapter(async (ctx) => { ## `context.platform` -This adapter's platform context contains a `client` object, which is [Fastly FetchEvent.client](https://js-compute-reference-docs.edgecompute.app/docs/globals/FetchEvent/#instance-properties). +```ts +export interface FastlyPlatformInfo { + /** Platform name */ + name: "fastly-compute"; + /** Event object */ + event: FetchEvent; +} +``` ## Limitations diff --git a/packages/adapter/adapter-fastly/src/index.ts b/packages/adapter/adapter-fastly/src/index.ts index 0784560c..b6bc1be5 100644 --- a/packages/adapter/adapter-fastly/src/index.ts +++ b/packages/adapter/adapter-fastly/src/index.ts @@ -4,23 +4,15 @@ import type { AdapterRequestContext, HattipHandler } from "@hattip/core"; import { env } from "fastly:env"; export interface FastlyPlatformInfo { + /** Platform name */ name: "fastly-compute"; - client: ClientInfo; + /** Event object */ + event: FetchEvent; } -export interface Geo { - city?: string; - country?: { - code?: string; - name?: string; - }; - subdivision?: { - code?: string; - name?: string; - }; -} - -export default function fastlyComputeAdapter(handler: HattipHandler) { +export default function fastlyComputeAdapter( + handler: HattipHandler, +) { addEventListener("fetch", (event) => { const context: AdapterRequestContext = { request: event.request, @@ -28,7 +20,7 @@ export default function fastlyComputeAdapter(handler: HattipHandler) { waitUntil: event.waitUntil.bind(event), platform: { name: "fastly-compute", - client: event.client, + event, }, passThrough() { // empty diff --git a/packages/adapter/adapter-netlify-edge/readme.md b/packages/adapter/adapter-netlify-edge/readme.md index ebf24c57..ad4382f2 100644 --- a/packages/adapter/adapter-netlify-edge/readme.md +++ b/packages/adapter/adapter-netlify-edge/readme.md @@ -15,4 +15,13 @@ export default netlifyEdgeAdapter(handler); ## `context.platform` -This adapter passes the [Netlify edge function context object](https://docs.netlify.com/netlify-labs/experimental-features/edge-functions/api/#netlify-specific-context-object) as `context.platform`. The type definitions are currently rudimentary and likely incomplete/inaccurate. +```ts +interface NetlifyEdgePlatformInfo { + /** Platform name */ + name: "netlify-edge"; + /** Netlify-specific context object */ + context: NetlifyContext; +} +``` + +See [Netlify's documentation](https://docs.netlify.com/functions/build-with-javascript/#synchronous-function-format) for the Netlify-specific context object. diff --git a/packages/adapter/adapter-netlify-edge/src/index.ts b/packages/adapter/adapter-netlify-edge/src/index.ts index aa4b6929..f36bebaf 100644 --- a/packages/adapter/adapter-netlify-edge/src/index.ts +++ b/packages/adapter/adapter-netlify-edge/src/index.ts @@ -1,7 +1,16 @@ import type { AdapterRequestContext, HattipHandler } from "@hattip/core"; -export interface NetlifyEdgePlatformInfo { +interface NetlifyEdgePlatformInfo { + /** Platform name */ name: "netlify-edge"; + /** + * Netlify-specific context object + * @see https://docs.netlify.com/edge-functions/api/#netlify-specific-context-object + */ + context: NetlifyContext; +} + +export interface NetlifyContext { ip: string | null; cookies: Cookies; geo: Geo; @@ -70,18 +79,18 @@ export interface DeleteCookieOptions { export type NetlifyEdgeFunction = ( request: Request, - info: Omit, + nlContext: NetlifyContext, ) => Response | undefined | Promise; export default function netlifyEdgeAdapter( - handler: HattipHandler, + handler: HattipHandler, ): NetlifyEdgeFunction { - return async function fetchHandler(request, info) { + return async function fetchHandler(request, nlContext) { let passThroughCalled = false; const context: AdapterRequestContext = { request, - ip: info.ip || "", + ip: nlContext.ip || "", waitUntil() { // No op }, @@ -90,7 +99,7 @@ export default function netlifyEdgeAdapter( }, platform: { name: "netlify-edge", - ...info, + context: nlContext, }, env(variable) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/packages/adapter/adapter-netlify-functions/readme.md b/packages/adapter/adapter-netlify-functions/readme.md index 9c039739..1f3536a8 100644 --- a/packages/adapter/adapter-netlify-functions/readme.md +++ b/packages/adapter/adapter-netlify-functions/readme.md @@ -23,4 +23,10 @@ Calling `context.passThrough` has no effect, the placeholder response will be re ## `context.platform` -This adapter's platform context contains the `event` and `context` properties which have the types `NetlifyFunctionEvent` and `NetlifyFunctionContext` respectively. +```ts +export interface NetlifyFunctionsPlatformInfo { + name: "netlify-functions"; + event: NetlifyFunctionEvent; + context: NetlifyFunctionContext; +} +``` diff --git a/packages/adapter/adapter-netlify-functions/src/index.ts b/packages/adapter/adapter-netlify-functions/src/index.ts index 31fce9a3..14dfe13a 100644 --- a/packages/adapter/adapter-netlify-functions/src/index.ts +++ b/packages/adapter/adapter-netlify-functions/src/index.ts @@ -23,7 +23,7 @@ export interface NetlifyFunctionsPlatformInfo { export type { NetlifyFunctionEvent, NetlifyFunctionContext }; export default function netlifyFunctionsAdapter( - handler: HattipHandler, + handler: HattipHandler, ): NetlifyFunction { return async (event, netlifyContext) => { const ip = diff --git a/packages/adapter/adapter-node/src/common.ts b/packages/adapter/adapter-node/src/common.ts index 5a9a2767..ff58486d 100644 --- a/packages/adapter/adapter-node/src/common.ts +++ b/packages/adapter/adapter-node/src/common.ts @@ -71,7 +71,7 @@ export interface NodePlatformInfo { * middleware in Connect-style frameworks like Express. */ export function createMiddleware( - handler: HattipHandler, + handler: HattipHandler, options: NodeAdapterOptions = {}, ): NodeMiddleware { const { diff --git a/packages/adapter/adapter-test/src/index.ts b/packages/adapter/adapter-test/src/index.ts index 9d770fd1..241cc045 100644 --- a/packages/adapter/adapter-test/src/index.ts +++ b/packages/adapter/adapter-test/src/index.ts @@ -3,19 +3,19 @@ import installNodeFetch from "@hattip/polyfills/node-fetch"; installNodeFetch(); -export interface CreateTestClientArgs { - handler: HattipHandler; +export interface CreateTestClientArgs

{ + handler: HattipHandler

; baseUrl?: string | URL; - platform?: any; + platform?: P; env?: Record; } -export function createTestClient({ +export function createTestClient

({ handler, baseUrl, - platform = { name: "test" }, + platform = { name: "test" } as any, env = Object.create(null), -}: CreateTestClientArgs): typeof fetch { +}: CreateTestClientArgs

): typeof fetch { return async function fetch(input, init) { let request: Request; if (input instanceof Request) { diff --git a/packages/adapter/adapter-vercel-edge/src/index.ts b/packages/adapter/adapter-vercel-edge/src/index.ts index fdd96163..64598a87 100644 --- a/packages/adapter/adapter-vercel-edge/src/index.ts +++ b/packages/adapter/adapter-vercel-edge/src/index.ts @@ -14,7 +14,7 @@ export type VercelEdgeFunction = ( ) => Response | Promise; export default function vercelEdgeAdapter( - handler: HattipHandler, + handler: HattipHandler, ): VercelEdgeFunction { return async function vercelEdgeFunction(request, event) { let passThroughCalled = false; diff --git a/packages/base/compose/src/index.ts b/packages/base/compose/src/index.ts index 31af8ad0..df75b062 100644 --- a/packages/base/compose/src/index.ts +++ b/packages/base/compose/src/index.ts @@ -10,8 +10,8 @@ export interface Locals {} /** * Request context */ -export interface RequestContext - extends AdapterRequestContext, +export interface RequestContext

+ extends AdapterRequestContext

, RequestContextExtensions { /** Parsed request URL */ url: URL; @@ -35,23 +35,31 @@ export type MaybeRespone = ResponseLike | void; export type MaybeAsyncResponse = MaybeRespone | Promise; -export type RequestHandler = (context: RequestContext) => MaybeAsyncResponse; +export type RequestHandler

= ( + context: RequestContext

, +) => MaybeAsyncResponse; -export type MaybeRequestHandler = false | null | undefined | RequestHandler; +export type MaybeRequestHandler

= + | false + | null + | undefined + | RequestHandler

; -export type RequestHandlerStack = MaybeRequestHandler | RequestHandlerStack[]; +export type RequestHandlerStack

= + | MaybeRequestHandler

+ | MaybeRequestHandler

[]; function finalHandler(context: RequestContext): Response { context.passThrough(); return new Response("Not found", { status: 404 }); } -export type PartialHandler = ( - context: RequestContext, +export type PartialHandler

= ( + context: RequestContext

, ) => Response | void | Promise; -export function composePartial( - handlers: RequestHandlerStack[], +export function composePartial

( + handlers: RequestHandlerStack

[], next?: () => Promise, ): PartialHandler { const flatHandlers = handlers.flat().filter(Boolean) as RequestHandler[]; @@ -69,8 +77,10 @@ export function composePartial( ); } -export function compose(...handlers: RequestHandlerStack[]): HattipHandler { - return composePartial([ +export function compose

( + ...handlers: RequestHandlerStack

[] +): HattipHandler

{ + return composePartial

([ (context) => { context.url = new URL(context.request.url); context.method = context.request.method; diff --git a/packages/base/core/index.d.ts b/packages/base/core/index.d.ts index f25b603f..3ebe3c39 100644 --- a/packages/base/core/index.d.ts +++ b/packages/base/core/index.d.ts @@ -48,8 +48,8 @@ export interface AdapterRequestContext

{ * @returns A response or a promise that resolves to a response. * @see https://developer.mozilla.org/en-US/docs/Web/API/Response */ -export type HattipHandler = ( - context: AdapterRequestContext, +export type HattipHandler

= ( + context: AdapterRequestContext

, ) => Response | Promise; declare global { diff --git a/packages/base/router/src/index.ts b/packages/base/router/src/index.ts index 83007c9e..d3b6fc51 100644 --- a/packages/base/router/src/index.ts +++ b/packages/base/router/src/index.ts @@ -13,105 +13,161 @@ import { } from "@hattip/compose"; import { HattipHandler } from "@hattip/core"; -export interface RouterContext

> - extends RequestContext { - params: P; +export interface RouterContext< + Params = Record, + Platform = unknown, +> extends RequestContext { + params: Params; } -export type RouteFn = - | (

(matcher: Matcher, handler: RouteHandler

) => void) - | (

(handler: RouteHandler

) => void); +export type RouteFn = + | (( + matcher: Matcher, + handler: RouteHandler, + ) => void) + | ((handler: RouteHandler) => void); -export interface Router { +export interface Router { /** Compose route handlers into a single handler */ - buildHandler(): HattipHandler; + buildHandler(): HattipHandler; /** Route handlers */ - handlers: RequestHandler[]; - - use

(matcher: Matcher, handler: RouteHandler

): void; - use

(handler: RouteHandler

): void; - - checkout

(matcher: Matcher, handler: RouteHandler

): void; - checkout

(handler: RouteHandler

): void; - - copy

(matcher: Matcher, handler: RouteHandler

): void; - copy

(handler: RouteHandler

): void; - - delete

(matcher: Matcher, handler: RouteHandler

): void; - delete

(handler: RouteHandler

): void; - - get

(matcher: Matcher, handler: RouteHandler

): void; - get

(handler: RouteHandler

): void; - - head

(matcher: Matcher, handler: RouteHandler

): void; - head

(handler: RouteHandler

): void; - - lock

(matcher: Matcher, handler: RouteHandler

): void; - lock

(handler: RouteHandler

): void; - - merge

(matcher: Matcher, handler: RouteHandler

): void; - merge

(handler: RouteHandler

): void; - - mkactivity

(matcher: Matcher, handler: RouteHandler

): void; - mkactivity

(handler: RouteHandler

): void; - - mkcol

(matcher: Matcher, handler: RouteHandler

): void; - mkcol

(handler: RouteHandler

): void; - - move

(matcher: Matcher, handler: RouteHandler

): void; - move

(handler: RouteHandler

): void; - - "m-search"

(matcher: Matcher, handler: RouteHandler

): void; - "m-search"

(handler: RouteHandler

): void; - - notify

(matcher: Matcher, handler: RouteHandler

): void; - notify

(handler: RouteHandler

): void; - - options

(matcher: Matcher, handler: RouteHandler

): void; - options

(handler: RouteHandler

): void; - - patch

(matcher: Matcher, handler: RouteHandler

): void; - patch

(handler: RouteHandler

): void; - - post

(matcher: Matcher, handler: RouteHandler

): void; - post

(handler: RouteHandler

): void; - - purge

(matcher: Matcher, handler: RouteHandler

): void; - purge

(handler: RouteHandler

): void; - - put

(matcher: Matcher, handler: RouteHandler

): void; - put

(handler: RouteHandler

): void; - - report

(matcher: Matcher, handler: RouteHandler

): void; - report

(handler: RouteHandler

): void; - - search

(matcher: Matcher, handler: RouteHandler

): void; - search

(handler: RouteHandler

): void; - - subscribe

(matcher: Matcher, handler: RouteHandler

): void; - subscribe

(handler: RouteHandler

): void; - - trace

(matcher: Matcher, handler: RouteHandler

): void; - trace

(handler: RouteHandler

): void; - - unlock

(matcher: Matcher, handler: RouteHandler

): void; - unlock

(handler: RouteHandler

): void; - - unsubscribe

(matcher: Matcher, handler: RouteHandler

): void; - unsubscribe

(handler: RouteHandler

): void; + handlers: RequestHandler[]; + + use

(matcher: Matcher, handler: RouteHandler): void; + use

(handler: RouteHandler): void; + + checkout

( + matcher: Matcher, + handler: RouteHandler, + ): void; + checkout

(handler: RouteHandler): void; + + copy

(matcher: Matcher, handler: RouteHandler): void; + copy

(handler: RouteHandler): void; + + delete

( + matcher: Matcher, + handler: RouteHandler, + ): void; + delete

(handler: RouteHandler): void; + + get

(matcher: Matcher, handler: RouteHandler): void; + get

(handler: RouteHandler): void; + + head

(matcher: Matcher, handler: RouteHandler): void; + head

(handler: RouteHandler): void; + + lock

(matcher: Matcher, handler: RouteHandler): void; + lock

(handler: RouteHandler): void; + + merge

( + matcher: Matcher, + handler: RouteHandler, + ): void; + merge

(handler: RouteHandler): void; + + mkactivity

( + matcher: Matcher, + handler: RouteHandler, + ): void; + mkactivity

(handler: RouteHandler): void; + + mkcol

( + matcher: Matcher, + handler: RouteHandler, + ): void; + mkcol

(handler: RouteHandler): void; + + move

(matcher: Matcher, handler: RouteHandler): void; + move

(handler: RouteHandler): void; + + "m-search"

( + matcher: Matcher, + handler: RouteHandler, + ): void; + "m-search"

(handler: RouteHandler): void; + + notify

( + matcher: Matcher, + handler: RouteHandler, + ): void; + notify

(handler: RouteHandler): void; + + options

( + matcher: Matcher, + handler: RouteHandler, + ): void; + options

(handler: RouteHandler): void; + + patch

( + matcher: Matcher, + handler: RouteHandler, + ): void; + patch

(handler: RouteHandler): void; + + post

(matcher: Matcher, handler: RouteHandler): void; + post

(handler: RouteHandler): void; + + purge

( + matcher: Matcher, + handler: RouteHandler, + ): void; + purge

(handler: RouteHandler): void; + + put

(matcher: Matcher, handler: RouteHandler): void; + put

(handler: RouteHandler): void; + + report

( + matcher: Matcher, + handler: RouteHandler, + ): void; + report

(handler: RouteHandler): void; + + search

( + matcher: Matcher, + handler: RouteHandler, + ): void; + search

(handler: RouteHandler): void; + + subscribe

( + matcher: Matcher, + handler: RouteHandler, + ): void; + subscribe

(handler: RouteHandler): void; + + trace

( + matcher: Matcher, + handler: RouteHandler, + ): void; + trace

(handler: RouteHandler): void; + + unlock

( + matcher: Matcher, + handler: RouteHandler, + ): void; + unlock

(handler: RouteHandler): void; + + unsubscribe

( + matcher: Matcher, + handler: RouteHandler, + ): void; + unsubscribe

(handler: RouteHandler): void; } -export type Matcher

> = +export type Matcher

, Platform = unknown> = | string | RegExp - | ((context: RequestContext) => undefined | P | Promise); + | (( + context: RequestContext, + ) => undefined | P | Promise); -export type RouteHandler

> = ( - context: RouterContext

, -) => MaybeAsyncResponse; +export type RouteHandler< + Params = Record, + Platform = unknown, +> = (context: RouterContext) => MaybeAsyncResponse; -export function createRouter(): Router { +export function createRouter(): Router { const self = { handlers: [] as RouteHandler[],