diff --git a/docs/5.migration/0.index.md b/docs/5.migration/0.index.md index dddda0383..28463a040 120000 --- a/docs/5.migration/0.index.md +++ b/docs/5.migration/0.index.md @@ -1 +1 @@ -../../MIGRATION.md \ No newline at end of file +../../MIGRATION.md diff --git a/src/h3.ts b/src/h3.ts index 322914ac8..8a2ea0318 100644 --- a/src/h3.ts +++ b/src/h3.ts @@ -4,7 +4,14 @@ import { toResponse, kNotFound } from "./response.ts"; import { callMiddleware, normalizeMiddleware } from "./middleware.ts"; import type { ServerRequest } from "srvx"; -import type { H3Config, H3CoreConfig, H3Plugin, MatchedRoute, RouterContext } from "./types/h3.ts"; +import type { + H3Config, + H3CoreConfig, + H3Plugin, + H3Response, + MatchedRoute, + RouterContext, +} from "./types/h3.ts"; import type { H3EventContext } from "./types/context.ts"; import type { EventHandler, @@ -40,7 +47,7 @@ export class H3Core implements H3CoreType { this.handler = this.handler.bind(this); } - fetch(request: ServerRequest): Response | Promise { + fetch(request: ServerRequest): H3Response | Promise { return this["~request"](request); } @@ -57,7 +64,7 @@ export class H3Core implements H3CoreType { : routeHandler(event); } - "~request"(request: ServerRequest, context?: H3EventContext): Response | Promise { + "~request"(request: ServerRequest, context?: H3EventContext): H3Response | Promise { // Create a new event instance const event = new H3Event(request, context, this as unknown as H3Type); @@ -114,7 +121,7 @@ export const H3 = /* @__PURE__ */ (() => { _req: ServerRequest | URL | string, _init?: RequestInit, context?: H3EventContext, - ): Response | Promise { + ): H3Response | Promise { return this["~request"](toRequest(_req, _init), context); } diff --git a/src/index.ts b/src/index.ts index 827dedddd..6c882f602 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export type { H3Config, H3CoreConfig, H3Plugin, + H3Response, H3Route, H3RouteMeta, HTTPMethod, @@ -194,6 +195,7 @@ export { type WebSocketHooks, type WebSocketPeer, type WebSocketMessage, + type WebSocketResponse, defineWebSocketHandler, defineWebSocket, } from "./utils/ws.ts"; diff --git a/src/response.ts b/src/response.ts index b572a7002..f9ab0b879 100644 --- a/src/response.ts +++ b/src/response.ts @@ -2,7 +2,7 @@ import { FastResponse } from "srvx"; import { HTTPError } from "./error.ts"; import { isJSONSerializable } from "./utils/internal/object.ts"; -import type { H3Config } from "./types/h3.ts"; +import type { H3Config, H3Response } from "./types/h3.ts"; import { kEventRes, kEventResHeaders, type H3Event } from "./event.ts"; export const kNotFound: symbol = /* @__PURE__ */ Symbol.for("h3.notFound"); @@ -12,11 +12,11 @@ export function toResponse( val: unknown, event: H3Event, config: H3Config = {}, -): Response | Promise { +): H3Response | Promise { if (typeof (val as PromiseLike)?.then === "function") { return ((val as Promise).catch?.((error) => error) || Promise.resolve(val)).then( (resolvedVal) => toResponse(resolvedVal, event, config), - ) as Promise; + ) as Promise; } const response = prepareResponse(val, event, config); diff --git a/src/types/h3.ts b/src/types/h3.ts index fae2f8c19..36d50d1b7 100644 --- a/src/types/h3.ts +++ b/src/types/h3.ts @@ -5,6 +5,7 @@ import type { MaybePromise } from "./_utils.ts"; import type { FetchHandler, ServerRequest } from "srvx"; // import type { MatchedRoute, RouterContext } from "rou3"; import type { H3Event } from "../event.ts"; +import type { Hooks as WebSocketHooks } from "crossws"; // Inlined from rou3 for type portability export interface RouterContext { @@ -21,7 +22,7 @@ export type MatchedRoute = { // https://www.rfc-editor.org/rfc/rfc7231#section-4.1 // prettier-ignore -export type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE"; +export type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE"; export interface H3Config { /** @@ -43,6 +44,16 @@ export interface H3Config { export type H3CoreConfig = Omit; +/** + * Response type returned by H3 fetch methods. + * + * Extends standard Response with an optional `.crossws` property that is present + * when the response originates from a WebSocket handler defined via `defineWebSocketHandler`. + */ +export type H3Response = Response & { + crossws?: Partial | Promise>; +}; + export type PreparedResponse = ResponseInit & { body?: BodyInit | null }; export interface H3RouteMeta { @@ -103,7 +114,7 @@ export declare class H3Core { * * Returned value is a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) Promise. */ - fetch(_request: ServerRequest): Response | Promise; + fetch(_request: ServerRequest): H3Response | Promise; /** * An h3 compatible event handler useful to compose multiple h3 app instances. @@ -111,7 +122,7 @@ export declare class H3Core { handler(event: H3Event): unknown | Promise; /** @internal */ - "~request"(request: ServerRequest, context?: H3EventContext): Response | Promise; + "~request"(request: ServerRequest, context?: H3EventContext): H3Response | Promise; /** @internal */ "~findRoute"(_event: H3Event): MatchedRoute | void; @@ -138,7 +149,7 @@ export declare class H3 extends H3Core { request: ServerRequest | URL | string, options?: RequestInit, context?: H3EventContext, - ): Response | Promise; + ): H3Response | Promise; /** * Register a global middleware. diff --git a/src/utils/ws.ts b/src/utils/ws.ts index 3cb038826..746f84a5f 100644 --- a/src/utils/ws.ts +++ b/src/utils/ws.ts @@ -2,7 +2,7 @@ import { defineHandler } from "../handler.ts"; import type { Hooks as WebSocketHooks } from "crossws"; import type { H3Event } from "../event.ts"; -import type { EventHandler } from "../types/handler.ts"; +import type { EventHandler, EventHandlerRequest } from "../types/handler.ts"; export type { Hooks as WebSocketHooks, @@ -10,6 +10,18 @@ export type { Peer as WebSocketPeer, } from "crossws"; +/** + * WebSocket response type with crossws hooks attached. + * + * This type represents a Response object augmented with WebSocket hooks. + * The `.crossws` property is used by CrossWS server plugins. + * + * @see https://h3.dev/guide/websocket + */ +export type WebSocketResponse = Response & { + crossws: Partial | Promise>; +}; + /** * Define WebSocket hooks. * @@ -28,8 +40,8 @@ export function defineWebSocketHandler( hooks: | Partial | ((event: H3Event) => Partial | Promise>), -): EventHandler { - return defineHandler(function _webSocketHandler(event) { +): EventHandler { + return defineHandler(function _webSocketHandler(event) { const crossws = typeof hooks === "function" ? hooks(event) : hooks; return Object.assign( @@ -39,6 +51,6 @@ export function defineWebSocketHandler( { crossws, }, - ); + ) as WebSocketResponse; }); } diff --git a/test/unit/types.test-d.ts b/test/unit/types.test-d.ts index e34682b33..12c6c237c 100644 --- a/test/unit/types.test-d.ts +++ b/test/unit/types.test-d.ts @@ -1,7 +1,9 @@ -import type { H3Event } from "../../src/index.ts"; +import type { H3Event, WebSocketResponse, H3Response } from "../../src/index.ts"; import { describe, it, expectTypeOf } from "vitest"; import { + H3, defineHandler, + defineWebSocketHandler, getQuery, readBody, readValidatedBody, @@ -123,4 +125,22 @@ describe("types", () => { }); }); }); + + describe("defineWebSocketHandler", () => { + it("should return a handler with a typed crossws property", () => { + const handler = defineWebSocketHandler({ message: () => {} }); + const result = handler({} as H3Event); + expectTypeOf(result).toHaveProperty("crossws"); + expectTypeOf(result.crossws).toEqualTypeOf(); + }); + }); + + describe("app.fetch", () => { + it("should return H3Response with optional crossws", async () => { + const app = new H3(); + const res = await app.fetch(new Request("http://localhost")); + expectTypeOf(res).toEqualTypeOf(); + expectTypeOf(res.crossws).toEqualTypeOf(); + }); + }); }); diff --git a/test/ws.test.ts b/test/ws.test.ts index 3c666d1f4..1215bc86a 100644 --- a/test/ws.test.ts +++ b/test/ws.test.ts @@ -17,7 +17,7 @@ describe("defineWebSocketHandler", () => { expect(res).toBeInstanceOf(Response); expect((res as Response).status).toBe(426); // expect((res as Response).statusText).toBe("Upgrade Required"); - expect((res as any).crossws).toEqual(hooks); + expect(res.crossws).toEqual(hooks); }); it("should attach the provided hooks with function argument", () => { @@ -26,6 +26,6 @@ describe("defineWebSocketHandler", () => { expect(res).toBeInstanceOf(Response); expect((res as Response).status).toBe(426); // expect((res as Response).statusText).toBe("Upgrade Required"); - expect((res as any).crossws).toEqual(hooks); + expect(res.crossws).toEqual(hooks); }); });