Skip to content

Commit

Permalink
feat!: integrate router with app and app.fetch (#822)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Jul 11, 2024
1 parent b6218cb commit da7d480
Show file tree
Hide file tree
Showing 47 changed files with 927 additions and 1,331 deletions.
156 changes: 95 additions & 61 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <value>`
- `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 <value>`.
- `sendError(event, <error>)`: Migrate to `throw createError(<error>)`.
- `sendStream(event, <stream>)`: Migrate to `return <stream>`.
- `sendWebResponse(event, <response>)`: Migrate to `return <response>`.

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, <value>)`: Migrate to `return iterable(<value>)`.
- `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

Expand All @@ -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.

Expand All @@ -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`
11 changes: 5 additions & 6 deletions docs/2.utils/98.advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,17 @@ Allowed characters: horizontal tabs, spaces or visible ascii characters: https:/

<!-- automd:jsdocs src="../../src/utils/base.ts" -->

### `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));
```

<!-- /automd -->
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
},
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions src/deprecated.ts → src/_deprecated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
import {
defineNodeHandler,
fromNodeHandler,
fromNodeRequest,
toNodeHandler,
} from "./adapters/node";
import {
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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` */
Expand Down
11 changes: 3 additions & 8 deletions src/adapters/node/event.ts → src/adapters/node/_event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
Loading

0 comments on commit da7d480

Please sign in to comment.