Skip to content

Commit

Permalink
refactor!: reduce node.js dependency (#178)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Oct 14, 2022
1 parent e542b07 commit ba2fe08
Show file tree
Hide file tree
Showing 31 changed files with 430 additions and 510 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"@nuxtjs/eslint-config-typescript"
],
"rules": {
"no-use-before-define": "off"
"no-use-before-define": "off",
"vue/one-component-per-file": "off"
}
}
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,15 +279,15 @@ All notable changes to this project will be documented in this file. See [standa

### Features

* expose nodeHandler and add backward compat with layer as `handle` ([54a944c](https://github.com/unjs/h3/commit/54a944c6dff731c104c0a42964d57ccfd342dec3))
* expose toNodeHandle and add backward compat with layer as `handle` ([54a944c](https://github.com/unjs/h3/commit/54a944c6dff731c104c0a42964d57ccfd342dec3))
* support lazy event handlers ([333a4ca](https://github.com/unjs/h3/commit/333a4cab3c278d3749c1e3bdfd78b9fc6c4cefe9))
* typecheck handler to be a function ([38493eb](https://github.com/unjs/h3/commit/38493eb9f65ba2a2811ba36379ad0b897a6f6e5a))


### Bug Fixes

* add missing types export ([53f0b58](https://github.com/unjs/h3/commit/53f0b58b66c9d181b2bca40dcfd27305014ff758))
* refine nodeHandler type as we always return promise ([1ba6019](https://github.com/unjs/h3/commit/1ba6019c35c8a76e368859e83790369233a7c301))
* refine toNodeHandle type as we always return promise ([1ba6019](https://github.com/unjs/h3/commit/1ba6019c35c8a76e368859e83790369233a7c301))

## [0.5.0](https://github.com/unjs/h3/compare/v0.4.2...v0.5.0) (2022-03-29)

Expand Down
86 changes: 34 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@

✔️  **Portable:** Works perfectly in Serverless, Workers, and Node.js

✔️  **Compatible:** Support connect/express middleware

✔️  **Minimal:** Small and tree-shakable

✔️  **Modern:** Native promise support
Expand All @@ -23,6 +21,8 @@

✔️  **Router:** Super fast route matching using [unjs/radix3](https://github.com/unjs/radix3)

✔️  **Compatible:** Compatibility layer with node/connect/express middleware

## Install

```bash
Expand All @@ -40,25 +40,25 @@ pnpm add h3

```ts
import { createServer } from 'http'
import { createApp } from 'h3'
import { createApp, eventHandler } from 'h3'

const app = createApp()
app.use('/', () => 'Hello world!')
app.use('/', eventHandler(() => 'Hello world!'))

createServer(app).listen(process.env.PORT || 3000)
createServer(toNodeListener(app)).listen(process.env.PORT || 3000)
```

<details>
<summary>Example using <a href="https://github.com/unjs/listhen">listhen</a> for an elegant listener.</summary>

```ts
import { createApp } from 'h3'
import { createApp, toNodeListener } from 'h3'
import { listen } from 'listhen'

const app = createApp()
app.use('/', () => 'Hello world!')
app.use('/', eventHandler(() => 'Hello world!'))

listen(app)
listen(toNodeListener(app))
```
</details>

Expand All @@ -69,13 +69,13 @@ The `app` instance created by `h3` uses a middleware stack (see [how it works](#
To opt-in using a more advanced and convenient routing system, we can create a router instance and register it to app instance.

```ts
import { createApp, createRouter } from 'h3'
import { createApp, eventHandler, createRouter } from 'h3'

const app = createApp()

const router = createRouter()
.get('/', () => 'Hello World!')
.get('/hello/:name', req => `Hello ${req.context.params.name}!`)
.get('/', eventHandler(() => 'Hello World!'))
.get('/hello/:name', eventHandler(event => `Hello ${event.context.params.name}!`))

app.use(router)
```
Expand All @@ -84,62 +84,57 @@ app.use(router)

Routes are internally stored in a [Radix Tree](https://en.wikipedia.org/wiki/Radix_tree) and matched using [unjs/radix3](https://github.com/unjs/radix3).

## More usage examples
## More app usage examples

```js
// Handle can directly return object or Promise<object> for JSON response
app.use('/api', (req) => ({ url: req.url }))
app.use('/api', eventHandler((event) => ({ url: event.req.url }))

// We can have better matching other than quick prefix match
app.use('/odd', () => 'Is odd!', { match: url => url.substr(1) % 2 })
app.use('/odd', eventHandler(() => 'Is odd!'), { match: url => url.substr(1) % 2 })

// Handle can directly return string for HTML response
app.use(() => '<h1>Hello world!</h1>')
app.use(eventHandler(() => '<h1>Hello world!</h1>'))

// We can chain calls to .use()
app.use('/1', () => '<h1>Hello world!</h1>')
.use('/2', () => '<h1>Goodbye!</h1>')
app.use('/1', eventHandler(() => '<h1>Hello world!</h1>'))
.use('/2', eventHandler(() => '<h1>Goodbye!</h1>'))

// Legacy middleware with 3rd argument are automatically promisified
app.use((req, res, next) => { req.setHeader('X-Foo', 'bar'); next() })

// Force promisify a legacy middleware
// app.use(someMiddleware, { promisify: true })
app.use(fromNodeMiddleware((req, res, next) => { req.setHeader('X-Foo', 'bar'); next() }))

// Lazy loaded routes using { lazy: true }
// app.use('/big', () => import('./big'), { lazy: true })
app.use('/big', () => import('./big-handler'), { lazy: true })
```
## Utilities
### Built-in
H3 has concept of compasable utilities that accept `event` (from `eventHandler((event) => {})`) as their first argument. This has several performance benefits over injecting them to `event` or `app` instances and global middleware commonly used in Node.js frameworks such as Express, which Only required code is evaluated and bundled and rest of utils can be tree-shaken when not used.
Instead of adding helpers to `req` and `res`, h3 exposes them as composable utilities.
### Built-in
- `useRawBody(req, encoding?)`
- `useBody(req)`
- `useCookies(req)`
- `useCookie(req, name)`
- `setCookie(res, name, value, opts?)`
- `deleteCookie(res, name, opts?)`
- `useQuery(req)`
- `useRawBody(event, encoding?)`
- `useBody(event)`
- `useCookies(event)`
- `useCookie(event, name)`
- `setCookie(event, name, value, opts?)`
- `deleteCookie(event, name, opts?)`
- `useQuery(event)`
- `getRouterParams(event)`
- `send(res, data, type?)`
- `sendRedirect(res, location, code=302)`
- `send(event, data, type?)`
- `sendRedirect(event, location, code=302)`
- `getRequestHeaders(event, headers)` (alias: `getHeaders`)
- `getRequestHeader(event, name)` (alias: `getHeader`)
- `setResponseHeaders(event, headers)` (alias: `setHeaders`)
- `setResponseHeader(event, name, value)` (alias: `setHeader`)
- `appendResponseHeaders(event, headers)` (alias: `appendHeaders`)
- `appendResponseHeader(event, name, value)` (alias: `appendHeader`)
- `writeEarlyHints(event, links, callback)`
- `sendError(event, error, debug?)`
- `useMethod(event, default?)`
- `isMethod(event, expected, allowHead?)`
- `assertMethod(event, expected, allowHead?)`
- `createError({ statusCode, statusMessage, data? })`
- `sendError(res, error, debug?)`
- `defineHandle(handle)`
- `defineMiddleware(middlware)`
- `useMethod(req, default?)`
- `isMethod(req, expected, allowHead?)`
- `assertMethod(req, expected, allowHead?)`
👉 You can learn more about usage in [JSDocs Documentation](https://www.jsdocs.io/package/h3#package-functions).
Expand All @@ -150,19 +145,6 @@ More composable utilities can be found in community packages.
- `validateBody(event, schema)` from [h3-typebox](https://github.com/kevinmarrec/h3-typebox)
- `validateQuery(event, schema)` from [h3-typebox](https://github.com/kevinmarrec/h3-typebox)
## How it works?

Using `createApp`, it returns a standard `(req, res)` handler function and internally an array called middleware stack. using`use()` method we can add an item to this internal stack.

When a request comes, each stack item that matches the route will be called and resolved until [`res.writableEnded`](https://nodejs.org/api/http.html#http_response_writableended) flag is set, which means the response is sent. If `writableEnded` is not set after all middleware, a `404` error will be thrown. And if one of the stack items resolves to a value, it will be serialized and sent as response as a shorthand method to sending responses.

For maximum compatibility with connect/express middleware (`req, res, next?` signature), h3 converts classic middleware into a promisified version ready to use with stack runner:

- If middleware has 3rd next/callback param, the promise will `resolve/reject` when called
- If middleware returns a promise, it will be **chained** to the main promise
- If calling middleware throws an immediate error, the promise will be rejected
- On `close` and `error` events of res, the promise will `resolve/reject` (to ensure if middleware simply calls `res.end`)

## License
MIT
12 changes: 7 additions & 5 deletions playground/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { listen } from 'listhen'
import { createApp, createRouter } from '../src'
import { createApp, createRouter, eventHandler, toNodeListener, parseCookies } from '../src'

const app = createApp()
const app = createApp({ debug: true })
const router = createRouter()
.get('/', () => 'Hello World!')
.get('/hello/:name', event => `Hello ${event.context.params.name}!`)
.get('/', eventHandler(() => 'Hello World!'))
.get('/hello/:name', eventHandler((event) => {
return `Hello ${parseCookies(event)}!`
}))

app.use(router)

listen(app)
listen(toNodeListener(app))
93 changes: 27 additions & 66 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import type { IncomingMessage as NodeIncomingMessage, ServerResponse as NodeServerResponse } from 'http'
import { withoutTrailingSlash } from 'ufo'
import { lazyEventHandler, toEventHandler, createEvent, isEventHandler, eventHandler } from './event'
import { createError, sendError, isError } from './error'
import { lazyEventHandler, toEventHandler, isEventHandler, eventHandler, H3Event } from './event'
import { createError } from './error'
import { send, sendStream, isStream, MIMES } from './utils'
import type {
Handler,
LazyHandler,
Middleware,
EventHandler,
CompatibilityEvent,
CompatibilityEventHandler,
LazyEventHandler
} from './types'
import type { EventHandler, LazyEventHandler } from './types'

export interface Layer {
route: string
Expand All @@ -24,89 +15,59 @@ export type Stack = Layer[]
export interface InputLayer {
route?: string
match?: Matcher
handler: Handler | LazyHandler | EventHandler | LazyEventHandler
handler: EventHandler
lazy?: boolean
/** @deprecated */
handle?: Handler
/** @deprecated */
promisify?: boolean
}

export type InputStack = InputLayer[]

export type Matcher = (url: string, event?: CompatibilityEvent) => boolean
export type Matcher = (url: string, event?: H3Event) => boolean

export interface AppUse {
(route: string | string[], handler: CompatibilityEventHandler | CompatibilityEventHandler[], options?: Partial<InputLayer>): App
(handler: CompatibilityEventHandler | CompatibilityEventHandler[], options?: Partial<InputLayer>): App
(route: string | string[], handler: EventHandler | EventHandler[], options?: Partial<InputLayer>): App
(handler: EventHandler | EventHandler[], options?: Partial<InputLayer>): App
(options: InputLayer): App
}

export type NodeHandler = (req: NodeIncomingMessage, res: NodeServerResponse) => Promise<void>
export interface AppOptions {
debug?: boolean
onError?: (error: Error, event: H3Event) => any
}

export interface App extends NodeHandler {
export interface App {
stack: Stack
handler: EventHandler
nodeHandler: NodeHandler
options: AppOptions
use: AppUse
}

export interface AppOptions {
debug?: boolean
onError?: (error: Error, event: CompatibilityEvent) => any
}

export function createApp (options: AppOptions = {}): App {
const stack: Stack = []

const handler = createAppEventHandler(stack, options)

const nodeHandler: NodeHandler = async function (req, res) {
const event = createEvent(req, res)
try {
await handler(event)
} catch (_error: any) {
const error = createError(_error)
if (!isError(_error)) {
error.unhandled = true
}

if (options.onError) {
await options.onError(error, event)
} else {
if (error.unhandled || error.fatal) {
console.error('[h3]', error.fatal ? '[fatal]' : '[unhandled]', error) // eslint-disable-line no-console
}
await sendError(event, error, !!options.debug)
}
}
const app: App = {
// @ts-ignore
use: (arg1, arg2, arg3) => use(app as App, arg1, arg2, arg3),
handler,
stack,
options
}

const app = nodeHandler as App
app.nodeHandler = nodeHandler
app.stack = stack
app.handler = handler

// @ts-ignore
app.use = (arg1, arg2, arg3) => use(app as App, arg1, arg2, arg3)

return app as App
return app
}

export function use (
app: App,
arg1: string | Handler | InputLayer | InputLayer[],
arg2?: Handler | Partial<InputLayer> | Handler[] | Middleware | Middleware[],
arg1: string | EventHandler | InputLayer | InputLayer[],
arg2?: Partial<InputLayer> | EventHandler | EventHandler[],
arg3?: Partial<InputLayer>
) {
if (Array.isArray(arg1)) {
arg1.forEach(i => use(app, i, arg2, arg3))
} else if (Array.isArray(arg2)) {
arg2.forEach(i => use(app, arg1, i, arg3))
} else if (typeof arg1 === 'string') {
app.stack.push(normalizeLayer({ ...arg3, route: arg1, handler: arg2 as Handler }))
app.stack.push(normalizeLayer({ ...arg3, route: arg1, handler: arg2 as EventHandler }))
} else if (typeof arg1 === 'function') {
app.stack.push(normalizeLayer({ ...arg2, route: '/', handler: arg1 as Handler }))
app.stack.push(normalizeLayer({ ...arg2, route: '/', handler: arg1 as EventHandler }))
} else {
app.stack.push(normalizeLayer({ ...arg1 }))
}
Expand All @@ -116,7 +77,7 @@ export function use (
export function createAppEventHandler (stack: Stack, options: AppOptions) {
const spacing = options.debug ? 2 : undefined
return eventHandler(async (event) => {
event.req.originalUrl = event.req.originalUrl || event.req.url || '/'
(event.req as any).originalUrl = (event.req as any).originalUrl || event.req.url || '/'
const reqUrl = event.req.url || '/'
for (const layer of stack) {
if (layer.route.length > 1) {
Expand Down Expand Up @@ -159,7 +120,7 @@ export function createAppEventHandler (stack: Stack, options: AppOptions) {
}

function normalizeLayer (input: InputLayer) {
let handler = input.handler || input.handle
let handler = input.handler
// @ts-ignore
if (handler.handler) {
// @ts-ignore
Expand All @@ -169,7 +130,7 @@ function normalizeLayer (input: InputLayer) {
if (input.lazy) {
handler = lazyEventHandler(handler as LazyEventHandler)
} else if (!isEventHandler(handler)) {
handler = toEventHandler(handler)
handler = toEventHandler(handler, null, input.route)
}

return {
Expand Down
6 changes: 3 additions & 3 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CompatibilityEvent } from './types'
import type { H3Event } from './event'
import { MIMES } from './utils'

/**
Expand Down Expand Up @@ -62,12 +62,12 @@ export function createError (input: string | Partial<H3Error>): H3Error {
* H3 internally uses this function to handle unhandled errors.<br>
* Note that calling this function will close the connection and no other data will be sent to client afterwards.
*
@param event {CompatibilityEvent} H3 event or req passed by h3 handler
@param event {H3Event} H3 event or req passed by h3 handler
* @param error {H3Error|Error} Raised error
* @param debug {Boolean} Whether application is in debug mode.<br>
* In the debug mode the stack trace of errors will be return in response.
*/
export function sendError (event: CompatibilityEvent, error: Error | H3Error, debug?: boolean) {
export function sendError (event: H3Event, error: Error | H3Error, debug?: boolean) {
if (event.res.writableEnded) { return }

const h3Error = isError(error) ? error : createError(error)
Expand Down

0 comments on commit ba2fe08

Please sign in to comment.