From da7d4806a080b7cb5daec8514759bdda48142bc1 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 11 Jul 2024 13:39:00 +0200 Subject: [PATCH] feat!: integrate router with app and `app.fetch` (#822) --- MIGRATION.md | 156 ++++++---- docs/2.utils/98.advanced.md | 11 +- package.json | 4 +- pnpm-lock.yaml | 10 +- src/{deprecated.ts => _deprecated.ts} | 15 +- src/adapters/node/{event.ts => _event.ts} | 11 +- src/adapters/node/{_internal.ts => _utils.ts} | 16 +- src/adapters/node/index.ts | 107 +++++-- src/adapters/node/utils.ts | 100 ------ src/adapters/web/_internal.ts | 37 +-- src/adapters/web/index.ts | 65 ++-- src/adapters/web/utils.ts | 173 ----------- src/app.ts | 180 ----------- src/event.ts | 23 +- src/h3.ts | 285 ++++++++++++++++++ src/handler.ts | 24 +- src/index.ts | 21 +- src/response.ts | 54 ++-- src/router.ts | 152 ---------- src/types/app.ts | 72 ----- src/types/context.ts | 7 +- src/types/h3.ts | 102 +++++++ src/types/handler.ts | 7 +- src/types/index.ts | 28 +- src/types/node.ts | 2 +- src/types/router.ts | 36 --- src/types/web.ts | 26 -- src/utils/base.ts | 41 +-- src/utils/cookie.ts | 8 - test/_setup.ts | 25 +- test/app.test.ts | 20 +- test/bench/spec.ts | 20 +- test/body.test.ts | 32 +- test/cors.test.ts | 39 +-- test/error.test.ts | 4 +- test/event.test.ts | 13 +- test/fixture/plain.ts | 15 - test/integrations.test.ts | 8 +- test/package.test.ts | 12 +- test/plain.test.ts | 72 ----- test/resolve.test.ts | 86 +++--- test/router.test.ts | 34 +-- test/session.test.ts | 6 +- test/static.test.ts | 2 +- test/status.test.ts | 61 ++-- test/utils.test.ts | 14 +- test/web.test.ts | 22 +- 47 files changed, 927 insertions(+), 1331 deletions(-) rename src/{deprecated.ts => _deprecated.ts} (93%) rename src/adapters/node/{event.ts => _event.ts} (95%) rename src/adapters/node/{_internal.ts => _utils.ts} (94%) delete mode 100644 src/adapters/node/utils.ts delete mode 100644 src/adapters/web/utils.ts delete mode 100644 src/app.ts create mode 100644 src/h3.ts delete mode 100644 src/router.ts delete mode 100644 src/types/app.ts create mode 100644 src/types/h3.ts delete mode 100644 src/types/router.ts delete mode 100644 src/types/web.ts delete mode 100644 test/fixture/plain.ts delete mode 100644 test/plain.test.ts diff --git a/MIGRATION.md b/MIGRATION.md index 6b59217d..f9f99846 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -12,42 +12,64 @@ h3 v2 includes some behavior and API changes that you need to consider applying > [!NOTE] > This is an undergoing migration guide and is not finished yet. -## Fully decoupled from Node.js +## Web adapter -We started migrating h3 towards Web standards since [v1.8](https://unjs.io/blog/2023-08-15-h3-towards-the-edge-of-the-web). h3 apps are now fully decoupled from Node.js using an adapter-based abstraction layer to support Web and Node.js runtime features and performances natively. +H3 v2 is now web native and you can directly use `app.fetch(request, init?, { context })`. -This migration significantly reduces your bundle sizes and overhead in Web-native runtimes such as [Bun](https://bun.sh/), [Deno](https://deno.com) and [Cloudflare Workers](https://workers.cloudflare.com/). +Old utils for plain handler and web handler are removed to embrace web standards. -Since v2, Event properties `event.node.{req,res}` and `event.web` is not available anymore, instead, you can use `getNodeContext(event)` and `getWebContext(event)` to access raw objects for each runtime. +## Event interface -## Response handling +Event properties `event.node.{req,res}` and `event.web` are not available anymore, instead, you can use `getNodeContext(event)` and `getWebContext(event)` utils to access raw objects for each runtime. + +`event.handler` property is removed since h3 relies on explicit responses. -You should always explicitly `return` or `throw` responses and errors from event handlers. +## Response handling -Previously h3 had `send*` utils that could interop the response handling lifecycle **anywhere** in any utility or middleware causing unpredictable application state control. To mitigate edge cases of this, previously h3 added `event.handler` property which is now gone! +You should always explicitly use `return` for response and `throw` for errors from event handlers. -If you were previously using these methods, you can replace them with `return` statements returning a text, JSON value, stream, or web `Response` (h3 smartly detects and handles them): +If you were previously using these methods, you can replace them with `return` statements returning a text, JSON, stream, or web `Response` (h3 smartly detects and handles them): -- `send(event, value)`: Use `return ` -- `sendNoContent(event)`: Use `return null` -- `sendError(event, error)`: Use `throw createError()` -- `sendStream(event, stream)`: Use `return stream` -- `sendWebResponse(event, response)`: Use `return response` +- `send(event, value)`: Migrate to `return `. +- `sendError(event, )`: Migrate to `throw createError()`. +- `sendStream(event, )`: Migrate to `return `. +- `sendWebResponse(event, )`: Migrate to `return `. Other send utils that are renamed and need explicit `return`: -- `sendIterable(event, value)`: Use `return iterable()` -- `sendRedirect(event, location, code)`: Use `return redirect(event, location, code)` -- `sendProxy(event, target)`: Use `return proxy(event, target)` +- `sendNoContent(event)` / `return null`: Migrate to `return noContent(event)`. +- `sendIterable(event, )`: Migrate to `return iterable()`. +- `sendRedirect(event, location, code)`: Migrate to `return redirect(event, location, code)`. +- `sendProxy(event, target)`: Migrate to `return proxy(event, target)`. - `handleCors(event)`: Check return value (boolean) and early `return` if handled. - `serveStatic(event, content)`: Make sure to add `return` before. ## App interface and router -- `app.use(() => handler, { lazy: true })` is no supported anymore. Instead you can use `app.use(defineLazyHabndler(() => handler), { lazy: true })` +Router functionality is now integrated in h3 app core. Instead of `createApp()` and `createRouter()` you can use `const app = createH3()`. + +Methods: + +- `app.use(handler)`: Add a global middleware. +- `app.use(route, handler)`: Add a routed middleware. +- `app.on(method, handler)` / `app.all(handler)` / `app.[METHOD](handler)`: Add a route handler. + +Running order: + +- All global middleware by the same order they added +- All routed middleware by from least specific to most specific paths (auto sorted) +- Matched route handler + +Any of middleware or route handler, can return a response. + +Other changes from v1: + +- Handlers registered with `app.use("/path", handler)` only match `/path` (not `/path/foo/bar`). For matching all subpaths like before, it should be updated to `app.use("/path/**", handler)`. +- The `event.path` received in each handler will have a full path without omitting the prefixes. use `withBase(base, handler)` utility to make prefixed app. (example: `withBase("/api", app.handler)`). +- `app.use(() => handler, { lazy: true })` is no supported anymore. Instead you can use `app.use(defineLazyEventHandler(() => handler), { lazy: true })` - `app.use(["/path1", "/path2"], ...)` and `app.use("/path", [handler1, handler2])` are not supported anymore. Instead use multiple `app.use()` calls. -- `app.use({ route, handler })` should be updated to `app.use({ prefix, handler })` (route property is used for router patterns) -- `app.resolve(path) => { route, handler }` changed to `app.resolve(method, path) => { prefix?, route?, handler }` (`prefix` is the registred prefix and `route` is the route pattern). +- Custom `match` function for `app.use` is not supported anymore (middleware can skip themselves). +- `app.resolve(path) => { route, handler }` changed to `app.resolve(method, path) => { method route, handler }` ### Router @@ -73,7 +95,7 @@ The legacy `readBody` and `readRawBody` utils are replaced with a new set of bod - Body utils won't throw an error if the incoming request has no body (or is a `GET` method for example) but instead, returns `undefined` - `readJSONBody` does not use [unjs/destr](https://destr.unjs.io) anymore. You should always filter and sanitize data coming from user to avoid [prototype-poisoning](https://medium.com/intrinsic-blog/javascript-prototype-poisoning-vulnerabilities-in-the-wild-7bc15347c96) -## Cookie and Headers +## Cookie and headers h3 migrated to leverage standard web [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) for all utils. @@ -85,65 +107,77 @@ For the [`Set-Cookie`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers h3 v2 deprecated some legacy and aliased utilities. +**App and router:** + +- `createRouter`: Migrate to `createH3` +- `createApp`: Migrate to `createH3` + **Handler:** -- `eventHandler`: Use `defineEventHandler` -- `toEventHandler`: (it is not required anymore) -- `lazyEventHandler`: Use `defineLazyEventHandler` +- `eventHandler`: Migrate to `defineEventHandler` +- `lazyEventHandler`: Migrate to `defineLazyEventHandler` +- `useBase`: Migrate to `withbase` +- `toEventHandler` / `isEventHandler`: (removed) Any function can be an event handler. **Request:** -- `getHeader`: Use `getRequestHeader` -- `getHeaders`: Use `getRequestHeaders` -- `getRequestPath`: Use `event.path` +- `getHeader`: Migrate to `getRequestHeader`. +- `getHeaders`: Migrate to `getRequestHeaders`. +- `getRequestPath`: Migrate to `event.path`. **Response:** -- `appendHeader`: Use `appendResponseHeader` -- `appendHeaders`: Use `appendResponseHeaders` -- `setHeader`: Use `setResponseHeader` -- `setHeaders` => Use `setResponseHeaders` +- `appendHeader`: Migrate to `appendResponseHeader`. +- `appendHeaders`: Migrate to `appendResponseHeaders`. +- `setHeader`: Migrate to `setResponseHeader`. +- `setHeaders`: Migrate to `setResponseHeaders`. **Node.js:** -- `defineNodeListener`: Use `defineNodeHandler` -- `fromNodeMiddleware`: Use `fromNodeHandler` -- `createEvent`: Use `fromNodeRequest` -- `toNodeListener`: Use `toNodeHandler` -- `callNodeListener`: Use `callNodeHandler` -- `promisifyNodeListener` (removed) -- `callNodeHandler`: (internal) +- `defineNodeListener`: Migrate to `defineNodeHandler`. +- `fromNodeMiddleware`: Migrate to `fromNodeHandler`. +- `toNodeListener`: Migrate to `toNodeHandler`. +- `createEvent`: (removed): Use Node.js adapter (`toNodeHandler(app)`). +- `fromNodeRequest`: (removed): Use Node.js adapter (`toNodeHandler(app)`). +- `promisifyNodeListener` (removed). +- `callNodeListener`: (removed). **Web:** -- `callWithWebRequest`: (removed) +- `fromPlainHandler`: (removed) Migrate to Web API. +- `toPlainHandler`: (removed) Migrate to Web API. +- `fromPlainRequest` (removed) Migrate to Web API or use `mockEvent` util for testing. +- `callWithPlainRequest` (removed) Migrate to Web API. +- `fromWebRequest`: (removed) Migrate to Web API. +- `callWithWebRequest`: (removed). **Body:** -- `readBody`: Use `readJSONBody` -- `readFormData`: Use `readFormDataBody` -- `readValidatedBody`: Use `readValidatedJSONBody` -- `getRequestWebStream`: Use `getBodyStream` -- `readMultipartFormData`: Migrate to `readFormDataBody` +- `readBody`: Migrate to `readJSONBody`. +- `readFormData`: Migrate to `readFormDataBody`. +- `readValidatedBody`: Migrate to `readValidatedJSONBody`. +- `getRequestWebStream`: Migrate to `getBodyStream`. +- `readMultipartFormData`: Migrate to `readFormDataBody`. -**Types:** +- **Utils:** -- `_RequestMiddleware`: Use `RequestMiddleware` -- `_ResponseMiddleware`: Use `ResponseMiddleware` -- `NodeListener`: Use `NodeHandler` -- `TypedHeaders`: Use `RequestHeaders` and `ResponseHeaders` -- `HTTPHeaderName`: Use `RequestHeaderName` and `ResponseHeaderName` -- `H3Headers`: Use native `Headers` -- `H3Response`: Use native `Response` -- `WebEventContext` -- `NodeEventContext` -- `NodePromisifiedHandler` -- `MultiPartData`: Use `FormData` -- `RouteNode`: Use `RouterEntry` - `CreateRouterOptions`: use `RouterOptions` +- `isStream`: Migrate to `instanceof ReadableStream` and `.pipe` properties for detecting Node.js `ReadableStream`. +- `isWebResponse`: Migrate to `use instanceof Response`. +- `MIMES`: (removed). -- **Utils:** +**Types:** -- `isStream`: Use `instanceof ReadableStream` and `.pipe` properties for detecting Node.js `ReadableStream` -- `isWebResponse`: Use `use instanceof Response` -- `MIMES`: Removed internal map. +- `App`: Migrate to `H3`. +- `AppOptions`: Migrate to `H3Config`. +- `_RequestMiddleware`: Migrate to `RequestMiddleware`. +- `_ResponseMiddleware`: Migrate to `ResponseMiddleware`. +- `NodeListener`: Migrate to `NodeHandler`. +- `TypedHeaders`: Migrate to `RequestHeaders` and `ResponseHeaders`. +- `HTTPHeaderName`: Migrate to `RequestHeaderName` and `ResponseHeaderName`. +- `H3Headers`: Migrate to native `Headers`. +- `H3Response`: Migrate to native `Response`. +- `MultiPartData`: Migrate to native `FormData`. +- `RouteNode`: Migrate to `RouterEntry`. + `CreateRouterOptions`: Migrate to `RouterOptions`. + +Removed type exports: `WebEventContext`, `NodeEventContext`, `NodePromisifiedHandler`, `AppUse`, `Stack`, `InputLayer`, `InputStack`, `Layer`, `Matcher`, `PlainHandler`, `PlainRequest`, `PlainReponse`, `WebHandler` diff --git a/docs/2.utils/98.advanced.md b/docs/2.utils/98.advanced.md index 57cb424d..a3552869 100644 --- a/docs/2.utils/98.advanced.md +++ b/docs/2.utils/98.advanced.md @@ -102,18 +102,17 @@ Allowed characters: horizontal tabs, spaces or visible ascii characters: https:/ -### `useBase(base, handler)` +### `withBase(base, input)` -Prefixes and executes a handler with a base path. +Returns a new event handler that removes the base url of the event before calling the original handler. **Example:** ```ts +const api = createApp() + .get("/", () => "Hello API!"); const app = createApp(); -const router = createRouter(); -const apiRouter = createRouter().get("/hello", () => "Hello API!"); -router.use("/api/**", useBase("/api", apiRouter.handler)); -app.use(router.handler); + .use("/api/**", withBase("/api", api.handler)); ``` diff --git a/package.json b/package.json index c8672541..b2ec6c30 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "build": "unbuild", "dev": "vitest", "lint": "eslint --cache . && prettier -c src test examples docs", - "lint:fix": "eslint --cache . --fix && prettier -c src test examples docs -w", + "lint:fix": "automd && eslint --cache . --fix && prettier -c src test examples docs -w", "play:bun": "bun ./test/fixture/bun.ts", "play:node": "node --import jiti/register test/fixture/node.ts", "play:plain": "node --import jiti/register test/fixture/plain.ts", @@ -42,7 +42,7 @@ "cookie-es": "^1.1.0", "iron-webcrypto": "^1.2.1", "ohash": "^1.1.3", - "rou3": "^0.2.0", + "rou3": "^0.4.0", "ufo": "^1.5.3", "uncrypto": "^0.1.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7792721a..63812585 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: ^1.1.3 version: 1.1.3 rou3: - specifier: ^0.2.0 - version: 0.2.0 + specifier: ^0.4.0 + version: 0.4.0 ufo: specifier: ^1.5.3 version: 1.5.3 @@ -2728,8 +2728,8 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rou3@0.2.0: - resolution: {integrity: sha512-pM+Zd++BIMNcfZXOsErMc8ycuYFB6Y5dmanMfyIOXwiDhYx3EDDoLPKEn5oMSl/P9s4Yi0Y2+cqzrTCw83Q7iQ==} + rou3@0.4.0: + resolution: {integrity: sha512-ETiLqy6XJXJ+254BLwdBCbb4diS0gfiTxUuYym+/TuMVsdS/EOPs07vIkXSRFVnOraFDEKGtRLniJSx441qr2Q==} run-applescript@5.0.0: resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} @@ -5851,7 +5851,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.18.0 fsevents: 2.3.3 - rou3@0.2.0: {} + rou3@0.4.0: {} run-applescript@5.0.0: dependencies: diff --git a/src/deprecated.ts b/src/_deprecated.ts similarity index 93% rename from src/deprecated.ts rename to src/_deprecated.ts index f116817b..5fbfe5a3 100644 --- a/src/deprecated.ts +++ b/src/_deprecated.ts @@ -25,7 +25,6 @@ import { import { defineNodeHandler, fromNodeHandler, - fromNodeRequest, toNodeHandler, } from "./adapters/node"; import { @@ -35,6 +34,8 @@ import { } from "./utils/body"; import { defineEventHandler, defineLazyEventHandler } from "./handler"; import { proxy } from "./utils/proxy"; +import { createH3 } from "./h3"; +import { withBase } from "./utils/base"; /** @deprecated Please use `getRequestHeader` */ export const getHeader = getRequestHeader; @@ -74,9 +75,6 @@ export const defineNodeListener = defineNodeHandler; /** @deprecated Please use `defineNodeHandler` */ export const fromNodeMiddleware = fromNodeHandler; -/** @deprecated Please use `fromNodeRequest` */ -export const createEvent = fromNodeRequest; - /** @deprecated Please use `toNodeHandler` */ export const toNodeListener = toNodeHandler; @@ -122,6 +120,15 @@ export function toEventHandler( return input; } +/** @deprecated Use `createH3()` */ +export const createApp = createH3; + +/** @deprecated Use `createH3()` */ +export const createRouter = createH3; + +/** @deprecated Use `withBase()` */ +export const useBase = withBase; + // --- Types --- /** @deprecated Please use `RequestMiddleware` */ diff --git a/src/adapters/node/event.ts b/src/adapters/node/_event.ts similarity index 95% rename from src/adapters/node/event.ts rename to src/adapters/node/_event.ts index 99108471..e3f6ea92 100644 --- a/src/adapters/node/event.ts +++ b/src/adapters/node/_event.ts @@ -2,12 +2,7 @@ import type { HTTPMethod } from "../../types"; import type { RawEvent } from "../../types/event"; import { splitCookiesString } from "../../utils/cookie"; import { NodeHeadersProxy } from "./_headers"; -import { - _normalizeHeaders, - _readBody, - _getBodyStream, - _sendResponse, -} from "./_internal"; +import { readNodeReqBody, getBodyStream } from "./_utils"; import type { NodeIncomingMessage, NodeServerResponse } from "../../types/node"; @@ -85,7 +80,7 @@ export class NodeEvent implements RawEvent { readRawBody() { if (!this._rawBody) { - this._rawBody = _readBody(this._req); + this._rawBody = readNodeReqBody(this._req); } return this._rawBody; } @@ -115,7 +110,7 @@ export class NodeEvent implements RawEvent { getBodyStream() { if (!this._bodyStream) { - this._bodyStream = _getBodyStream(this._req); + this._bodyStream = getBodyStream(this._req); } return this._bodyStream; } diff --git a/src/adapters/node/_internal.ts b/src/adapters/node/_utils.ts similarity index 94% rename from src/adapters/node/_internal.ts rename to src/adapters/node/_utils.ts index 00b66bbd..29d4b757 100644 --- a/src/adapters/node/_internal.ts +++ b/src/adapters/node/_utils.ts @@ -14,7 +14,7 @@ import { sanitizeStatusMessage, } from "../../utils/sanitize"; -export function _getBodyStream( +export function getBodyStream( req: NodeIncomingMessage, ): ReadableStream { return new ReadableStream({ @@ -32,7 +32,7 @@ export function _getBodyStream( }); } -export function _sendResponse( +export function sendNodeResponse( nodeRes: NodeServerResponse, handlerRes: ResponseBody, ): Promise { @@ -71,7 +71,7 @@ export function _sendResponse( }, }), ) - .then(() => _endResponse(nodeRes)); + .then(() => endNodeResponse(nodeRes)); } // Node.js Readable Streams @@ -93,14 +93,14 @@ export function _sendResponse( // https://react.dev/reference/react-dom/server/renderToPipeableStream (handlerRes as any).abort?.(); }); - }).then(() => _endResponse(nodeRes)); + }).then(() => endNodeResponse(nodeRes)); } // Send as string or buffer - return _endResponse(nodeRes, handlerRes); + return endNodeResponse(nodeRes, handlerRes); } -export function _endResponse( +export function endNodeResponse( res: NodeServerResponse, chunk?: any, ): Promise { @@ -109,7 +109,7 @@ export function _endResponse( }); } -export function _normalizeHeaders( +export function normalizeHeaders( headers: Record, ): Record { const normalized: Record = Object.create(null); @@ -123,7 +123,7 @@ export function _normalizeHeaders( const payloadMethods = ["PATCH", "POST", "PUT", "DELETE"] as string[]; -export function _readBody( +export function readNodeReqBody( req: NodeIncomingMessage, ): undefined | Promise { // Check if request method requires a payload diff --git a/src/adapters/node/index.ts b/src/adapters/node/index.ts index aebc620a..166244f0 100644 --- a/src/adapters/node/index.ts +++ b/src/adapters/node/index.ts @@ -1,23 +1,96 @@ +import type { + H3, + EventHandler, + EventHandlerResponse, + H3Event, +} from "../../types"; import type { NodeHandler, NodeMiddleware } from "../../types/node"; +import { _kRaw } from "../../event"; +import { EventWrapper } from "../../event"; +import { NodeEvent } from "./_event"; +import { sendNodeResponse, callNodeHandler } from "./_utils"; +import { errorToH3Response, prepareResponse } from "../../response"; -export { - // Handler - fromNodeHandler, - toNodeHandler, +/** + * Convert H3 app instance to a NodeHandler with (IncomingMessage, ServerResponse) => void signature. + */ +export function toNodeHandler(app: H3): NodeHandler { + const nodeHandler: NodeHandler = async function (nodeReq, nodeRes) { + const rawEvent = new NodeEvent(nodeReq, nodeRes); + const event = new EventWrapper(rawEvent); - // Request - fromNodeRequest, + const _res = await app.handler(event).catch((error: unknown) => error); - // Context - getNodeContext, -} from "./utils"; + if (nodeRes.headersSent || nodeRes.writableEnded) { + return; + } -export type { - NodeHandler, - NodeMiddleware, - NodeIncomingMessage, - NodeServerResponse, -} from "../../types/node"; + const resBody = await prepareResponse(event, _res, app.config); -export const defineNodeHandler = (handler: NodeHandler) => handler; -export const defineNodeMiddleware = (handler: NodeMiddleware) => handler; + await sendNodeResponse(nodeRes, resBody).catch((sendError) => { + // Possible cases: Stream canceled, headers already sent, etc. + if (nodeRes.headersSent || nodeRes.writableEnded) { + return; + } + const errRes = errorToH3Response(sendError, app.config); + if (errRes.status) { + nodeRes.statusCode = errRes.status; + } + if (errRes.statusText) { + nodeRes.statusMessage = errRes.statusText; + } + nodeRes.end(errRes.body); + }); + if (app.config.onAfterResponse) { + await app.config.onAfterResponse(event, { body: resBody }); + } + }; + + return nodeHandler; +} + +/** + * Convert a Node.js handler function (req, res, next?) to an EventHandler. + * + * **Note:** The returned event handler requires to be executed with h3 Node.js handler. + */ +export function fromNodeHandler(handler: NodeMiddleware): EventHandler; +export function fromNodeHandler(handler: NodeHandler): EventHandler; +export function fromNodeHandler( + handler: NodeHandler | NodeMiddleware, +): EventHandler { + if (typeof handler !== "function") { + throw new TypeError(`Invalid handler. It should be a function: ${handler}`); + } + return (event) => { + const nodeCtx = getNodeContext(event); + if (!nodeCtx) { + throw new Error( + "[h3] Executing Node.js middleware is not supported in this server!", + ); + } + return callNodeHandler( + handler, + nodeCtx.req, + nodeCtx.res, + ) as EventHandlerResponse; + }; +} + +export function getNodeContext( + event: H3Event, +): undefined | ReturnType { + const raw = event[_kRaw] as NodeEvent; + if (!(raw?.constructor as any)?.isNode) { + return undefined; + } + return raw.getContext(); +} + +export function defineNodeHandler(handler: NodeHandler) { + return handler; +} + +export function defineNodeMiddleware(handler: NodeMiddleware) { + return handler; +} diff --git a/src/adapters/node/utils.ts b/src/adapters/node/utils.ts deleted file mode 100644 index a139b7eb..00000000 --- a/src/adapters/node/utils.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { - App, - EventHandler, - EventHandlerResponse, - H3Event, -} from "../../types"; -import type { - NodeHandler, - NodeIncomingMessage, - NodeMiddleware, - NodeServerResponse, -} from "../../types/node"; -import { _kRaw } from "../../event"; -import { isEventHandler } from "../../handler"; -import { EventWrapper } from "../../event"; -import { NodeEvent } from "./event"; -import { _sendResponse, callNodeHandler } from "./_internal"; -import { errorToAppResponse } from "../../response"; - -/** - * Convert H3 app instance to a NodeHandler with (IncomingMessage, ServerResponse) => void signature. - */ -export function toNodeHandler(app: App): NodeHandler { - const nodeHandler: NodeHandler = async function (req, res) { - const rawEvent = new NodeEvent(req, res); - const event = new EventWrapper(rawEvent); - const appResponse = await app.handler(event); - await _sendResponse(res, appResponse).catch((sendError) => { - // Possible cases: Stream canceled, headers already sent, etc. - if (res.headersSent || res.writableEnded) { - return; - } - const errRes = errorToAppResponse(sendError, app.options); - if (errRes.status) { - res.statusCode = errRes.status; - } - if (errRes.statusText) { - res.statusMessage = errRes.statusText; - } - res.end(errRes.body); - }); - if (app.options.onAfterResponse) { - await app.options.onAfterResponse(event, { body: appResponse }); - } - }; - return nodeHandler; -} - -/** - * Convert a Node.js handler function (req, res, next?) to an EventHandler. - * - * **Note:** The returned event handler requires to be executed with h3 Node.js handler. - */ -export function fromNodeHandler(handler: NodeMiddleware): EventHandler; -export function fromNodeHandler(handler: NodeHandler): EventHandler; -export function fromNodeHandler( - handler: NodeHandler | NodeMiddleware, -): EventHandler { - if (isEventHandler(handler)) { - return handler; - } - if (typeof handler !== "function") { - throw new TypeError(`Invalid handler. It should be a function: ${handler}`); - } - return (event) => { - const nodeCtx = getNodeContext(event); - if (!nodeCtx) { - throw new Error( - "[h3] Executing Node.js middleware is not supported in this server!", - ); - } - return callNodeHandler( - handler, - nodeCtx.req, - nodeCtx.res, - ) as EventHandlerResponse; - }; -} - -/*** - * Create a H3Event object from a Node.js request and response. - */ -export function fromNodeRequest( - req: NodeIncomingMessage, - res: NodeServerResponse, -): H3Event { - const rawEvent = new NodeEvent(req, res); - const event = new EventWrapper(rawEvent); - return event; -} - -export function getNodeContext( - event: H3Event, -): undefined | ReturnType { - const raw = event[_kRaw] as NodeEvent; - if (!(raw?.constructor as any)?.isNode) { - return undefined; - } - return raw.getContext(); -} diff --git a/src/adapters/web/_internal.ts b/src/adapters/web/_internal.ts index aa388c5a..42fe7da7 100644 --- a/src/adapters/web/_internal.ts +++ b/src/adapters/web/_internal.ts @@ -1,8 +1,6 @@ import type { Readable as NodeReadableStream } from "node:stream"; -import type { App, H3EventContext, ResponseBody } from "../../types"; -import { EventWrapper, _kRaw } from "../../event"; -import { WebEvent } from "./event"; - +import type { ResponseBody } from "../../types"; +import { _kRaw } from "../../event"; type WebNormalizedResponseBody = Exclude; export function _normalizeResponse( @@ -35,34 +33,3 @@ export function _pathToRequestURL(path: string, headers?: HeadersInit): string { const protocol = h.get("x-forwarded-proto") === "https" ? "https" : "http"; return `${protocol}://${host}${path}`; } - -export const nullBodyResponses = new Set([101, 204, 205, 304]); - -export async function appFetch( - app: App, - request: Request, - context?: H3EventContext, -): Promise<{ - body: WebNormalizedResponseBody; - status: Response["status"]; - statusText: Response["statusText"]; - headers: Headers; -}> { - const rawEvent = new WebEvent(request); - const event = new EventWrapper(rawEvent, context); - - const _appResponseBody = await app.handler(event); - - // https://developer.mozilla.org/en-US/docs/Web/API/Response/body - const responseBody = - nullBodyResponses.has(rawEvent.responseCode!) || request.method === "HEAD" - ? null - : _normalizeResponse(_appResponseBody); - - return { - status: rawEvent.responseCode, - statusText: rawEvent.responseMessage, - headers: rawEvent._responseHeaders, - body: responseBody, - }; -} diff --git a/src/adapters/web/index.ts b/src/adapters/web/index.ts index 9c123561..8b4d208e 100644 --- a/src/adapters/web/index.ts +++ b/src/adapters/web/index.ts @@ -1,26 +1,49 @@ -export { - // --Web-- +import type { H3, EventHandler, H3Event, H3EventContext } from "../../types"; +import { _kRaw } from "../../event"; +import { WebEvent } from "./event"; +import { _normalizeResponse, _pathToRequestURL } from "./_internal"; - // Web Handler - fromWebHandler, - toWebHandler, +type WebHandler = ( + request: Request, + context?: H3EventContext, +) => Promise; - // Web Request - fromWebRequest, - toWebRequest, +export function toWebHandler(app: H3): WebHandler { + return (request, context) => { + return app.fetch(request, { h3: context }); + }; +} - // Web Context - getWebContext, +export function fromWebHandler(handler: WebHandler): EventHandler { + return (event) => handler(toWebRequest(event), event.context); +} - // --Plain-- +/** + * Convert an H3Event object to a web Request object. + * + */ +export function toWebRequest(event: H3Event): Request { + return ( + (event[_kRaw] as WebEvent)._req || + new Request( + _pathToRequestURL(event[_kRaw].path, event[_kRaw].getHeaders()), + { + // @ts-ignore Undici option + duplex: "half", + method: event[_kRaw].method, + headers: event[_kRaw].getHeaders(), + body: event[_kRaw].getBodyStream(), + }, + ) + ); +} - // Plain Handler - fromPlainHandler, - toPlainHandler, - - // Plain Request - fromPlainRequest, - - // Call - callWithPlainRequest, -} from "./utils"; +export function getWebContext( + event: H3Event, +): undefined | ReturnType { + const raw = event[_kRaw] as WebEvent; + if (!(raw?.constructor as any)?.isWeb) { + return undefined; + } + return raw.getContext(); +} diff --git a/src/adapters/web/utils.ts b/src/adapters/web/utils.ts deleted file mode 100644 index 3f8c3769..00000000 --- a/src/adapters/web/utils.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { App, EventHandler, H3Event, H3EventContext } from "../../types"; -import type { - WebHandler, - PlainHandler, - PlainRequest, - PlainResponse, -} from "../../types/web"; -import { EventWrapper, _kRaw } from "../../event"; -import { WebEvent } from "./event"; -import { _normalizeResponse, _pathToRequestURL, appFetch } from "./_internal"; - -/** - * Convert H3 app instance to a WebHandler with (Request, H3EventContext) => Promise signature. - */ -export function toWebHandler(app: App): WebHandler { - const webHandler: WebHandler = async (request, context) => { - const res = await appFetch(app, request, context); - return new Response(res.body, res); - }; - return webHandler; -} - -export function fromWebHandler(handler: WebHandler): EventHandler { - return (event) => handler(toWebRequest(event), event.context); -} - -/** - * Convert an H3Event object to a web Request object. - * - */ -export function toWebRequest(event: H3Event): Request { - return ( - (event[_kRaw] as WebEvent)._req || - new Request( - _pathToRequestURL(event[_kRaw].path, event[_kRaw].getHeaders()), - { - // @ts-ignore Undici option - duplex: "half", - method: event[_kRaw].method, - headers: event[_kRaw].getHeaders(), - body: event[_kRaw].getBodyStream(), - }, - ) - ); -} - -/** - * Create an H3Event object from a web Request object. - */ -export function fromWebRequest( - request: Request, - context?: H3EventContext, -): H3Event { - const rawEvent = new WebEvent(request); - const event = new EventWrapper(rawEvent); - if (context) { - Object.assign(event.context, context); - } - return event; -} - -export function getWebContext( - event: H3Event, -): undefined | ReturnType { - const raw = event[_kRaw] as WebEvent; - if (!(raw?.constructor as any)?.isWeb) { - return undefined; - } - return raw.getContext(); -} - -// ---------------------------- -// Plain -// ---------------------------- - -/** - * Convert H3 app instance to a PlainHandler with (PlainRequest, H3EventContext) => Promise signature. - */ -export function toPlainHandler(app: App) { - const handler: PlainHandler = async (request, context) => { - return callWithPlainRequest(app, request, context); - }; - return handler; -} - -/** - * Convert a PlainHandler to an EventHandler. - */ -export function fromPlainHandler(handler: PlainHandler): EventHandler { - return async (event) => { - const res = await handler( - { - get method() { - return event.method; - }, - get path() { - return event.path; - }, - get headers() { - return event[_kRaw].getHeaders(); - }, - get body() { - return event[_kRaw].getBodyStream(); - }, - }, - event.context, - ); - event[_kRaw].responseCode = res.status; - event[_kRaw].responseMessage = res.statusText; - - const hasSetCookie = res.setCookie?.length > 0; - for (const [key, value] of Object.entries(res.headers)) { - if (key === "set-cookie" && hasSetCookie) { - continue; - } - event[_kRaw].setResponseHeader(key, value); - } - if (res.setCookie?.length > 0) { - for (const cookie of res.setCookie) { - event[_kRaw].appendResponseHeader("set-cookie", cookie); - } - } - - return res.body; - }; -} - -/** - * Create an H3Event object from a plain request object. - */ -export function fromPlainRequest( - request: PlainRequest, - context?: H3EventContext, -): H3Event { - return fromWebRequest( - new Request(_pathToRequestURL(request.path, request.headers), { - method: request.method, - headers: request.headers, - body: request.body, - }), - context, - ); -} - -export async function callWithPlainRequest( - app: App, - request: PlainRequest, - context?: H3EventContext, -): Promise { - const res = await appFetch( - app, - new Request(_pathToRequestURL(request.path, request.headers), { - method: request.method, - headers: request.headers, - body: request.body, - }), - context, - ); - - const setCookie = res.headers.getSetCookie(); - const headersObject = Object.fromEntries(res.headers.entries()); - if (setCookie.length > 0) { - headersObject["set-cookie"] = setCookie.join(", "); - } - - return { - status: res.status, - statusText: res.statusText, - headers: headersObject, - setCookie: setCookie, - body: res.body, - }; -} diff --git a/src/app.ts b/src/app.ts deleted file mode 100644 index 363b7a9d..00000000 --- a/src/app.ts +++ /dev/null @@ -1,180 +0,0 @@ -import type { - App, - Stack, - AppOptions, - EventHandler, - InputLayer, - HTTPMethod, - H3Event, - Layer, -} from "./types"; -import { _kRaw } from "./event"; -import { - getPathname, - joinURL, - withoutTrailingSlash, -} from "./utils/internal/path"; -import { prepareResponse } from "./response"; -import { createError } from "./error"; -import { ResolvedEventHandler } from "./types/handler"; - -/** - * Create a new h3 app instance. - */ -export function createApp(options: AppOptions = {}): App { - const app = new H3App(options); - return app; -} - -class H3App implements App { - stack: Stack = []; - options: AppOptions; - - constructor(options: AppOptions) { - this.options = options; - this.handler = this.handler.bind(this); - (this.handler as EventHandler).__resolve__ = this.resolve.bind(this); - } - - get websocket() { - return { - ...this.options.websocket, - resolve: async (info: { url: string; method?: string }) => { - const pathname = getPathname(info.url || "/"); - const method = (info.method || "GET") as HTTPMethod; - const resolved = await this.resolve(method, pathname); - return resolved?.handler?.__websocket__ || {}; - }, - }; - } - - async handler(event: H3Event) { - try { - // Keep a copy of incoming url - const _reqPath = event[_kRaw].path || "/"; - - // Layer path is the path without the prefix - let _layerPath: string; - - // Call onRequest hook - if (this.options.onRequest) { - await this.options.onRequest(event); - } - - // Run through stack - for (const layer of this.stack) { - // 1. Remove prefix from path - if (layer.prefix.length > 1) { - if (!_reqPath.startsWith(layer.prefix)) { - continue; - } - _layerPath = _reqPath.slice(layer.prefix.length) || "/"; - } else { - _layerPath = _reqPath; - } - - // 2. Custom matcher - if (layer.match && !layer.match(_layerPath, event)) { - continue; - } - - // 3. Update event path with layer path - event[_kRaw].path = _layerPath; - - // 4. Handle request - const val = await layer.handler(event); - - // 5. Handle response - const _body = val === undefined ? undefined : await val; - if (_body !== undefined) { - return prepareResponse(event, _body, this.options); - } - } - - // Throw 404 is no handler in the stack responded - throw createError({ - statusCode: 404, - statusMessage: `Cannot find any path matching ${event.path || "/"}.`, - }); - } catch (error: unknown) { - return prepareResponse(event, error, this.options); - } - } - - async resolve( - method: HTTPMethod, - path: string, - ): Promise | undefined> { - let _layerPath: string; - for (const layer of this.stack) { - if (layer.prefix === "/" && !layer.handler.__resolve__) { - continue; - } - if (!path.startsWith(layer.prefix)) { - continue; - } - _layerPath = path.slice(layer.prefix.length) || "/"; - if (layer.match && !layer.match(_layerPath, undefined)) { - continue; - } - if (!layer.handler.__resolve__) { - return { - prefix: layer.prefix, - handler: layer.handler, - method, - }; - } - const _resolved = await layer.handler.__resolve__(method, _layerPath); - if (!_resolved) { - continue; - } - const prefix = joinURL(layer.prefix || "/", _resolved.prefix || "/"); - return { - ..._resolved, - method: _resolved.method || method, - prefix, - handler: _resolved.handler || layer.handler, - }; - } - } - - use(route: string, handler: EventHandler, options?: Partial): App; - use(handler: EventHandler, options?: Partial): App; - use(options: InputLayer): App; - use( - arg1: string | EventHandler | InputLayer, - arg2?: EventHandler | EventHandler[] | Partial, - arg3?: Partial, - ) { - const arg1T = typeof arg1; - if (arg1T === "string") { - this.stack.push( - _normalizeLayer({ - ...arg3, - prefix: arg1 as string, - handler: arg2 as EventHandler, - }), - ); - } else if (arg1T === "function") { - this.stack.push( - _normalizeLayer({ ...arg2, handler: arg1 as EventHandler }), - ); - } else { - this.stack.push(_normalizeLayer(arg1 as InputLayer)); - } - return this; - } -} - -function _normalizeLayer(input: InputLayer): Layer { - let handler = input.handler; - if (handler.handler) { - handler = handler.handler; - } - - return { - prefix: withoutTrailingSlash(input.prefix), - match: input.match, - handler, - } as Layer; -} diff --git a/src/event.ts b/src/event.ts index 3e8bb832..d24efbc8 100644 --- a/src/event.ts +++ b/src/event.ts @@ -1,7 +1,8 @@ +import { WebEvent } from "./adapters/web/event"; import type { H3EventContext, H3Event } from "./types"; import { RawEvent } from "./types/event"; -export const _kRaw: unique symbol = Symbol.for("h3.internal.raw"); +export const _kRaw: unique symbol = Symbol.for("h3.rawEvent"); export class EventWrapper implements H3Event { static "__is_event__" = true; @@ -51,3 +52,23 @@ export function isEvent(input: any): input is H3Event { input.__is_event__ /* Backward compatibility with h3 v1 */ ); } + +export function mockEvent( + _request: string | URL | Request, + options?: RequestInit & { h3?: H3EventContext }, +) { + let request: Request; + if (typeof _request === "string") { + let url = _request; + if (url[0] === "/") { + url = `http://localhost${url}`; + } + request = new Request(url, options); + } else if (options || _request instanceof URL) { + request = new Request(_request, options); + } else { + request = _request; + } + const webEvent = new WebEvent(request); + return new EventWrapper(webEvent, options?.h3); +} diff --git a/src/h3.ts b/src/h3.ts new file mode 100644 index 00000000..04cc840f --- /dev/null +++ b/src/h3.ts @@ -0,0 +1,285 @@ +import type { + H3, + H3Config, + EventHandler, + HTTPMethod, + H3Event, + H3EventContext, + EventHandlerRequest, +} from "./types"; +import type { H3Route } from "./types/h3"; +import { + createRouter, + addRoute, + findAllRoutes, + RouterContext, + findRoute, +} from "rou3"; +import { _kRaw, EventWrapper } from "./event"; +import { getPathname, joinURL } from "./utils/internal/path"; +import { ResolvedEventHandler } from "./types/handler"; +import { WebEvent } from "./adapters/web/event"; +import { _kNotFound, prepareResponse } from "./response"; +import { _normalizeResponse } from "./adapters/web/_internal"; + +/** + * Create a new h3 instance. + */ +export function createH3(config: H3Config = {}): H3 { + return new _H3(config); +} + +class _H3 implements H3 { + config: H3Config; + + _middleware?: H3Route[]; + _mRouter?: RouterContext; + _router?: RouterContext; + + handler: EventHandler>; + + constructor(config: H3Config) { + this.config = config; + + this.fetch = this.fetch.bind(this); + + this.handler = Object.assign((event: H3Event) => this._handler(event), < + Partial + >{ + resolve: (method, path) => this.resolve(method, path), + websocket: this.config.websocket, + }); + } + + get websocket() { + return { + ...this.config.websocket, + resolve: async (info: { url: string; method?: string }) => { + const pathname = getPathname(info.url || "/"); + const method = (info.method || "GET") as HTTPMethod; + const resolved = await this.resolve(method, pathname); + return resolved?.handler?.websocket?.hooks || {}; + }, + }; + } + + async fetch( + _request: Request | URL | string, + options?: RequestInit & { h3?: H3EventContext }, + ): Promise { + // Normalize request + let request: Request; + if (typeof _request === "string") { + let url = _request; + if (url[0] === "/") { + url = `http://localhost${url}`; + } + request = new Request(url, options); + } else if (options || _request instanceof URL) { + request = new Request(_request, options); + } else { + request = _request; + } + + // Create event context + const rawEvent = new WebEvent(request); + const event = new EventWrapper(rawEvent, options?.h3); + + // Handle request + const _res = await this._handler(event) + .catch((error: any) => error) + .then((res) => prepareResponse(event, res, this.config)); + + // Create response + const status = rawEvent.responseCode; + // prettier-ignore + // https://developer.mozilla.org/en-US/docs/Web/API/Response/body + const isNullBody = status === 101 || status === 204 || status === 205 || status === 304 || request.method === "HEAD"; + return new Response(isNullBody ? null : _normalizeResponse(_res), { + status, + statusText: rawEvent.responseMessage, + headers: rawEvent.getResponseHeaders(), + }); + } + + async _handler(event: H3Event) { + const _path = event.path; + const _queryIndex = _path.indexOf("?"); + const pathname = _queryIndex === -1 ? _path : _path.slice(0, _queryIndex); + + // 1. Hooks + if (this.config.onRequest) { + await this.config.onRequest(event); + } + + // 2. Global middleware + const _middleware = this._middleware; + if (_middleware) { + for (const entry of _middleware) { + const result = await entry.handler(event); + if (result !== undefined && result !== _kNotFound) { + return result; + } + } + } + + // 3. Middleware router + const _mRouter = this._mRouter; + if (_mRouter) { + const matches = findAllRoutes(_mRouter, event.method, pathname); + for (const match of matches) { + const result = await match.data.handler(event); + if (result !== undefined && result !== _kNotFound) { + return result; + } + } + } + + // 4. Route handler + if (this._router) { + const match = findRoute(this._router, event.method, pathname)?.[0]; + if (match) { + event.context.params = match.params; + event.context.matchedRoute = match.data; + return match.data.handler(event); + } + } + + // 5. 404 + return _kNotFound; + } + + async resolve( + method: HTTPMethod, + path: string, + ): Promise { + const match = + (this._mRouter && findRoute(this._mRouter, method, path)?.pop()) || + (this._router && findRoute(this._router, method, path)?.pop()); + + if (!match) { + return undefined; + } + + const resolved = { + route: match.data.route, + handler: match.data.handler, + params: match.params, + }; + + while (resolved.handler?.resolve) { + const _resolved = await resolved.handler.resolve(method, path); + if (!_resolved) { + break; + } + if (_resolved.route) { + let base = resolved.route || ""; + if (base.endsWith("/**")) { + base = base.slice(0, -3); + } + resolved.route = joinURL(base, _resolved.route); + } + if (_resolved.params) { + resolved.params = { ...resolved.params, ..._resolved.params }; + } + if (!_resolved.handler || _resolved.handler === resolved.handler) { + break; + } + resolved.handler = _resolved.handler; + } + + return resolved; + } + + all(route: string, handler: EventHandler | H3) { + return this.on("", route, handler); + } + get(route: string, handler: EventHandler | H3) { + return this.on("GET", route, handler); + } + post(route: string, handler: EventHandler | H3) { + return this.on("POST", route, handler); + } + put(route: string, handler: EventHandler | H3) { + return this.on("PUT", route, handler); + } + delete(route: string, handler: EventHandler | H3) { + return this.on("DELETE", route, handler); + } + patch(route: string, handler: EventHandler | H3) { + return this.on("PATCH", route, handler); + } + head(route: string, handler: EventHandler | H3) { + return this.on("HEAD", route, handler); + } + options(route: string, handler: EventHandler | H3) { + return this.on("OPTIONS", route, handler); + } + connect(route: string, handler: EventHandler | H3) { + return this.on("CONNECT", route, handler); + } + trace(route: string, handler: EventHandler | H3) { + return this.on("TRACE", route, handler); + } + on( + method: HTTPMethod | Lowercase | "", + route: string, + handler: EventHandler | H3, + ): this { + if (!this._router) { + this._router = createRouter(); + } + const _method = (method || "").toUpperCase(); + const _handler = (handler as H3)?.handler || handler; + addRoute(this._router, _method, route, { + method: _method, + route, + handler: _handler, + }); + return this; + } + + use( + arg1: string | EventHandler | H3 | H3Route, + arg2?: EventHandler | H3 | Partial, + arg3?: Partial, + ) { + const arg1T = typeof arg1; + const entry = {} as H3Route; + let _handler: EventHandler | H3; + if (arg1T === "string") { + // (route, handler, details) + entry.route = (arg1 as string) || arg3?.route; + entry.method = arg3?.method as HTTPMethod; + _handler = (arg2 as EventHandler | H3) || arg3?.handler; + } else if (arg1T === "function") { + // (handler, details) + entry.route = (arg2 as H3Route)?.route; + entry.method = (arg2 as H3Route)?.method; + _handler = (arg1 as EventHandler | H3) || (arg2 as H3Route)?.handler; + } else { + // (details) + entry.route = (arg1 as H3Route).route; + entry.method = (arg1 as H3Route).method; + _handler = (arg1 as H3Route).handler; + } + + entry.handler = (_handler as H3)?.handler || _handler; + entry.method = (entry.method || "").toUpperCase() as HTTPMethod; + + if (entry.route) { + // Routed middleware/handler + if (!this._mRouter) { + this._mRouter = createRouter(); + } + addRoute(this._mRouter, entry.method, entry.route, entry); + } else { + // Global middleware + if (!this._middleware) { + this._middleware = []; + } + this._middleware.push(entry); + } + return this; + } +} diff --git a/src/handler.ts b/src/handler.ts index 01da32b6..e3b73f0a 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -11,7 +11,6 @@ import type { EventHandlerObject, } from "./types"; import { _kRaw } from "./event"; -import { hasProp } from "./utils/internal/object"; type _EventHandlerHooks< Request extends EventHandlerRequest = EventHandlerRequest, @@ -53,7 +52,6 @@ export function defineEventHandler< ): EventHandler { // Function Syntax if (typeof handler === "function") { - handler.__is_handler__ = true; return handler; } // Object Syntax @@ -61,12 +59,11 @@ export function defineEventHandler< onRequest: _normalizeArray(handler.onRequest), onBeforeResponse: _normalizeArray(handler.onBeforeResponse), }; - const _handler: EventHandler = (event: H3Event) => { + const _handler: EventHandler = (event) => { return _callHandler(event, handler.handler, _hooks); }; - _handler.__is_handler__ = true; - _handler.__resolve__ = handler.handler.__resolve__; - _handler.__websocket__ = handler.websocket; + _handler.resolve = handler.handler.resolve; + _handler.websocket = { hooks: handler.websocket }; return _handler as EventHandler; } @@ -109,15 +106,6 @@ export function defineResponseMiddleware< return fn; } -/** - * Checks if any kind of input is an event handler. - * @param input The input to check. - * @returns True if the input is an event handler, false otherwise. - */ -export function isEventHandler(input: any): input is EventHandler { - return hasProp(input, "__is_handler__"); -} - export function dynamicEventHandler( initial?: EventHandler, ): DynamicEventHandler { @@ -166,10 +154,10 @@ export function defineLazyEventHandler( return resolveHandler().then((r) => r.handler(event)); }; - handler.__resolve__ = (method, path) => + handler.resolve = (method, path) => Promise.resolve( - resolveHandler().then((r) => - r.handler.__resolve__ ? r.handler.__resolve__(method, path) : r, + resolveHandler().then(({ handler }) => + handler.resolve ? handler.resolve(method, path) : { handler }, ), ); diff --git a/src/index.ts b/src/index.ts index 4094a4cc..e8e4bee1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,14 @@ // Types export * from "./types"; -// App -export { createApp } from "./app"; +// H3 +export { createH3 } from "./h3"; // Event -export { isEvent } from "./event"; +export { isEvent, mockEvent } from "./event"; // Handler export { - isEventHandler, defineEventHandler, defineLazyEventHandler, dynamicEventHandler, @@ -20,9 +19,6 @@ export { // Error export { createError, isError } from "./error"; -// Router -export { createRouter } from "./router"; - // ---- Adapters ---- // Node @@ -30,7 +26,6 @@ export { getNodeContext, fromNodeHandler, toNodeHandler, - fromNodeRequest, defineNodeHandler, defineNodeMiddleware, } from "./adapters/node"; @@ -40,12 +35,7 @@ export { getWebContext, fromWebHandler, toWebHandler, - fromWebRequest, toWebRequest, - fromPlainHandler, - toPlainHandler, - fromPlainRequest, - callWithPlainRequest, } from "./adapters/web"; // ------ Utils ------ @@ -85,6 +75,7 @@ export { getResponseStatusText, redirect, iterable, + noContent, } from "./utils/response"; // Proxy @@ -127,7 +118,7 @@ export { handleCacheHeaders } from "./utils/cache"; export { serveStatic } from "./utils/static"; // Base -export { useBase } from "./utils/base"; +export { withBase } from "./utils/base"; // Session export { @@ -156,4 +147,4 @@ export { defineWebSocketHandler, defineWebSocket } from "./utils/ws"; // ---- Deprecated ---- -export * from "./deprecated"; +export * from "./_deprecated"; diff --git a/src/response.ts b/src/response.ts index 658a28c1..2e5f8811 100644 --- a/src/response.ts +++ b/src/response.ts @@ -1,30 +1,32 @@ -import type { AppOptions, H3Event, ResponseBody } from "./types"; -import type { AppResponse, H3Error } from "./types/app"; +import type { H3Config, H3Event, ResponseBody } from "./types"; +import type { H3Response, H3Error } from "./types/h3"; import { createError } from "./error"; import { isJSONSerializable } from "./utils/internal/object"; import { MIMES } from "./utils/internal/consts"; import { _kRaw } from "./event"; +export const _kNotFound = Symbol.for("h3.notFound"); + export async function prepareResponse( event: H3Event, body: unknown, - options: AppOptions, + config: H3Config, ) { - const res = await _normalizeResponseBody(body, options); + const res = await normalizeResponseBody(event, body, config); if (res.error) { if (res.error.unhandled) { console.error("[h3] Unhandled Error:", res.error); } - if (options.onError) { + if (config.onError) { try { - await options.onError(res.error, event); + await config.onError(res.error, event); } catch (hookError) { console.error("[h3] Error while calling `onError` hook:", hookError); } } } - if (options.onBeforeResponse) { - await options.onBeforeResponse(event, res); + if (config.onBeforeResponse) { + await config.onBeforeResponse(event, res); } if (res.contentType && !event[_kRaw].getResponseHeader("content-type")) { event[_kRaw].setResponseHeader("content-type", res.contentType); @@ -43,13 +45,25 @@ export async function prepareResponse( return res.body; } -function _normalizeResponseBody( +function normalizeResponseBody( + event: H3Event, val: unknown, - options: AppOptions, -): AppResponse | Promise { + config: H3Config, +): H3Response | Promise { // Empty Content if (val === null || val === undefined) { - return { body: "", status: 204 }; + return { body: "" }; + } + + // Not found + if (val === _kNotFound) { + return errorToH3Response( + { + statusCode: 404, + statusMessage: `Cannot find any route matching [${event.method}] ${event.path}`, + }, + config, + ); } const valType = typeof val; @@ -66,13 +80,13 @@ function _normalizeResponseBody( // Error (should be before JSON) if (val instanceof Error) { - return errorToAppResponse(val, options); + return errorToH3Response(val, config); } // JSON if (isJSONSerializable(val, valType)) { return { - body: JSON.stringify(val, undefined, options.debug ? 2 : undefined), + body: JSON.stringify(val, undefined, config.debug ? 2 : undefined), contentType: MIMES.json, }; } @@ -104,12 +118,12 @@ function _normalizeResponseBody( // Symbol or Function is not supported if (valType === "symbol" || valType === "function") { - return errorToAppResponse( + return errorToH3Response( { statusCode: 500, statusMessage: `[h3] Cannot send ${valType} as response.`, }, - options, + config, ); } @@ -118,10 +132,10 @@ function _normalizeResponseBody( }; } -export function errorToAppResponse( +export function errorToH3Response( _error: Partial | Error, - options: AppOptions, -): AppResponse { + config: H3Config, +): H3Response { const error = createError(_error as H3Error); return { error, @@ -133,7 +147,7 @@ export function errorToAppResponse( statusMessage: error.statusMessage, data: error.data, stack: - options.debug && error.stack + config.debug && error.stack ? error.stack.split("\n").map((l) => l.trim()) : undefined, }), diff --git a/src/router.ts b/src/router.ts deleted file mode 100644 index 9d62e398..00000000 --- a/src/router.ts +++ /dev/null @@ -1,152 +0,0 @@ -import type { - RouterOptions, - EventHandler, - HTTPMethod, - RouterEntry, - Router, - H3Event, -} from "./types"; -import { - type RouterContext, - createRouter as _createRouter, - findRoute, - addRoute, -} from "rou3"; -import { createError } from "./error"; - -/** - * Create a new h3 router instance. - */ -export function createRouter(opts: RouterOptions = {}): Router { - return new H3Router(opts); -} - -class H3Router implements Router { - _router: RouterContext; - _options: RouterOptions; - constructor(opts: RouterOptions = {}) { - this._router = _createRouter(); - this._options = opts; - this.handler = this.handler.bind(this); - (this.handler as EventHandler).__resolve__ = this._resolveRoute.bind(this); - } - - all(path: string, handler: EventHandler) { - return this.add("", path, handler); - } - - use(path: string, handler: EventHandler) { - return this.all(path, handler); - } - - get(path: string, handler: EventHandler) { - return this.add("GET", path, handler); - } - - post(path: string, handler: EventHandler) { - return this.add("POST", path, handler); - } - - put(path: string, handler: EventHandler) { - return this.add("PUT", path, handler); - } - - delete(path: string, handler: EventHandler) { - return this.add("DELETE", path, handler); - } - - patch(path: string, handler: EventHandler) { - return this.add("PATCH", path, handler); - } - - head(path: string, handler: EventHandler) { - return this.add("HEAD", path, handler); - } - - options(path: string, handler: EventHandler) { - return this.add("OPTIONS", path, handler); - } - - connect(path: string, handler: EventHandler) { - return this.add("CONNECT", path, handler); - } - - trace(path: string, handler: EventHandler) { - return this.add("TRACE", path, handler); - } - - add( - method: HTTPMethod | Lowercase | "", - path: string, - handler: EventHandler, - ): this { - const _method = (method || "").toUpperCase(); - addRoute(this._router, _method, path, { - method: _method, - route: path, - handler, - }); - return this; - } - - handler(event: H3Event) { - // Match handler - const match = this._findRoute( - event.method.toUpperCase() as HTTPMethod, - event.path, - ); - - // No match (method or route) - if (!match) { - if (this._options.preemptive) { - throw createError({ - statusCode: 404, - name: "Not Found", - statusMessage: `Cannot find any route matching [${event.method}] ${event.path || "/"}`, - }); - } else { - return; // Let app match other handlers - } - } - - // Add matched route and params to the context - event.context.matchedRoute = match.data; - event.context.params = match.params || Object.create(null); - - // Call handler - return Promise.resolve(match.data.handler(event)).then((res) => { - if (res === undefined && this._options.preemptive) { - return null; // Send empty content - } - return res; - }); - } - - _findRoute(method: HTTPMethod = "GET", path = "/") { - // Remove query parameters for matching - const qIndex = path.indexOf("?"); - if (qIndex !== -1) { - path = path.slice(0, Math.max(0, qIndex)); - } - return findRoute(this._router, method, path) as - | { data: RouterEntry; params?: Record } - | undefined; - } - - async _resolveRoute(method: HTTPMethod = "GET", path: string) { - const match = this._findRoute(method, path); - if (!match) { - return; - } - const resolved = { - route: match.data.route, - handler: match.data.handler, - params: match.params, - }; - if (resolved.handler.__resolve__) { - const _resolved = await resolved.handler.__resolve__(method, path); - return { ...resolved, ..._resolved }; - } - return resolved; - } -} diff --git a/src/types/app.ts b/src/types/app.ts deleted file mode 100644 index 35f8cb93..00000000 --- a/src/types/app.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { AdapterOptions as CrossWSAdapterOptions } from "crossws"; -import type { H3Event } from "./event"; -import type { - EventHandler, - EventHandlerRequest, - EventHandlerResolver, - ResponseBody, -} from "./handler"; -import type { H3Error } from "../error"; - -type MaybePromise = T | Promise; - -export type { H3Error } from "../error"; - -export interface Layer { - prefix: string; - match?: Matcher; - handler: EventHandler; -} - -export type Stack = Layer[]; - -export interface InputLayer { - prefix?: string; - match?: Matcher; - handler: EventHandler & { handler?: EventHandler }; -} - -export type InputStack = InputLayer[]; - -export type Matcher = (path: string, event?: H3Event) => boolean; - -export interface AppResponse { - error?: H3Error; - body: ResponseBody; - contentType?: string; - headers?: Headers; - status?: number; - statusText?: string; -} - -export interface AppUse { - (prefix: string, handler: EventHandler, options?: Partial): App; - (handler: EventHandler, options?: Partial): App; - (options: InputLayer): App; -} - -export type WebSocketOptions = CrossWSAdapterOptions; - -export interface AppOptions { - debug?: boolean; - onError?: (error: H3Error, event: H3Event) => MaybePromise; - onRequest?: (event: H3Event) => MaybePromise; - onBeforeResponse?: ( - event: H3Event, - response: AppResponse, - ) => MaybePromise; - onAfterResponse?: ( - event: H3Event, - response?: AppResponse, - ) => MaybePromise; - websocket?: WebSocketOptions; -} - -export interface App { - stack: Stack; - handler: EventHandler>; - options: AppOptions; - use: AppUse; - resolve: EventHandlerResolver; - readonly websocket: WebSocketOptions; -} diff --git a/src/types/context.ts b/src/types/context.ts index 3c9d5d1d..67171662 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -1,17 +1,20 @@ +import type { H3Route } from "./h3"; import type { Session } from "./utils/session"; -import type { RouterEntry } from "./router"; export interface H3EventContext extends Record { /* Matched router parameters */ params?: Record; + /** * Matched router Node * * @experimental The object structure may change in non-major version. */ - matchedRoute?: RouterEntry; + matchedRoute?: H3Route; + /* Cached session data */ sessions?: Record; + /* Trusted IP Address of client */ clientAddress?: string; } diff --git a/src/types/h3.ts b/src/types/h3.ts new file mode 100644 index 00000000..523f73f6 --- /dev/null +++ b/src/types/h3.ts @@ -0,0 +1,102 @@ +import type * as crossws from "crossws"; +import type { H3Event } from "./event"; +import type { + EventHandler, + EventHandlerRequest, + ResolvedEventHandler, + ResponseBody, +} from "./handler"; +import type { H3Error } from "../error"; +import type { HTTPMethod } from "./http"; +import { H3EventContext } from "./context"; + +export type { H3Error } from "../error"; + +type MaybePromise = T | Promise; + +export interface H3Response { + error?: H3Error; + body: ResponseBody; + contentType?: string; + headers?: Headers; + status?: number; + statusText?: string; +} + +export interface H3Config { + debug?: boolean; + websocket?: WebSocketOptions; + + onError?: (error: H3Error, event: H3Event) => MaybePromise; + onRequest?: (event: H3Event) => MaybePromise; + onBeforeResponse?: ( + event: H3Event, + response: H3Response, + ) => MaybePromise; + onAfterResponse?: ( + event: H3Event, + response?: H3Response, + ) => MaybePromise; +} + +export interface WebSocketOptions { + resolve?: crossws.ResolveHooks; + hooks?: Partial; + adapterHooks?: Partial; +} + +export interface H3Route { + route?: string; + method?: HTTPMethod; + handler: EventHandler; +} + +type AddRoute = (route: string, handler: EventHandler | H3) => H3; + +export interface H3 { + readonly config: H3Config; + + /** websocket options */ + websocket: WebSocketOptions; + + /** fetch request */ + fetch( + request: Request | URL | string, + options?: RequestInit & { h3?: H3EventContext }, + ): Promise; + + /** main event handler */ + handler: EventHandler>; + _handler: EventHandler>; + + /** resolve event handler */ + resolve: ( + method: HTTPMethod, + path: string, + ) => Promise; + + /** add middleware */ + use( + route: string, + handler: EventHandler | H3, + details?: Partial, + ): H3; + use(handler: EventHandler | H3, details?: Partial): H3; + use(details: H3Route): H3; + + on: ( + method: "" | HTTPMethod | Lowercase, + path: string, + handler: EventHandler | H3, + ) => H3; + all: AddRoute; + get: AddRoute; + post: AddRoute; + put: AddRoute; + delete: AddRoute; + patch: AddRoute; + head: AddRoute; + options: AddRoute; + connect: AddRoute; + trace: AddRoute; +} diff --git a/src/types/handler.ts b/src/types/handler.ts index d232a4ff..01897521 100644 --- a/src/types/handler.ts +++ b/src/types/handler.ts @@ -3,6 +3,7 @@ import type { QueryObject } from "ufo"; import type { H3Event } from "./event"; import type { Hooks as WSHooks } from "crossws"; import type { HTTPMethod } from "./http"; +import type { H3 } from "./h3"; export type ResponseBody = | undefined // middleware pass @@ -29,7 +30,6 @@ type MaybePromise = T | Promise; export type ResolvedEventHandler = { method?: HTTPMethod; route?: string; - prefix?: string; handler?: EventHandler; params?: Record; }; @@ -42,10 +42,7 @@ export type EventHandlerResolver = ( export interface EventHandler< Request extends EventHandlerRequest = EventHandlerRequest, Response extends EventHandlerResponse = EventHandlerResponse, -> { - __is_handler__?: true; - __resolve__?: EventHandlerResolver; - __websocket__?: Partial; +> extends Partial> { (event: H3Event): Response; } diff --git a/src/types/index.ts b/src/types/index.ts index f6fbc42b..c2d1f31e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,16 +1,5 @@ -// App -export type { - App, - AppOptions, - AppUse, - Stack, - InputLayer, - InputStack, - Layer, - WebSocketOptions, - Matcher, - H3Error, -} from "./app"; +// H3 +export type { H3, H3Config, WebSocketOptions, H3Error } from "./h3"; // Event export type { H3Event } from "./event"; @@ -30,21 +19,10 @@ export type { ResponseMiddleware, } from "./handler"; -// Web -export type { - PlainHandler, - PlainRequest, - PlainResponse, - WebHandler, -} from "./web"; - -// Router -export type { Router, RouterOptions, RouterEntry } from "./router"; - // Context export type { H3EventContext } from "./context"; -// SSE +// EventStream export type { EventStreamMessage, EventStreamOptions } from "./utils/sse"; export type { EventStream } from "../utils/internal/event-stream"; diff --git a/src/types/node.ts b/src/types/node.ts index 0ec4836f..6288792d 100644 --- a/src/types/node.ts +++ b/src/types/node.ts @@ -19,4 +19,4 @@ export type NodeMiddleware = ( next: (error?: Error) => void, ) => unknown | Promise; -export type { NodeEvent } from "../adapters/node/event"; +export type { NodeEvent } from "../adapters/node/_event"; diff --git a/src/types/router.ts b/src/types/router.ts deleted file mode 100644 index f61dbad1..00000000 --- a/src/types/router.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { EventHandler } from "./handler"; -import type { HTTPMethod } from "./http"; - -type AddRoute = (path: string, handler: EventHandler) => Router; - -export interface Router { - all: AddRoute; - /** @deprecated please use router.all */ - use: Router["all"]; - get: AddRoute; - post: AddRoute; - put: AddRoute; - delete: AddRoute; - patch: AddRoute; - head: AddRoute; - options: AddRoute; - connect: AddRoute; - trace: AddRoute; - add: ( - method: "" | HTTPMethod | Lowercase, - path: string, - handler: EventHandler, - ) => Router; - - handler: EventHandler; -} - -export interface RouterEntry { - method: HTTPMethod; - route: string; - handler: EventHandler; -} - -export interface RouterOptions { - preemptive?: boolean; -} diff --git a/src/types/web.ts b/src/types/web.ts deleted file mode 100644 index 6b012ea6..00000000 --- a/src/types/web.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { H3EventContext } from "./context"; - -export type WebHandler = ( - request: Request, - context?: H3EventContext, -) => Promise; - -export type PlainHandler = ( - request: PlainRequest, - context?: H3EventContext, -) => Promise; - -export interface PlainRequest { - path: string; - method: string; - headers: HeadersInit; - body?: BodyInit; -} - -export interface PlainResponse { - status: number; - statusText: string | undefined; - headers: Record; - setCookie: string[]; - body?: unknown; -} diff --git a/src/utils/base.ts b/src/utils/base.ts index 8c477a78..c037c7cb 100644 --- a/src/utils/base.ts +++ b/src/utils/base.ts @@ -1,37 +1,38 @@ -import type { EventHandler } from "../types"; import { _kRaw } from "../event"; -import { withoutTrailingSlash, withoutBase } from "./internal/path"; +import { H3, EventHandler } from "../types"; +import { withoutBase, withoutTrailingSlash } from "./internal/path"; /** - * Prefixes and executes a handler with a base path. + * Returns a new event handler that removes the base url of the event before calling the original handler. * * @example + * const api = createApp() + * .get("/", () => "Hello API!"); * const app = createApp(); - * const router = createRouter(); + * .use("/api/**", withBase("/api", api.handler)); * - * const apiRouter = createRouter().get("/hello", () => "Hello API!"); - * - * router.use("/api/**", useBase("/api", apiRouter.handler)); - * - * app.use(router.handler); - * - * @param base The base path to prefix. When set to an empty string, the handler will be run as is. + * @param base The base path to prefix. * @param handler The event handler to use with the adapted path. */ -export function useBase(base: string, handler: EventHandler): EventHandler { +export function withBase(base: string, input: EventHandler | H3): EventHandler { base = withoutTrailingSlash(base); - if (!base || base === "/") { - return handler; - } + const _originalHandler = (input as H3)?.handler || (input as EventHandler); - return async (event) => { + const _handler: EventHandler = async (event) => { const _pathBefore = event[_kRaw].path || "/"; event[_kRaw].path = withoutBase(event.path || "/", base); - try { - return await handler(event); - } finally { + return Promise.resolve(_originalHandler(event)).finally(() => { event[_kRaw].path = _pathBefore; - } + }); }; + + _handler.websocket = _originalHandler.websocket; + _handler.resolve = _originalHandler.resolve + ? (method, path) => { + return _originalHandler.resolve!(method, withoutBase(path, base)); + } + : undefined; + + return _handler; } diff --git a/src/utils/cookie.ts b/src/utils/cookie.ts index 8edc6379..4717daef 100644 --- a/src/utils/cookie.ts +++ b/src/utils/cookie.ts @@ -63,14 +63,6 @@ export function setCookie( parseCookie(cookie), ); if (_key === newCookieKey) { - console.log( - "Overwriting cookie:", - setCookie, - "to", - newCookie, - "key", - _key, - ); continue; } event[_kRaw].appendResponseHeader("set-cookie", cookie); diff --git a/test/_setup.ts b/test/_setup.ts index f549e9f0..6c6d930e 100644 --- a/test/_setup.ts +++ b/test/_setup.ts @@ -1,27 +1,18 @@ import type { Mock } from "vitest"; -import type { App, AppOptions, H3Error, H3Event } from "../src/types"; -import type { PlainHandler, WebHandler } from "../src/types"; +import type { H3, H3Config, H3Error, H3Event } from "../src/types"; import { beforeEach, afterEach, vi } from "vitest"; import supertest from "supertest"; import { Server as NodeServer } from "node:http"; import { Client as UndiciClient } from "undici"; import { getRandomPort } from "get-port-please"; -import { - createApp, - NodeHandler, - toNodeHandler, - toPlainHandler, - toWebHandler, -} from "../src"; +import { createApp, NodeHandler, toNodeHandler } from "../src"; interface TestContext { request: ReturnType; - webHandler: WebHandler; nodeHandler: NodeHandler; - plainHandler: PlainHandler; - app: App; + app: H3; server?: NodeServer; client?: UndiciClient; @@ -29,10 +20,10 @@ interface TestContext { errors: H3Error[]; - onRequest: Mock>; - onError: Mock>; - onBeforeResponse: Mock>; - onAfterResponse: Mock>; + onRequest: Mock>; + onError: Mock>; + onBeforeResponse: Mock>; + onAfterResponse: Mock>; } export function setupTest( @@ -57,9 +48,7 @@ export function setupTest( onAfterResponse: ctx.onAfterResponse, }); - ctx.webHandler = toWebHandler(ctx.app); ctx.nodeHandler = toNodeHandler(ctx.app); - ctx.plainHandler = toPlainHandler(ctx.app); ctx.request = supertest(ctx.nodeHandler); diff --git a/test/app.test.ts b/test/app.test.ts index cc1fa602..93880944 100644 --- a/test/app.test.ts +++ b/test/app.test.ts @@ -11,7 +11,7 @@ describe("app", () => { ctx.app.use("/api", (event) => ({ url: event.path })); const res = await ctx.request.get("/api"); - expect(res.body).toEqual({ url: "/" }); + expect(res.body).toEqual({ url: "/api" }); }); it("can return bigint directly", async () => { @@ -56,11 +56,11 @@ describe("app", () => { expect(res.text).toBe("Hello World!"); }); - it("can return a 204 response", async () => { + it("can return a null response", async () => { ctx.app.use("/api", () => null); const res = await ctx.request.get("/api"); - expect(res.statusCode).toBe(204); + expect(res.statusCode).toBe(200); expect(res.text).toEqual(""); expect(res.ok).toBeTruthy(); }); @@ -222,7 +222,7 @@ describe("app", () => { }); it("can take an object", async () => { - ctx.app.use({ prefix: "/", handler: () => "valid" }); + ctx.app.use({ route: "/", handler: () => "valid" }); const response = await ctx.request.get("/"); expect(response.text).toEqual("valid"); @@ -236,18 +236,6 @@ describe("app", () => { expect(response.text).toEqual("done"); }); - it("can use a custom matcher", async () => { - ctx.app.use("/odd", () => "Is odd!", { - match: (url) => Boolean(Number(url.slice(1)) % 2), - }); - - const res = await ctx.request.get("/odd/41"); - expect(res.text).toBe("Is odd!"); - - const notFound = await ctx.request.get("/odd/2"); - expect(notFound.status).toBe(404); - }); - it("can normalise route definitions", async () => { ctx.app.use("/test/", () => "valid"); diff --git a/test/bench/spec.ts b/test/bench/spec.ts index 0ac2d05b..51939a44 100644 --- a/test/bench/spec.ts +++ b/test/bench/spec.ts @@ -1,11 +1,4 @@ -import { - createRouter, - createApp, - readJSONBody, - toWebHandler, - getQuery, - setResponseHeader, -} from "../../src"; +import { createH3, readJSONBody, getQuery, setResponseHeader } from "../../src"; // https://github.com/pi0/web-framework-benchmarks // https://github.com/SaltyAom/bun-http-framework-benchmark @@ -46,23 +39,22 @@ export function createBenchApps() { } export function createH3App() { - const router = createRouter(); - const app = createApp().use(router); + const app = createH3(); // [GET] / - router.get("/", () => "Hi"); + app.get("/", () => "Hi"); // [GET] /id/:id - router.get("/id/:id", (event) => { + app.get("/id/:id", (event) => { const query = getQuery(event); setResponseHeader(event, "x-powered-by", "benchmark"); return `${event.context.params!.id} ${query.name}`; }); // [POST] /json - router.post("/json", (event) => readJSONBody(event)); + app.post("/json", (event) => readJSONBody(event)); - return toWebHandler(app); + return app.fetch; } export function createBaselineApp() { diff --git a/test/body.test.ts b/test/body.test.ts index 67b34ee5..cd24982d 100644 --- a/test/body.test.ts +++ b/test/body.test.ts @@ -8,7 +8,7 @@ describe("body", () => { const ctx = setupTest({ startServer: true }); it("can read simple string", async () => { - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { const body = await readTextBody(request); expect(body).toEqual('{"bool":true,"name":"string","number":1}'); return "200"; @@ -28,7 +28,7 @@ describe("body", () => { it("can read chunked string", async () => { const requestJsonUrl = new URL("assets/sample.json", import.meta.url); - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { const body = await readTextBody(request); const json = (await readFile(requestJsonUrl)).toString("utf8"); @@ -46,7 +46,7 @@ describe("body", () => { it("returns undefined if body is not present", async () => { let _body: string | undefined = "initial"; - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { _body = await readTextBody(request); return "200"; }); @@ -61,7 +61,7 @@ describe("body", () => { it("returns an empty string if body is string", async () => { let _body: string | undefined = "initial"; - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { _body = await readJSONBody(request); return "200"; }); @@ -77,7 +77,7 @@ describe("body", () => { it("returns an empty object string if body is empty object", async () => { let _body: string | undefined = "initial"; - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { _body = await readTextBody(request); return "200"; }); @@ -92,7 +92,7 @@ describe("body", () => { }); it("can parse json payload", async () => { - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { const body = await readJSONBody(request); expect(body).toMatchObject({ bool: true, @@ -116,7 +116,7 @@ describe("body", () => { it("handles non-present body", async () => { let _body: string | undefined; - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { _body = await readJSONBody(request); return "200"; }); @@ -130,7 +130,7 @@ describe("body", () => { it("handles empty body", async () => { let _body: string | undefined = "initial"; - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { _body = await readTextBody(request); return "200"; }); @@ -148,7 +148,7 @@ describe("body", () => { it("handles empty object as body", async () => { let _body: string | undefined = "initial"; - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { _body = await readJSONBody(request); return "200"; }); @@ -162,7 +162,7 @@ describe("body", () => { }); it("parse the form encoded into an object", async () => { - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { const body = await readJSONBody(request); expect(body).toMatchObject({ field: "value", @@ -184,7 +184,7 @@ describe("body", () => { }); it("parses multipart form data", async () => { - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { const formData = await readFormDataBody(request); return [...formData!.entries()].map(([name, value]) => ({ name, @@ -222,7 +222,7 @@ describe("body", () => { it("returns undefined if body is not present with text/plain", async () => { let _body: string | undefined; - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { _body = await readTextBody(request); return "200"; }); @@ -240,7 +240,7 @@ describe("body", () => { it("returns undefined if body is not present with json", async () => { let _body: string | undefined; - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { _body = await readTextBody(request); return "200"; }); @@ -258,7 +258,7 @@ describe("body", () => { it("returns the string if content type is text/*", async () => { let _body: string | undefined; - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { _body = await readTextBody(request); return "200"; }); @@ -276,7 +276,7 @@ describe("body", () => { }); it("returns string as is if cannot parse with unknown content type", async () => { - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { const _body = await readTextBody(request); return _body; }); @@ -294,7 +294,7 @@ describe("body", () => { }); it("fails if json is invalid", async () => { - ctx.app.use("/", async (request) => { + ctx.app.use("/api/test", async (request) => { const _body = await readJSONBody(request); return _body; }); diff --git a/test/cors.test.ts b/test/cors.test.ts index d5a4edab..6e0c8cf4 100644 --- a/test/cors.test.ts +++ b/test/cors.test.ts @@ -1,7 +1,6 @@ import type { H3CorsOptions } from "../src/types"; import { expect, it, describe } from "vitest"; -import { fromPlainRequest } from "../src/adapters/web"; -import { isPreflightRequest, isCorsOriginAllowed } from "../src"; +import { mockEvent, isPreflightRequest, isCorsOriginAllowed } from "../src"; import { resolveCorsOptions, createOriginHeaders, @@ -53,8 +52,7 @@ describe("resolveCorsOptions", () => { describe("isPreflightRequest", () => { it("can detect preflight request", () => { - const eventMock = fromPlainRequest({ - path: "/", + const eventMock = mockEvent("/", { method: "OPTIONS", headers: { origin: "https://example.com", @@ -66,8 +64,7 @@ describe("isPreflightRequest", () => { }); it("can detect request of non-OPTIONS method)", () => { - const eventMock = fromPlainRequest({ - path: "/", + const eventMock = mockEvent("/", { method: "GET", headers: { origin: "https://example.com", @@ -79,8 +76,7 @@ describe("isPreflightRequest", () => { }); it("can detect request without origin header", () => { - const eventMock = fromPlainRequest({ - path: "/", + const eventMock = mockEvent("/", { method: "OPTIONS", headers: { "access-control-request-method": "GET", @@ -91,8 +87,7 @@ describe("isPreflightRequest", () => { }); it("can detect request without AccessControlRequestMethod header", () => { - const eventMock = fromPlainRequest({ - path: "/", + const eventMock = mockEvent("/", { method: "OPTIONS", headers: { origin: "https://example.com", @@ -178,8 +173,7 @@ describe("isCorsOriginAllowed", () => { describe("createOriginHeaders", () => { it('returns an object whose `access-control-allow-origin` is `"*"` if `origin` option is not defined, or `"*"`', () => { - const eventMock = fromPlainRequest({ - path: "/", + const eventMock = mockEvent("/", { method: "OPTIONS", headers: { origin: "https://example.com", @@ -199,8 +193,7 @@ describe("createOriginHeaders", () => { }); it('returns an object whose `access-control-allow-origin` is `"*"` if `origin` header is not defined', () => { - const eventMock = fromPlainRequest({ - path: "/", + const eventMock = mockEvent("/", { method: "OPTIONS", headers: {}, }); @@ -212,8 +205,7 @@ describe("createOriginHeaders", () => { }); it('returns an object with `access-control-allow-origin` and `vary` keys if `origin` option is `"null"`', () => { - const eventMock = fromPlainRequest({ - path: "/", + const eventMock = mockEvent("/", { method: "OPTIONS", headers: { origin: "https://example.com", @@ -230,8 +222,7 @@ describe("createOriginHeaders", () => { }); it("returns an object with `access-control-allow-origin` and `vary` keys if `origin` option and `origin` header matches", () => { - const eventMock = fromPlainRequest({ - path: "/", + const eventMock = mockEvent("/", { method: "OPTIONS", headers: { origin: "http://example.com", @@ -255,8 +246,7 @@ describe("createOriginHeaders", () => { }); it("returns an empty object if `origin` option is one that is not allowed", () => { - const eventMock = fromPlainRequest({ - path: "/", + const eventMock = mockEvent("/", { method: "OPTIONS", headers: { origin: "https://example.com", @@ -326,8 +316,7 @@ describe("createCredentialsHeaders", () => { describe("createAllowHeaderHeaders", () => { it('returns an object with `access-control-allow-headers` and `vary` keys according to `access-control-request-headers` header if `allowHeaders` option is not defined, `"*"`, or an empty array', () => { - const eventMock = fromPlainRequest({ - path: "/", + const eventMock = mockEvent("/", { method: "OPTIONS", headers: { "access-control-request-headers": "CUSTOM-HEADER", @@ -356,8 +345,7 @@ describe("createAllowHeaderHeaders", () => { }); it("returns an object with `access-control-allow-headers` and `vary` keys according to `allowHeaders` option if `access-control-request-headers` header is not defined", () => { - const eventMock = fromPlainRequest({ - path: "/", + const eventMock = mockEvent("/", { method: "OPTIONS", headers: {}, }); @@ -372,8 +360,7 @@ describe("createAllowHeaderHeaders", () => { }); it('returns an empty object if `allowHeaders` option is not defined, `"*"`, or an empty array, and `access-control-request-headers` is not defined', () => { - const eventMock = fromPlainRequest({ - path: "/", + const eventMock = mockEvent("/", { method: "OPTIONS", headers: {}, }); diff --git a/test/error.test.ts b/test/error.test.ts index 8b1cfaa5..4fc5e8c6 100644 --- a/test/error.test.ts +++ b/test/error.test.ts @@ -26,7 +26,7 @@ describe("error", () => { }); it("can send internal error", async () => { - ctx.app.use("/", () => { + ctx.app.use("/api/test", () => { throw new Error("Booo"); }); const result = await ctx.request.get("/api/test"); @@ -40,7 +40,7 @@ describe("error", () => { it("can send runtime error", async () => { consoleMock.mockReset(); - ctx.app.use("/", () => { + ctx.app.use("/api/test", () => { throw createError({ statusCode: 400, statusMessage: "Bad Request", diff --git a/test/event.test.ts b/test/event.test.ts index 7bcb515d..d92a61e5 100644 --- a/test/event.test.ts +++ b/test/event.test.ts @@ -6,7 +6,7 @@ describe("Event", () => { const ctx = setupTest(); it("can read the method", async () => { - ctx.app.use("/", (event) => { + ctx.app.use("/*", (event) => { expect(event.method).toBe(event.method); expect(event.method).toBe("POST"); return "200"; @@ -16,7 +16,7 @@ describe("Event", () => { }); it("can read the headers", async () => { - ctx.app.use("/", (event) => { + ctx.app.use("/*", (event) => { return { headers: [...event.headers.entries()], }; @@ -33,7 +33,7 @@ describe("Event", () => { }); it("can get request url", async () => { - ctx.app.use("/", (event) => { + ctx.app.use("/*", (event) => { return getRequestURL(event); }); const result = await ctx.request.get("/hello"); @@ -41,7 +41,7 @@ describe("Event", () => { }); it("can read request body", async () => { - ctx.app.use("/", async (event) => { + ctx.app.use("/*", async (event) => { const bodyStream = getBodyStream(event); let bytes = 0; // @ts-expect-error iterator @@ -64,10 +64,7 @@ describe("Event", () => { ctx.app.use("/", async (event) => { expect(event.method).toBe("POST"); expect(event.headers.get("x-test")).toBe("123"); - // TODO: Find a workaround for Node.js 16 - if (!process.versions.node.startsWith("16")) { - expect(await readJSONBody(event)).toMatchObject({ hello: "world" }); - } + expect(await readJSONBody(event)).toMatchObject({ hello: "world" }); return "200"; }); const result = await ctx.request diff --git a/test/fixture/plain.ts b/test/fixture/plain.ts deleted file mode 100644 index 8f640bc5..00000000 --- a/test/fixture/plain.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { toPlainHandler } from "../../src"; -import { app } from "./app"; - -const plainHandler = toPlainHandler(app); - -const res = await plainHandler({ - path: "/", - method: "GET", - headers: { - foo: "bar", - }, - body: undefined, -}); - -console.log(res); diff --git a/test/integrations.test.ts b/test/integrations.test.ts index ed0fb324..8ab33c95 100644 --- a/test/integrations.test.ts +++ b/test/integrations.test.ts @@ -45,7 +45,7 @@ describe("integration with express", () => { it("can be used as express middleware", async () => { const expressApp = express(); ctx.app.use( - "/api/hello", + "/api/*", fromNodeHandler((_req, res, next) => { (res as any).prop = "42"; next(); @@ -64,7 +64,7 @@ describe("integration with express", () => { const res = await ctx.request.get("/api/hello"); - expect(res.body).toEqual({ url: "/", prop: "42" }); + expect(res.body).toEqual({ url: "/api/hello", prop: "42" }); }); it("can wrap a connect instance", async () => { @@ -73,7 +73,7 @@ describe("integration with express", () => { res.setHeader("content-type", "application/json"); res.end(JSON.stringify({ connect: "works" })); }); - ctx.app.use("/", fromNodeHandler(connectApp)); + ctx.app.use("/**", fromNodeHandler(connectApp)); const res = await ctx.request.get("/api/connect"); expect(res.body).toEqual({ connect: "works" }); @@ -99,6 +99,6 @@ describe("integration with express", () => { const res = await ctx.request.get("/api/hello"); - expect(res.body).toEqual({ url: "/", prop: "42" }); + expect(res.body).toEqual({ url: "/api/hello", prop: "42" }); }); }); diff --git a/test/package.test.ts b/test/package.test.ts index 46cb0a19..7636fe04 100644 --- a/test/package.test.ts +++ b/test/package.test.ts @@ -12,13 +12,12 @@ describe("h3 package", () => { "appendResponseHeader", "appendResponseHeaders", "assertMethod", - "callWithPlainRequest", "clearResponseHeaders", "clearSession", "createApp", "createError", - "createEvent", "createEventStream", + "createH3", "createRouter", "defaultContentType", "defineEventHandler", @@ -36,11 +35,7 @@ describe("h3 package", () => { "fetchWithEvent", "fromNodeHandler", "fromNodeMiddleware", - "fromNodeRequest", - "fromPlainHandler", - "fromPlainRequest", "fromWebHandler", - "fromWebRequest", "getBodyStream", "getCookie", "getHeader", @@ -73,11 +68,12 @@ describe("h3 package", () => { "isCorsOriginAllowed", "isError", "isEvent", - "isEventHandler", "isMethod", "isPreflightRequest", "iterable", "lazyEventHandler", + "mockEvent", + "noContent", "parseCookies", "proxy", "proxyRequest", @@ -111,13 +107,13 @@ describe("h3 package", () => { "toEventHandler", "toNodeHandler", "toNodeListener", - "toPlainHandler", "toWebHandler", "toWebRequest", "unsealSession", "updateSession", "useBase", "useSession", + "withBase", "writeEarlyHints", ] `); diff --git a/test/plain.test.ts b/test/plain.test.ts deleted file mode 100644 index 7600eb68..00000000 --- a/test/plain.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { - readTextBody, - appendResponseHeader, - setResponseStatus, - getRequestHeaders, - getQuery, -} from "../src"; -import { setupTest } from "./_setup"; - -describe("Plain handler", () => { - const ctx = setupTest(); - - it("works", async () => { - ctx.app.use("/test", async (event) => { - const body = await readTextBody(event); - setResponseStatus(event, 201, "Created"); - appendResponseHeader( - event, - "content-type", - "application/json;charset=UTF-8", - ); - appendResponseHeader(event, "set-cookie", "a=123"); - appendResponseHeader(event, "set-cookie", "b=123"); - appendResponseHeader(event, "set-cookie", "c=123"); - appendResponseHeader(event, "set-cookie", "d=123"); - return { - method: event.method, - path: event.path, - headers: getRequestHeaders(event), - body, - contextKeys: Object.keys(event.context), - query: getQuery(event), - }; - }); - - const res = await ctx.plainHandler( - { - method: "POST", - path: "/test/foo/bar?test=123", - headers: [["x-test", "true"]], - body: "request body", - }, - { - test: true, - }, - ); - - expect(res).toMatchObject({ - status: 201, - statusText: "Created", - headers: { - "content-type": "application/json;charset=UTF-8", - "set-cookie": "a=123, b=123, c=123, d=123", - }, - setCookie: ["a=123", "b=123", "c=123", "d=123"], - }); - - expect(typeof res.body).toBe("string"); - expect(JSON.parse(res.body as string)).toMatchObject({ - method: "POST", - path: "/foo/bar?test=123", - body: "request body", - headers: { - "content-type": "text/plain;charset=UTF-8", - "x-test": "true", - }, - contextKeys: ["test"], - query: { test: "123" }, - }); - }); -}); diff --git a/test/resolve.test.ts b/test/resolve.test.ts index b1db2423..9999faa7 100644 --- a/test/resolve.test.ts +++ b/test/resolve.test.ts @@ -1,79 +1,98 @@ import { describe, it, expect } from "vitest"; -import { createApp, createRouter, defineLazyEventHandler } from "../src"; +import { createApp, defineLazyEventHandler } from "../src"; +import { withBase } from "../src/utils/base"; -describe("Event handler resolver", () => { - const testHandlers = Array.from({ length: 10 }).map((_, i) => () => i); +describe("Event handler resolver", async () => { + const _handlers = Object.create(null); + const _h = (name: string) => { + if (!_handlers[name]) { + _handlers[name] = { [name]: () => name }[name]; + } + return _handlers[name]; + }; const app = createApp(); // Middleware - app.use(testHandlers[0]); - app.use("/", testHandlers[1]); + app.use("/", _h("root middleware")); + app.use("/**", _h("/**")); // Path prefix - app.use("/test", testHandlers[2]); + app.use("/test/**", _h("/test/**")); app.use( "/lazy", - defineLazyEventHandler(() => testHandlers[3]), + defineLazyEventHandler(() => _h("lazy")), ); // Sub app const nestedApp = createApp(); - app.use("/nested", nestedApp as any); - nestedApp.use("/path", testHandlers[4]); + nestedApp.use("/path/**", _h("/nested/path/**")); nestedApp.use( "/lazy", - defineLazyEventHandler(() => Promise.resolve(testHandlers[5])), + defineLazyEventHandler(() => Promise.resolve(_h("/nested/lazy"))), ); + app.use("/nested/**", withBase("/nested", nestedApp.handler)); // Router - const router = createRouter(); - app.use("/router", router.handler); - router.get("/", testHandlers[6]); - router.get("/:id", testHandlers[7]); + const router = createApp(); + router.get("/", _h("/router")); + router.get("/:id", _h("/router/:id")); router.get( "/lazy", - defineLazyEventHandler(() => testHandlers[8]), + defineLazyEventHandler(() => Promise.resolve(_h("/router/lazy"))), ); + app.use("/router/**", withBase("/router", router)); describe("middleware", () => { - it("does not resolves /", async () => { - expect(await app.resolve("GET", "/")).toBeUndefined(); + it("resolves /", async () => { + expect(await app.resolve("GET", "/")).toMatchObject({ + handler: _h("root middleware"), + }); + }); + + it("resolves /foo/bar", async () => { + expect(await app.resolve("GET", "/foo/bar")).toMatchObject({ + route: "/**", + handler: _h("/**"), + }); }); }); describe("path prefix", () => { it("resolves /test", async () => { expect(await app.resolve("GET", "/test")).toMatchObject({ - prefix: "/test", - handler: testHandlers[2], + route: "/test/**", + handler: _h("/test/**"), }); }); it("resolves /test/foo", async () => { - expect((await app.resolve("GET", "/test/foo"))?.prefix).toEqual("/test"); + expect(await app.resolve("GET", "/test/foo")).toMatchObject({ + route: "/test/**", + handler: _h("/test/**"), + }); }); }); it("resolves /lazy", async () => { expect(await app.resolve("GET", "/lazy")).toMatchObject({ - prefix: "/lazy", - handler: testHandlers[3], + route: "/lazy", + handler: _h("lazy"), }); }); describe("nested app", () => { it("resolves /nested/path/foo", async () => { expect(await app.resolve("GET", "/nested/path/foo")).toMatchObject({ - prefix: "/nested/path", - handler: testHandlers[4], + route: "/nested/path/**", + handler: _h("/nested/path/**"), }); }); it("resolves /nested/lazy", async () => { expect(await app.resolve("GET", "/nested/lazy")).toMatchObject({ - prefix: "/nested/lazy", - handler: testHandlers[5], + route: "/nested/lazy", + handler: _h("/nested/lazy"), }); }); }); @@ -81,9 +100,8 @@ describe("Event handler resolver", () => { describe("router", () => { it("resolves /router", async () => { expect(await app.resolve("GET", "/router")).toMatchObject({ - prefix: "/router", - route: "/", - handler: testHandlers[6], + route: "/router", + handler: _h("/router"), }); expect(await app.resolve("GET", "/router/")).toMatchObject( (await app.resolve("GET", "/router")) as any, @@ -92,17 +110,15 @@ describe("Event handler resolver", () => { it("resolves /router/:id", async () => { expect(await app.resolve("GET", "/router/foo")).toMatchObject({ - prefix: "/router", - route: "/:id", - handler: testHandlers[7], + route: "/router/:id", + handler: _h("/router/:id"), }); }); it("resolves /router/lazy", async () => { expect(await app.resolve("GET", "/router/lazy")).toMatchObject({ - prefix: "/router", - route: "/lazy", - handler: testHandlers[8], + route: "/router/lazy", + handler: _h("/router/lazy"), }); }); }); diff --git a/test/router.test.ts b/test/router.test.ts index e32925e0..81343faf 100644 --- a/test/router.test.ts +++ b/test/router.test.ts @@ -1,15 +1,15 @@ -import type { Router } from "../src/types"; +import type { H3 } from "../src/types"; import { describe, it, expect, beforeEach } from "vitest"; -import { createRouter, getRouterParams, getRouterParam } from "../src"; +import { getRouterParams, getRouterParam, createApp } from "../src"; import { setupTest } from "./_setup"; describe("router", () => { const ctx = setupTest(); - let router: Router; + let router: H3; beforeEach(() => { - router = createRouter() + router = createApp() .get("/", () => "Hello") .get("/test/?/a", () => "/test/?/a") .get("/many/routes", () => "many routes") @@ -26,7 +26,7 @@ describe("router", () => { }); it("Multiple Routers", async () => { - const secondRouter = createRouter().get("/router2", () => "router2"); + const secondRouter = createApp().get("/router2", () => "router2"); ctx.app.use(secondRouter); @@ -91,13 +91,13 @@ describe("router", () => { describe("router (preemptive)", () => { const ctx = setupTest(); - let router: Router; + let router: H3; beforeEach(() => { - router = createRouter({ preemptive: true }) + router = createApp() .get("/test", () => "Test") .get("/undefined", () => undefined); - ctx.app.use(router); + ctx.app.all("/**", router); }); it("Handle /test", async () => { @@ -114,7 +114,7 @@ describe("router (preemptive)", () => { }); it("Not matching route method", async () => { - const res = await ctx.request.head("/test"); + const res = await ctx.request.head("/404"); expect(res.status).toEqual(404); }); @@ -129,7 +129,7 @@ describe("getRouterParams", () => { describe("with router", () => { it("can return router params", async () => { - const router = createRouter().get("/test/params/:name", (event) => { + const router = createApp().get("/test/params/:name", (event) => { expect(getRouterParams(event)).toMatchObject({ name: "string" }); return "200"; }); @@ -140,7 +140,7 @@ describe("getRouterParams", () => { }); it("can decode router params", async () => { - const router = createRouter().get("/test/params/:name", (event) => { + const router = createApp().get("/test/params/:name", (event) => { expect(getRouterParams(event, { decode: true })).toMatchObject({ name: "string with space", }); @@ -155,7 +155,7 @@ describe("getRouterParams", () => { describe("without router", () => { it("can return an empty object if router is not used", async () => { - ctx.app.use("/", (event) => { + ctx.app.use("/**", (event) => { expect(getRouterParams(event)).toMatchObject({}); return "200"; }); @@ -171,7 +171,7 @@ describe("getRouterParam", () => { describe("with router", () => { it("can return a value of router params corresponding to the given name", async () => { - const router = createRouter().get("/test/params/:name", (event) => { + const router = createApp().get("/test/params/:name", (event) => { expect(getRouterParam(event, "name")).toEqual("string"); return "200"; }); @@ -182,7 +182,7 @@ describe("getRouterParam", () => { }); it("can decode a value of router params corresponding to the given name", async () => { - const router = createRouter().get("/test/params/:name", (event) => { + const router = createApp().get("/test/params/:name", (event) => { expect(getRouterParam(event, "name", { decode: true })).toEqual( "string with space", ); @@ -197,7 +197,7 @@ describe("getRouterParam", () => { describe("without router", () => { it("can return `undefined` for any keys", async () => { - ctx.app.use("/", (request) => { + ctx.app.use("/**", (request) => { expect(getRouterParam(request, "name")).toEqual(undefined); return "200"; }); @@ -213,7 +213,7 @@ describe("event.context.matchedRoute", () => { describe("with router", () => { it("can return the matched path", async () => { - const router = createRouter().get("/test/:template", (event) => { + const router = createApp().get("/test/:template", (event) => { expect(event.context.matchedRoute).toMatchObject({ method: "GET", route: "/test/:template", @@ -230,7 +230,7 @@ describe("event.context.matchedRoute", () => { describe("without router", () => { it("can return `undefined` for matched path", async () => { - ctx.app.use("/", (event) => { + ctx.app.use("/**", (event) => { expect(event.context.matchedRoute).toEqual(undefined); return "200"; }); diff --git a/test/session.test.ts b/test/session.test.ts index f12e9b1c..58868efe 100644 --- a/test/session.test.ts +++ b/test/session.test.ts @@ -1,12 +1,12 @@ import type { SessionConfig } from "../src/types"; import { describe, it, expect, beforeEach } from "vitest"; -import { createRouter, useSession, readJSONBody } from "../src"; +import { useSession, readJSONBody, createApp } from "../src"; import { setupTest } from "./_setup"; describe("session", () => { const ctx = setupTest(); - let router: ReturnType; + let router: ReturnType; let cookie = ""; @@ -18,7 +18,7 @@ describe("session", () => { }; beforeEach(() => { - router = createRouter({ preemptive: true }); + router = createApp({}); router.use("/", async (event) => { const session = await useSession(event, sessionConfig); if (event.method === "POST") { diff --git a/test/static.test.ts b/test/static.test.ts index fd3f01c3..67872c5d 100644 --- a/test/static.test.ts +++ b/test/static.test.ts @@ -26,7 +26,7 @@ describe("Serve Static", () => { encodings: { gzip: ".gz", br: ".br" }, }; - ctx.app.use("/", (event) => { + ctx.app.use("/**", (event) => { return serveStatic(event, serveStaticOptions); }); }); diff --git a/test/status.test.ts b/test/status.test.ts index 26d31b4c..14a4748c 100644 --- a/test/status.test.ts +++ b/test/status.test.ts @@ -1,27 +1,28 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { toPlainHandler } from "../src/adapters/web"; -import { setResponseStatus } from "../src"; +import { describe, it, expect } from "vitest"; +import { setResponseStatus, noContent } from "../src"; import { setupTest } from "./_setup"; +async function webResponseToPlain(res: Response) { + return { + status: res.status, + statusText: res.statusText, + body: await res.text(), + headers: Object.fromEntries(res.headers), + }; +} + describe("setResponseStatus", () => { const ctx = setupTest(); - let handler: ReturnType; - - beforeEach(() => { - handler = toPlainHandler(ctx.app); - }); describe("content response", () => { it("sets status 200 as default", async () => { ctx.app.use("/test", () => "text"); - const res = await handler({ + const res = await ctx.app.fetch("/test", { method: "POST", - path: "/test", - headers: [], }); - expect(res).toMatchObject({ + expect(await webResponseToPlain(res)).toMatchObject({ status: 200, statusText: "", body: "text", @@ -36,14 +37,12 @@ describe("setResponseStatus", () => { return "text"; }); - const res = await handler({ + const res = await ctx.app.fetch("/test", { method: "POST", - path: "/test", - headers: [], body: "", }); - expect(res).toMatchObject({ + expect(await webResponseToPlain(res)).toMatchObject({ status: 418, statusText: "status-text", body: "text", @@ -56,20 +55,18 @@ describe("setResponseStatus", () => { describe("no content response", () => { it("sets status 204 as default", async () => { - ctx.app.use("/test", () => { - return null; + ctx.app.use("/test", (event) => { + return noContent(event); }); - const res = await handler({ + const res = await ctx.app.fetch("/test", { method: "POST", - path: "/test", - headers: [], }); - expect(res).toMatchObject({ + expect(await webResponseToPlain(res)).toMatchObject({ status: 204, statusText: "", - body: null, + body: "", headers: {}, }); }); @@ -79,14 +76,12 @@ describe("setResponseStatus", () => { return ""; }); - const res = await handler({ + const res = await ctx.app.fetch("/test", { method: "POST", - path: "/test", - headers: [], body: "", }); - expect(res).toMatchObject({ + expect(await webResponseToPlain(res)).toMatchObject({ status: 418, statusText: "status-text", body: "", @@ -100,18 +95,12 @@ describe("setResponseStatus", () => { return ""; }); - const res = await handler({ - method: "GET", - path: "/test", - headers: [], - }); - - // console.log(res.headers); + const res = await ctx.app.fetch("/test"); - expect(res).toMatchObject({ + expect(await webResponseToPlain(res)).toMatchObject({ status: 304, statusText: "Not Modified", - body: null, + body: "", headers: {}, }); }); diff --git a/test/utils.test.ts b/test/utils.test.ts index 8d6c4556..61d108b9 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -160,7 +160,7 @@ describe("", () => { describe("useBase", () => { it("can prefix routes", async () => { ctx.app.use( - "/", + "/**", useBase("/api", (event) => Promise.resolve(event.path)), ); const result = await ctx.request.get("/api/test"); @@ -169,7 +169,7 @@ describe("", () => { }); it("does nothing when not provided a base", async () => { ctx.app.use( - "/", + "/**", useBase("", (event) => Promise.resolve(event.path)), ); const result = await ctx.request.get("/api/test"); @@ -180,7 +180,7 @@ describe("", () => { describe("getQuery", () => { it("can parse query params", async () => { - ctx.app.use("/", (event) => { + ctx.app.use("/**", (event) => { const query = getQuery(event); expect(query).toMatchObject({ bool: "true", @@ -199,7 +199,7 @@ describe("", () => { describe("getMethod", () => { it("can get method", async () => { - ctx.app.use("/", (event) => event.method); + ctx.app.use("/*", (event) => event.method); expect((await ctx.request.get("/api")).text).toBe("GET"); expect((await ctx.request.post("/api")).text).toBe("POST"); }); @@ -229,7 +229,7 @@ describe("", () => { ]; for (const test of tests) { it("getRequestURL: " + JSON.stringify(test), async () => { - ctx.app.use("/", (event) => { + ctx.app.use("/**", (event) => { const url = getRequestURL(event, { xForwardedProto: true, xForwardedHost: true, @@ -366,7 +366,7 @@ describe("", () => { it("uses the request ip when no x-forwarded-for header set", async () => { ctx.app.use((event) => getRequestFingerprint(event, { hash: false })); - ctx.app.options.onRequest = (event) => { + ctx.app.config.onRequest = (event) => { const { socket } = getNodeContext(event)?.req || {}; Object.defineProperty(socket, "remoteAddress", { get(): any { @@ -395,7 +395,7 @@ describe("", () => { describe("readFormDataBody", () => { it("can handle form as FormData in event handler", async () => { - ctx.app.use("/", async (event) => { + ctx.app.use("/api/*", async (event) => { const formData = await readFormDataBody(event); const user = formData!.get("user"); expect(formData instanceof FormData).toBe(true); diff --git a/test/web.test.ts b/test/web.test.ts index e02de4c8..ce5b20f1 100644 --- a/test/web.test.ts +++ b/test/web.test.ts @@ -11,7 +11,7 @@ describe("Web handler", () => { const ctx = setupTest(); it("works", async () => { - ctx.app.use("/test", async (event) => { + ctx.app.use("/test/**", async (event) => { const body = await readTextBody(event); setResponseStatus(event, 201, "Created"); return { @@ -24,18 +24,16 @@ describe("Web handler", () => { }; }); - const res = await ctx.webHandler( - new Request(new URL("/test/foo/bar?test=123", "http://localhost"), { - method: "POST", - headers: { - "X-Test": "true", - }, - body: "request body", - }), - { + const res = await ctx.app.fetch("/test/foo/bar?test=123", { + method: "POST", + headers: { + "X-Test": "true", + }, + body: "request body", + h3: { test: true, }, - ); + }); expect(res.status).toBe(201); expect(res.statusText).toBe("Created"); @@ -45,7 +43,7 @@ describe("Web handler", () => { expect(await res.json()).toMatchObject({ method: "POST", - path: "/foo/bar?test=123", + path: "/test/foo/bar?test=123", body: "request body", headers: { "content-type": "text/plain;charset=UTF-8",