diff --git a/.vscode/settings.json b/.vscode/settings.json index e39fc976..94acf8e5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,9 @@ { "typescript.tsdk": "node_modules/typescript/lib", + "editor.formatOnSave": true, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, "deno.enablePaths": ["./_deno"], "deno.enable": false } diff --git a/packages/adapter/adapter-node/package.json b/packages/adapter/adapter-node/package.json index 8dd60311..9da7a5a0 100644 --- a/packages/adapter/adapter-node/package.json +++ b/packages/adapter/adapter-node/package.json @@ -7,9 +7,12 @@ "dist" ], "exports": { - ".": "./dist/index.js", - "./native-fetch": "./dist/native-fetch.js", - "./whatwg-node": "./dist/whatwg-node.js", + ".": "./dist/http/index.js", + "./native-fetch": "./dist/http/native-fetch.js", + "./whatwg-node": "./dist/http/whatwg-node.js", + "./http2": "./dist/http2/index.js", + "./http2/native-fetch": "./dist/http2/native-fetch.js", + "./http2/whatwg-node": "./dist/http2/whatwg-node.js", "./request": "./dist/request.js", "./response": "./dist/response.js", "./fast-fetch": "./dist/fast-fetch.js" diff --git a/packages/adapter/adapter-node/src/common.ts b/packages/adapter/adapter-node/src/common.ts deleted file mode 100644 index 2693ff59..00000000 --- a/packages/adapter/adapter-node/src/common.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { AdapterRequestContext, HattipHandler } from "@hattip/core"; -import { - createServer as createHttpServer, - Server as HttpServer, - IncomingMessage, - ServerResponse, - ServerOptions, -} from "node:http"; -import type { Socket } from "node:net"; -import { NodeRequestAdapterOptions, createRequestAdapter } from "./request"; -import { sendResponse } from "./response"; -import process from "node:process"; - -interface PossiblyEncryptedSocket extends Socket { - encrypted?: boolean; -} - -/** - * `IncomingMessage` possibly augmented by Express-specific - * `ip` and `protocol` properties. - */ -export interface DecoratedRequest extends Omit { - ip?: string; - protocol?: string; - socket: PossiblyEncryptedSocket; -} - -/** Connect/Express style request listener/middleware */ -export type NodeMiddleware = ( - req: DecoratedRequest, - res: ServerResponse, - next?: (err?: unknown) => void, -) => void; - -/** Adapter options */ -export interface NodeAdapterOptions extends NodeRequestAdapterOptions { - /** - * Whether to call the next middleware in the chain even if the request - * was handled.@default true - */ - alwaysCallNext?: boolean; -} - -export interface NodePlatformInfo { - name: "node"; - request: DecoratedRequest; - response: ServerResponse; -} - -/** - * Creates a request handler to be passed to http.createServer() or used as a - * middleware in Connect-style frameworks like Express. - */ -export function createMiddleware( - handler: HattipHandler, - options: NodeAdapterOptions = {}, -): NodeMiddleware { - const { alwaysCallNext = true, ...requestOptions } = options; - - const requestAdapter = createRequestAdapter(requestOptions); - - return async (req, res, next) => { - try { - const [request, ip] = requestAdapter(req, res); - - let passThroughCalled = false; - - const context: AdapterRequestContext = { - request, - - ip, - - env(variable) { - return process.env[variable]; - }, - - waitUntil(promise) { - // Do nothing - void promise; - }, - - passThrough() { - passThroughCalled = true; - }, - - platform: { - name: "node", - request: req, - response: res, - }, - }; - - const response = await handler(context); - - if (passThroughCalled && next) { - next(); - return; - } - - await sendResponse(req, res, response); - - if (next && alwaysCallNext) { - next(); - } - } catch (error) { - if (next) { - next(error); - } else { - console.error(error); - - if (!res.headersSent) { - res.statusCode = 500; - } - - if (!res.writableEnded) { - res.end(); - } - } - } - }; -} - -/** - * Create an HTTP server - */ -export function createServer( - handler: HattipHandler, - adapterOptions?: NodeAdapterOptions, - serverOptions?: ServerOptions, -): HttpServer { - const listener = createMiddleware(handler, adapterOptions); - return serverOptions - ? createHttpServer(serverOptions, listener) - : createHttpServer(listener); -} diff --git a/packages/adapter/adapter-node/src/http/common.ts b/packages/adapter/adapter-node/src/http/common.ts new file mode 100644 index 00000000..47aaadb1 --- /dev/null +++ b/packages/adapter/adapter-node/src/http/common.ts @@ -0,0 +1,28 @@ +import type { HattipHandler } from "@hattip/core"; +import * as http from "node:http"; +import { NodeAdapterOptions } from "../types"; +import { NodePlatformInfo, Server, ServerOptions } from "../types/http"; +import { createMiddleware } from "../middleware"; + +/** + * Create an HTTP/1.1 server + */ +export function createServer( + handler: HattipHandler, + adapterOptions?: NodeAdapterOptions, + serverOptions?: ServerOptions, +): Server { + const listener = createMiddleware(handler, adapterOptions); + return serverOptions + ? http.createServer(serverOptions, listener) + : http.createServer(listener); +} + +export { createMiddleware } from "../middleware"; + +export type { NodeAdapterOptions } from "../types"; +export type { + DecoratedRequest, + NodeMiddleware, + NodePlatformInfo, +} from "../types/http"; diff --git a/packages/adapter/adapter-node/src/http/index.ts b/packages/adapter/adapter-node/src/http/index.ts new file mode 100644 index 00000000..7dd0748d --- /dev/null +++ b/packages/adapter/adapter-node/src/http/index.ts @@ -0,0 +1,3 @@ +import "../polyfills/node-fetch"; + +export * from "./common"; diff --git a/packages/adapter/adapter-node/src/http/native-fetch.ts b/packages/adapter/adapter-node/src/http/native-fetch.ts new file mode 100644 index 00000000..5fa0e97d --- /dev/null +++ b/packages/adapter/adapter-node/src/http/native-fetch.ts @@ -0,0 +1,3 @@ +import "../polyfills/native-fetch"; + +export * from "./common"; diff --git a/packages/adapter/adapter-node/src/http/whatwg-node.ts b/packages/adapter/adapter-node/src/http/whatwg-node.ts new file mode 100644 index 00000000..5ec2d2c2 --- /dev/null +++ b/packages/adapter/adapter-node/src/http/whatwg-node.ts @@ -0,0 +1,3 @@ +import "../polyfills/whatwg-node"; + +export * from "./common"; diff --git a/packages/adapter/adapter-node/src/http2/common.ts b/packages/adapter/adapter-node/src/http2/common.ts new file mode 100644 index 00000000..008b6935 --- /dev/null +++ b/packages/adapter/adapter-node/src/http2/common.ts @@ -0,0 +1,37 @@ +import type { HattipHandler } from "@hattip/core"; +import * as http2 from "node:http2"; +import { NodeAdapterOptions } from "../types"; +import { + IncomingMessage, + NodePlatformInfo, + Server, + ServerOptions, + ServerResponse, +} from "../types/http2"; +import * as hattip from "../middleware"; + +/** + * Create an HTTP/2 server + */ +export function createServer( + handler: HattipHandler, + adapterOptions?: NodeAdapterOptions, + serverOptions?: ServerOptions, +): Server { + const listener = hattip.createMiddleware(handler, adapterOptions); + return serverOptions + ? http2.createServer(serverOptions, listener) + : http2.createServer(listener); +} + +export const createMiddleware = hattip.createMiddleware< + IncomingMessage, + ServerResponse +>; + +export type { NodeAdapterOptions } from "../types"; +export type { + DecoratedRequest, + NodeMiddleware, + NodePlatformInfo, +} from "../types/http2"; diff --git a/packages/adapter/adapter-node/src/http2/index.ts b/packages/adapter/adapter-node/src/http2/index.ts new file mode 100644 index 00000000..7dd0748d --- /dev/null +++ b/packages/adapter/adapter-node/src/http2/index.ts @@ -0,0 +1,3 @@ +import "../polyfills/node-fetch"; + +export * from "./common"; diff --git a/packages/adapter/adapter-node/src/http2/native-fetch.ts b/packages/adapter/adapter-node/src/http2/native-fetch.ts new file mode 100644 index 00000000..5fa0e97d --- /dev/null +++ b/packages/adapter/adapter-node/src/http2/native-fetch.ts @@ -0,0 +1,3 @@ +import "../polyfills/native-fetch"; + +export * from "./common"; diff --git a/packages/adapter/adapter-node/src/http2/whatwg-node.ts b/packages/adapter/adapter-node/src/http2/whatwg-node.ts new file mode 100644 index 00000000..5ec2d2c2 --- /dev/null +++ b/packages/adapter/adapter-node/src/http2/whatwg-node.ts @@ -0,0 +1,3 @@ +import "../polyfills/whatwg-node"; + +export * from "./common"; diff --git a/packages/adapter/adapter-node/src/index.ts b/packages/adapter/adapter-node/src/index.ts deleted file mode 100644 index 87a9d87f..00000000 --- a/packages/adapter/adapter-node/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ServerResponse } from "node:http"; -import { DecoratedRequest, NodeAdapterOptions } from "./common"; -import installNodeFetch from "@hattip/polyfills/node-fetch"; -import installGetSetCookie from "@hattip/polyfills/get-set-cookie"; -import installCrypto from "@hattip/polyfills/crypto"; - -installNodeFetch(); -installGetSetCookie(); -installCrypto(); - -export type { DecoratedRequest, NodeAdapterOptions }; - -/** Connect/Express style request listener/middleware */ -export type NodeMiddleware = ( - req: DecoratedRequest, - res: ServerResponse, - next?: () => void, -) => void; - -export interface NodePlatformInfo { - request: DecoratedRequest; - response: ServerResponse; -} - -export { createMiddleware, createServer } from "./common"; diff --git a/packages/adapter/adapter-node/src/middleware.ts b/packages/adapter/adapter-node/src/middleware.ts new file mode 100644 index 00000000..dc2b48ad --- /dev/null +++ b/packages/adapter/adapter-node/src/middleware.ts @@ -0,0 +1,85 @@ +import type { AdapterRequestContext, HattipHandler } from "@hattip/core"; +import process from "node:process"; +import { createRequestAdapter } from "./request"; +import { sendResponse } from "./response"; +import { NodeMiddleware, NodePlatformInfo } from "./types/common"; +import { IncomingMessage, NodeAdapterOptions, ServerResponse } from "./types"; +import type * as http from "./types/http"; + +/** + * Creates a request handler to be passed to http.createServer() or used as a + * middleware in Connect-style frameworks like Express. + */ +export function createMiddleware< + NodeRequest extends IncomingMessage = http.IncomingMessage, + NodeResponse extends ServerResponse = http.ServerResponse, +>( + handler: HattipHandler>, + options: NodeAdapterOptions = {}, +): NodeMiddleware { + const { alwaysCallNext = true, ...requestOptions } = options; + + const requestAdapter = createRequestAdapter(requestOptions); + + return async (req, res, next) => { + try { + const [request, ip] = requestAdapter(req, res); + + let passThroughCalled = false; + + const context: AdapterRequestContext< + NodePlatformInfo + > = { + request, + + ip, + + env(variable) { + return process.env[variable]; + }, + + waitUntil(promise) { + // Do nothing + void promise; + }, + + passThrough() { + passThroughCalled = true; + }, + + platform: { + name: "node", + request: req, + response: res, + }, + }; + + const response = await handler(context); + + if (passThroughCalled && next) { + next(); + return; + } + + await sendResponse(req, res, response); + + if (next && alwaysCallNext) { + next(); + } + } catch (error) { + if (next) { + next(error); + } else { + console.error(error); + + if (!res.headersSent) { + res.statusCode = 500; + } + + if (!res.writableEnded) { + res.end(); + } + } + } + }; +} diff --git a/packages/adapter/adapter-node/src/native-fetch.ts b/packages/adapter/adapter-node/src/polyfills/native-fetch.ts similarity index 72% rename from packages/adapter/adapter-node/src/native-fetch.ts rename to packages/adapter/adapter-node/src/polyfills/native-fetch.ts index 60a234bc..ae8be0fd 100644 --- a/packages/adapter/adapter-node/src/native-fetch.ts +++ b/packages/adapter/adapter-node/src/polyfills/native-fetch.ts @@ -12,12 +12,3 @@ for (const key of Object.keys(webStream)) { (globalThis as any)[key] = (webStream as any)[key]; } } - -export type { - DecoratedRequest, - NodeMiddleware, - NodeAdapterOptions, - NodePlatformInfo, -} from "./common"; - -export { createMiddleware, createServer } from "./common"; diff --git a/packages/adapter/adapter-node/src/polyfills/node-fetch.ts b/packages/adapter/adapter-node/src/polyfills/node-fetch.ts new file mode 100644 index 00000000..d1df780f --- /dev/null +++ b/packages/adapter/adapter-node/src/polyfills/node-fetch.ts @@ -0,0 +1,7 @@ +import installNodeFetch from "@hattip/polyfills/node-fetch"; +import installGetSetCookie from "@hattip/polyfills/get-set-cookie"; +import installCrypto from "@hattip/polyfills/crypto"; + +installNodeFetch(); +installGetSetCookie(); +installCrypto(); diff --git a/packages/adapter/adapter-node/src/polyfills/whatwg-node.ts b/packages/adapter/adapter-node/src/polyfills/whatwg-node.ts new file mode 100644 index 00000000..d29bec6a --- /dev/null +++ b/packages/adapter/adapter-node/src/polyfills/whatwg-node.ts @@ -0,0 +1,8 @@ +// TODO: Remove or update this rule! +import installCrypto from "@hattip/polyfills/crypto"; +import installGetSetCookie from "@hattip/polyfills/get-set-cookie"; +import installWhatwgNodeFetch from "@hattip/polyfills/whatwg-node"; + +installWhatwgNodeFetch(); +installGetSetCookie(); +installCrypto(); diff --git a/packages/adapter/adapter-node/src/request.ts b/packages/adapter/adapter-node/src/request.ts index a70e377d..35405543 100644 --- a/packages/adapter/adapter-node/src/request.ts +++ b/packages/adapter/adapter-node/src/request.ts @@ -1,52 +1,15 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import type { IncomingMessage, ServerResponse } from "node:http"; -import type { Socket } from "node:net"; import process from "node:process"; -import { Buffer } from "node:buffer"; import { Readable } from "node:stream"; +import { + DecoratedRequest, + NodeRequestAdapterOptions, + ServerResponse, +} from "./types"; // @ts-ignore const isDeno = typeof Deno !== "undefined"; -interface PossiblyEncryptedSocket extends Socket { - encrypted?: boolean; -} - -/** - * `IncomingMessage` possibly augmented with some environment-specific - * properties. - */ -export interface DecoratedRequest extends Omit { - ip?: string; - protocol?: string; - socket?: PossiblyEncryptedSocket; - rawBody?: Buffer | null; -} - -/** Adapter options */ -export interface NodeRequestAdapterOptions { - /** - * Set the origin part of the URL to a constant value. - * It defaults to `process.env.ORIGIN`. If neither is set, - * the origin is computed from the protocol and hostname. - * To determine the protocol, `req.protocol` is tried first. - * If `trustProxy` is set, `X-Forwarded-Proto` header is used. - * Otherwise, `req.socket.encrypted` is used. - * To determine the hostname, `X-Forwarded-Host` - * (if `trustProxy` is set) or `Host` header is used. - */ - origin?: string; - /** - * Whether to trust `X-Forwarded-*` headers. `X-Forwarded-Proto` - * and `X-Forwarded-Host` are used to determine the origin when - * `origin` and `process.env.ORIGIN` are not set. `X-Forwarded-For` - * is used to determine the IP address. The leftmost values are used - * if multiple values are set. Defaults to true if `process.env.TRUST_PROXY` - * is set to `1`, otherwise false. - */ - trustProxy?: boolean; -} - /** Create a function that converts a Node HTTP request into a fetch API `Request` object */ export function createRequestAdapter( options: NodeRequestAdapterOptions = {}, @@ -87,14 +50,14 @@ export function createRequestAdapter( const ip = req.ip || (trustProxy && parseForwardedHeader("for")) || - req.socket?.remoteAddress || + req.socket.remoteAddress || ""; const protocol = protocolOverride || req.protocol || (trustProxy && parseForwardedHeader("proto")) || - (req.socket?.encrypted && "https") || + (req.socket.encrypted && "https") || "http"; let host = diff --git a/packages/adapter/adapter-node/src/response.ts b/packages/adapter/adapter-node/src/response.ts index ce0a6b2d..bd880208 100644 --- a/packages/adapter/adapter-node/src/response.ts +++ b/packages/adapter/adapter-node/src/response.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { ServerResponse } from "node:http"; import { rawBodySymbol } from "./raw-body-symbol"; -import { DecoratedRequest } from "./common"; +import { DecoratedRequest, ServerResponse } from "./types"; // @ts-ignore const deno = typeof Deno !== "undefined"; @@ -25,7 +24,9 @@ if (deno) { export async function sendResponse( req: DecoratedRequest, res: ServerResponse, - fetchResponse: Response, + fetchResponse: Response & { + [rawBodySymbol]?: any; + }, ): Promise { const controller = new AbortController(); const signal = controller.signal; @@ -40,9 +41,9 @@ export async function sendResponse( const hasContentLength = fetchResponse.headers.has("Content-Length"); - if ((fetchResponse as any)[rawBodySymbol]) { + if (fetchResponse[rawBodySymbol]) { writeHead(fetchResponse, res); - res.end((fetchResponse as any)[rawBodySymbol]); + res.end(fetchResponse[rawBodySymbol]); return; } @@ -129,9 +130,17 @@ function writeHead(fetchResponse: Response, nodeResponse: ServerResponse) { } } +type GenericResponse = { + write(chunk: Uint8Array): boolean; + once(event: "drain", listener: () => void): void; + once(event: "error", listener: (err: unknown) => void): void; + off(event: "drain", listener: () => void): void; + off(event: "error", listener: (err: unknown) => void): void; +}; + async function writeAndAwait( chunk: Uint8Array, - res: ServerResponse, + res: GenericResponse, signal: AbortSignal, ) { const written = res.write(chunk); diff --git a/packages/adapter/adapter-node/src/types/common.ts b/packages/adapter/adapter-node/src/types/common.ts new file mode 100644 index 00000000..054dbfd1 --- /dev/null +++ b/packages/adapter/adapter-node/src/types/common.ts @@ -0,0 +1,26 @@ +import type { Socket } from "node:net"; +import type { Buffer } from "node:buffer"; + +/** + * `IncomingMessage` possibly augmented by Express-specific + * `ip` and `protocol` properties. + */ +export type DecoratedRequest = Omit & { + ip?: string; + protocol?: string; + socket: Socket & { encrypted?: boolean }; + rawBody?: Buffer | null; +}; + +/** Connect/Express style request listener/middleware */ +export type NodeMiddleware = ( + req: DecoratedRequest, + res: NodeResponse, + next?: (err?: unknown) => void, +) => void; + +export interface NodePlatformInfo { + name: "node"; + request: DecoratedRequest; + response: NodeResponse; +} diff --git a/packages/adapter/adapter-node/src/types/http.ts b/packages/adapter/adapter-node/src/types/http.ts new file mode 100644 index 00000000..c4707566 --- /dev/null +++ b/packages/adapter/adapter-node/src/types/http.ts @@ -0,0 +1,26 @@ +import type { + IncomingMessage, + ServerResponse, + Server, + ServerOptions, +} from "node:http"; + +export type { IncomingMessage, ServerResponse, Server, ServerOptions }; + +/** + * `IncomingMessage` possibly augmented by Express-specific + * `ip` and `protocol` properties. + */ +export type DecoratedRequest = + import("./common").DecoratedRequest; + +/** Connect/Express style request listener/middleware */ +export type NodeMiddleware = import("./common").NodeMiddleware< + IncomingMessage, + ServerResponse +>; + +export type NodePlatformInfo = import("./common").NodePlatformInfo< + IncomingMessage, + ServerResponse +>; diff --git a/packages/adapter/adapter-node/src/types/http2.ts b/packages/adapter/adapter-node/src/types/http2.ts new file mode 100644 index 00000000..af7ae13c --- /dev/null +++ b/packages/adapter/adapter-node/src/types/http2.ts @@ -0,0 +1,26 @@ +import type { + Http2ServerRequest as IncomingMessage, + Http2ServerResponse as ServerResponse, + Http2Server as Server, + ServerOptions, +} from "node:http2"; + +export type { IncomingMessage, ServerResponse, Server, ServerOptions }; + +/** + * `IncomingMessage` possibly augmented by Express-specific + * `ip` and `protocol` properties. + */ +export type DecoratedRequest = + import("./common").DecoratedRequest; + +/** Connect/Express style request listener/middleware */ +export type NodeMiddleware = import("./common").NodeMiddleware< + IncomingMessage, + ServerResponse +>; + +export type NodePlatformInfo = import("./common").NodePlatformInfo< + IncomingMessage, + ServerResponse +>; diff --git a/packages/adapter/adapter-node/src/types/index.ts b/packages/adapter/adapter-node/src/types/index.ts new file mode 100644 index 00000000..c2228c2a --- /dev/null +++ b/packages/adapter/adapter-node/src/types/index.ts @@ -0,0 +1,52 @@ +import type * as http2 from "./http2"; +import type * as http from "./http"; + +export type IncomingMessage = http.IncomingMessage | http2.IncomingMessage; +export type ServerResponse = http.ServerResponse | http2.ServerResponse; + +export type DecoratedRequest = + import("./common").DecoratedRequest; + +/** Connect/Express style request listener/middleware */ +export type NodeMiddleware = import("./common").NodeMiddleware< + IncomingMessage, + ServerResponse +>; + +export type NodePlatformInfo = import("./common").NodePlatformInfo< + IncomingMessage, + ServerResponse +>; + +/** Adapter options */ +export interface NodeRequestAdapterOptions { + /** + * Set the origin part of the URL to a constant value. + * It defaults to `process.env.ORIGIN`. If neither is set, + * the origin is computed from the protocol and hostname. + * To determine the protocol, `req.protocol` is tried first. + * If `trustProxy` is set, `X-Forwarded-Proto` header is used. + * Otherwise, `req.socket.encrypted` is used. + * To determine the hostname, `X-Forwarded-Host` + * (if `trustProxy` is set) or `Host` header is used. + */ + origin?: string; + /** + * Whether to trust `X-Forwarded-*` headers. `X-Forwarded-Proto` + * and `X-Forwarded-Host` are used to determine the origin when + * `origin` and `process.env.ORIGIN` are not set. `X-Forwarded-For` + * is used to determine the IP address. The leftmost values are used + * if multiple values are set. Defaults to true if `process.env.TRUST_PROXY` + * is set to `1`, otherwise false. + */ + trustProxy?: boolean; +} + +/** Adapter options */ +export interface NodeAdapterOptions extends NodeRequestAdapterOptions { + /** + * Whether to call the next middleware in the chain even if the request + * was handled.@default true + */ + alwaysCallNext?: boolean; +} diff --git a/packages/adapter/adapter-node/src/whatwg-node.ts b/packages/adapter/adapter-node/src/whatwg-node.ts deleted file mode 100644 index 702afbe1..00000000 --- a/packages/adapter/adapter-node/src/whatwg-node.ts +++ /dev/null @@ -1,26 +0,0 @@ -// TODO: Remove or update this rule! -import { ServerResponse } from "node:http"; -import { DecoratedRequest, NodeAdapterOptions } from "./common"; -import installWhatwgNodeFetch from "@hattip/polyfills/whatwg-node"; -import installGetSetCookie from "@hattip/polyfills/get-set-cookie"; -import installCrypto from "@hattip/polyfills/crypto"; - -installWhatwgNodeFetch(); -installGetSetCookie(); -installCrypto(); - -export type { DecoratedRequest, NodeAdapterOptions }; - -/** Connect/Express style request listener/middleware */ -export type NodeMiddleware = ( - req: DecoratedRequest, - res: ServerResponse, - next?: () => void, -) => void; - -export interface NodePlatformInfo { - request: DecoratedRequest; - response: ServerResponse; -} - -export { createMiddleware, createServer } from "./common"; diff --git a/packages/adapter/adapter-node/tsup.config.ts b/packages/adapter/adapter-node/tsup.config.ts index 699292e5..f20e1886 100644 --- a/packages/adapter/adapter-node/tsup.config.ts +++ b/packages/adapter/adapter-node/tsup.config.ts @@ -3,9 +3,12 @@ import { defineConfig } from "tsup"; export default defineConfig([ { entry: [ - "./src/index.ts", - "./src/native-fetch.ts", - "./src/whatwg-node.ts", + "./src/http/index.ts", + "./src/http/native-fetch.ts", + "./src/http/whatwg-node.ts", + "./src/http2/index.ts", + "./src/http2/native-fetch.ts", + "./src/http2/whatwg-node.ts", "./src/request.ts", "./src/response.ts", "./src/fast-fetch.ts", diff --git a/packages/middleware/static/src/node.ts b/packages/middleware/static/src/node.ts index c486bc81..34cfcf83 100644 --- a/packages/middleware/static/src/node.ts +++ b/packages/middleware/static/src/node.ts @@ -1,6 +1,7 @@ import { parseHeaderValue } from "@hattip/headers"; import { createReadStream } from "node:fs"; -import type { IncomingMessage, ServerResponse } from "node:http"; +import type * as http from "node:http"; +import type * as http2 from "node:http2"; import { fileURLToPath } from "node:url"; export interface ReadOnlyFile { @@ -10,7 +11,13 @@ export interface ReadOnlyFile { readonly etag?: string; } -export interface StaticMiddlewareOptions { +type IncomingMessage = http.IncomingMessage | http2.Http2ServerRequest; +type ServerResponse = http.ServerResponse | http2.Http2ServerResponse; + +export interface StaticMiddlewareOptions< + NodeRequest extends IncomingMessage, + NodeResponse extends ServerResponse, +> { /** * The URL path to serve files from. It must start and end with a slash. * @default "/" @@ -34,17 +41,16 @@ export interface StaticMiddlewareOptions { /** * Callback function to set custom headers. */ - setHeaders?( - req: IncomingMessage, - res: ServerResponse, - file: ReadOnlyFile, - ): void; + setHeaders?(req: NodeRequest, res: NodeResponse, file: ReadOnlyFile): void; } -export function createStaticMiddleware( +export function createStaticMiddleware< + NodeRequest extends IncomingMessage = http.IncomingMessage, + NodeResponse extends ServerResponse = http.ServerResponse, +>( root: string | URL, files: Map, - options: StaticMiddlewareOptions = {}, + options: StaticMiddlewareOptions = {}, ) { if (root instanceof URL || root.startsWith("file://")) { root = fileURLToPath(root); @@ -59,8 +65,8 @@ export function createStaticMiddleware( } = options; return function staticMiddleware( - req: IncomingMessage, - res: ServerResponse, + req: NodeRequest, + res: NodeResponse, ): boolean { const method = req.method; const isHeadRequest = method === "HEAD"; diff --git a/testbed/basic/ci.test.ts b/testbed/basic/ci.test.ts index 59ebcaa5..da8fd56a 100644 --- a/testbed/basic/ci.test.ts +++ b/testbed/basic/ci.test.ts @@ -77,6 +77,11 @@ if (process.env.CI === "true") { command: `node entry-node-fast-fetch.js`, skipCryptoTest: nodeVersionMajor < 16, }, + { + name: "Node HTTP/2 with native fetch", + platform: "node", + command: "node --experimental-fetch entry-node-http2.js", + }, { name: "Deno", platform: "deno", diff --git a/testbed/basic/entry-node-http2.js b/testbed/basic/entry-node-http2.js new file mode 100644 index 00000000..c06d7b50 --- /dev/null +++ b/testbed/basic/entry-node-http2.js @@ -0,0 +1,23 @@ +// @ts-check +import * as http2 from "node:http2"; +import { createMiddleware } from "@hattip/adapter-node/http2"; +import handler from "./index.js"; +import { walk } from "@hattip/walk"; +import { createStaticMiddleware } from "@hattip/static/node"; + +const root = new URL("./public", import.meta.url); +const files = walk(root); + +/** + * @type {(request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => boolean} + */ +const staticMiddleware = createStaticMiddleware(root, files, { gzip: true }); +const middleware = createMiddleware(handler); + +http2 + .createServer((req, res) => { + return staticMiddleware(req, res) || middleware(req, res); + }) + .listen(3000, "127.0.0.1", () => { + console.log("Server listening on http://127.0.0.1:3000"); + }); diff --git a/testbed/basic/tsconfig.json b/testbed/basic/tsconfig.json index d50df27e..6b267f23 100644 --- a/testbed/basic/tsconfig.json +++ b/testbed/basic/tsconfig.json @@ -6,7 +6,7 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, - "moduleResolution": "Node", + "moduleResolution": "bundler", "checkJs": true, "noEmit": true },