From 7ad1367a7025a85d60fb845b703a28817fdbbaa3 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 29 Mar 2022 12:56:15 +0200 Subject: [PATCH] refactor!: use events api for utils with compatibility layer --- src/app.ts | 52 +++++++++++++++++++-------------------- src/error.ts | 20 +++++++-------- src/event.ts | 40 +++++++++++++++++++++++------- src/handle.ts | 2 +- src/router.ts | 2 -- src/types/index.ts | 3 +++ src/types/node.ts | 19 +++++++++++++- src/utils/body.ts | 48 +++++++++++++----------------------- src/utils/cookie.ts | 26 ++++++++++---------- src/utils/request.ts | 18 +++++++------- src/utils/response.ts | 34 ++++++++++++------------- test/integrations.test.ts | 2 -- 12 files changed, 145 insertions(+), 121 deletions(-) create mode 100644 src/types/index.ts diff --git a/src/app.ts b/src/app.ts index e2988d7e..41add717 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,7 @@ +import type http from 'http' import { withoutTrailingSlash } from 'ufo' import { lazyHandle, promisifyHandle } from './handle' -import { toEventHandler, createEvent } from './event' +import { toEventHandler, createEvent, H3CompatibilityEvent } from './event' import { createError, sendError } from './error' import { send, sendStream, isStream, MIMES } from './utils' import type { IncomingMessage, ServerResponse } from './types/node' @@ -25,7 +26,7 @@ export interface InputLayer { export type InputStack = InputLayer[] -export type Matcher = (url: string, req?: IncomingMessage) => boolean +export type Matcher = (url: string, event?: H3CompatibilityEvent) => boolean export interface AppUse { (route: string | string [], handle: Middleware | Middleware[], options?: Partial): App @@ -35,8 +36,9 @@ export interface AppUse { (options: InputLayer): App } -export interface App { - (req: IncomingMessage, res: ServerResponse): Promise +export type AppHandler = (req: http.IncomingMessage, res: http.ServerResponse) => Promise + +export interface App extends AppHandler { stack: Stack _handle: PHandle use: AppUse @@ -44,7 +46,7 @@ export interface App { export interface AppOptions { debug?: boolean - onError?: (error: Error, req: IncomingMessage, res: ServerResponse) => any + onError?: (error: Error, event: H3CompatibilityEvent) => any } export function createApp (options: AppOptions = {}): App { @@ -52,15 +54,15 @@ export function createApp (options: AppOptions = {}): App { const _handle = createHandle(stack, options) - // @ts-ignore - const app: Partial = function (req: IncomingMessage, res: ServerResponse) { - return _handle(req, res).catch((error: Error) => { + const app: App = function (req, res) { + const event = createEvent(req, res) + return _handle(event).catch((error: Error) => { if (options.onError) { - return options.onError(error, req, res) + return options.onError(error, event) } - return sendError(res, error, !!options.debug) + return sendError(event, error, !!options.debug) }) - } + } as App app.stack = stack app._handle = _handle @@ -91,46 +93,44 @@ export function use ( return app } -export function createHandle (stack: Stack, options: AppOptions): PHandle { +export function createHandle (stack: Stack, options: AppOptions) { const spacing = options.debug ? 2 : undefined - return async function handle (req: IncomingMessage, res: ServerResponse) { - const event = createEvent(req, res) + return async function handle (event: H3CompatibilityEvent) { + event.req.originalUrl = event.req.originalUrl || event.req.url || '/' - // @ts-ignore express/connect compatibility - req.originalUrl = req.originalUrl || req.url || '/' - const reqUrl = req.url || '/' + const reqUrl = event.req.url || '/' for (const layer of stack) { if (layer.route.length > 1) { if (!reqUrl.startsWith(layer.route)) { continue } - req.url = reqUrl.slice(layer.route.length) || '/' + event.req.url = reqUrl.slice(layer.route.length) || '/' } else { - req.url = reqUrl + event.req.url = reqUrl } - if (layer.match && !layer.match(req.url as string, req)) { + if (layer.match && !layer.match(event.req.url as string, event)) { continue } const val = await layer.handler(event) - if (res.writableEnded) { + if (event.res.writableEnded) { return } const type = typeof val if (type === 'string') { - return send(res, val, MIMES.html) + return send(event, val, MIMES.html) } else if (isStream(val)) { - return sendStream(res, val) + return sendStream(event, val) } else if (type === 'object' || type === 'boolean' || type === 'number' /* IS_JSON */) { if (val && (val as Buffer).buffer) { - return send(res, val) + return send(event, val) } else if (val instanceof Error) { throw createError(val) } else { - return send(res, JSON.stringify(val, null, spacing), MIMES.json) + return send(event, JSON.stringify(val, null, spacing), MIMES.json) } } } - if (!res.writableEnded) { + if (!event.res.writableEnded) { throw createError({ statusCode: 404, statusMessage: 'Not Found' }) } } diff --git a/src/error.ts b/src/error.ts index 294de4bc..b5178df3 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,4 +1,4 @@ -import type { ServerResponse } from './types/node' +import type { H3CompatibilityEvent } from './event' import { MIMES } from './utils' /** @@ -50,12 +50,12 @@ export function createError (input: Partial): H3Error { * H3 internally uses this function to handle unhandled errors.
* Note that calling this function will close the connection and no other data will be sent to client afterwards. * - * @param res {ServerResponse} The ServerResponse object is passed as the second parameter in the handler function + @param event {H3CompatibilityEvent} H3 event or req passed by h3 handler * @param error {H3Error|Error} Raised error * @param debug {Boolean} Whether application is in debug mode.
* In the debug mode the stack trace of errors will be return in response. */ -export function sendError (res: ServerResponse, error: Error | H3Error, debug?: boolean) { +export function sendError (event: H3CompatibilityEvent, error: Error | H3Error, debug?: boolean) { let h3Error: H3Error if (error instanceof H3Error) { h3Error = error @@ -64,16 +64,16 @@ export function sendError (res: ServerResponse, error: Error | H3Error, debug?: h3Error = createError(error) } - if (res.writableEnded) { + if (event.res.writableEnded) { return } - res.statusCode = h3Error.statusCode - res.statusMessage = h3Error.statusMessage + event.res.statusCode = h3Error.statusCode + event.res.statusMessage = h3Error.statusMessage const responseBody = { - statusCode: res.statusCode, - statusMessage: res.statusMessage, + statusCode: event.res.statusCode, + statusMessage: event.res.statusMessage, stack: [] as string[], data: h3Error.data } @@ -82,6 +82,6 @@ export function sendError (res: ServerResponse, error: Error | H3Error, debug?: responseBody.stack = (h3Error.stack || '').split('\n').map(l => l.trim()) } - res.setHeader('Content-Type', MIMES.json) - res.end(JSON.stringify(responseBody, null, 2)) + event.res.setHeader('Content-Type', MIMES.json) + event.res.end(JSON.stringify(responseBody, null, 2)) } diff --git a/src/event.ts b/src/event.ts index 196ef886..0c86bd56 100644 --- a/src/event.ts +++ b/src/event.ts @@ -1,19 +1,23 @@ +import type http from 'http' import type { IncomingMessage, ServerResponse } from './types/node' import type { Handle, Middleware } from './handle' export interface H3Event { + '__is_event__': true req: IncomingMessage res: ServerResponse, next?: (err?: Error) => void } +export type H3CompatibilityEvent = H3Event | IncomingMessage | ServerResponse + export type _JSONValue = T | T[] | Record export type JSONValue = _JSONValue<_JSONValue> export type H3Response = void | JSONValue | Buffer export interface H3EventHandler { - __is_handler__?: true - (event: H3Event): H3Response| Promise + '__is_handler__'?: true + (event: H3CompatibilityEvent): H3Response| Promise } export function defineEventHandler (handler: H3EventHandler) { @@ -21,8 +25,8 @@ export function defineEventHandler (handler: H3EventHandler) { return handler } -export function isEventHandler (handler: H3EventHandler | Handle | Middleware): handler is H3EventHandler { - return '__is_handler__' in handler +export function isEventHandler (input: any): input is H3EventHandler { + return '__is_handler__' in input } export function toEventHandler (handler: H3EventHandler | Handle | Middleware): H3EventHandler { @@ -31,18 +35,36 @@ export function toEventHandler (handler: H3EventHandler | Handle | Middleware): } if (handler.length > 2) { return defineEventHandler((event) => { - return (handler as Middleware)(event.req, event.res, event.next!) + return (handler as Middleware)(event.req as IncomingMessage, event.res, event.next!) }) } else { return defineEventHandler((event) => { - return (handler as Handle)(event.req, event.res) + return (handler as Handle)(event.req as IncomingMessage, event.res) }) } } -export function createEvent (req: IncomingMessage, res: ServerResponse): H3Event { - return { +export function createEvent (req: http.IncomingMessage, res: http.ServerResponse): H3CompatibilityEvent { + const event = { + __is_event__: true, req, res - } + } as H3Event + + // Add backward comatibility for interchangable usage of {event,req,res}.{event,req,res} + event.event = event + + req.event = event + req.req = req + req.res = res + + res.event = event + // res.req = req + res.res = res + + return event +} + +export function isEvent (input: any): input is H3Event { + return '__is_event__' in input } diff --git a/src/handle.ts b/src/handle.ts index 0425db82..f8fad0c7 100644 --- a/src/handle.ts +++ b/src/handle.ts @@ -1,5 +1,5 @@ import { withoutTrailingSlash, withoutBase } from 'ufo' -import type { IncomingMessage, ServerResponse } from './types/node' +import type { IncomingMessage, ServerResponse } from './types' export type Handle = (req: IncomingMessage & ReqT, res: ServerResponse) => T export type PHandle = Handle> diff --git a/src/router.ts b/src/router.ts index bee1179e..13c8ffe4 100644 --- a/src/router.ts +++ b/src/router.ts @@ -64,11 +64,9 @@ export function createRouter (): Router { } // Add params - // @ts-ignore req.params = matched.params || {} // Call handler - // @ts-ignore return handler(req, res) } diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..cd06f4ec --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,3 @@ +export * from './cookie' +export * from './http' +export * from './node' diff --git a/src/types/node.ts b/src/types/node.ts index ba6e5399..28ff15aa 100644 --- a/src/types/node.ts +++ b/src/types/node.ts @@ -1,3 +1,20 @@ -export type { IncomingMessage, ServerResponse } from 'http' +import type http from 'http' +import type { H3Event } from '../event' + +export interface H3CompatbilityAugmentions { + event: H3Event, + req: H3Event['req'], + res: H3Event['res'] +} + +export interface IncomingMessage extends http.IncomingMessage, H3CompatbilityAugmentions { + originalUrl?: string // Connect and Express +} +export interface ServerResponse extends http.ServerResponse, Omit { + req: http.ServerResponse['req'] & { + event: H3Event + originalUrl?: string // Connect and Express + } +} export type Encoding = false | 'ascii' | 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'base64' | 'latin1' | 'binary' | 'hex' diff --git a/src/utils/body.ts b/src/utils/body.ts index 9ff7362e..92ca8574 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -1,7 +1,7 @@ -import type { IncomingMessage } from 'http' import destr from 'destr' import type { Encoding } from '../types/node' import type { HTTPMethod } from '../types/http' +import type { H3CompatibilityEvent } from '../event' import { assertMethod } from './request' const RawBodySymbol = Symbol('h3RawBody') @@ -9,49 +9,41 @@ const ParsedBodySymbol = Symbol('h3RawBody') const PayloadMethods = ['PATCH', 'POST', 'PUT', 'DELETE'] as HTTPMethod[] -interface _IncomingMessage extends IncomingMessage { - [RawBodySymbol]?: Promise - ParsedBodySymbol?: any - body?: any // unenv -} - /** * Reads body of the request and returns encoded raw string (default) or `Buffer` if encoding if falsy. - * @param req {IncomingMessage} An IncomingMessage object is created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server) + * @param event {H3CompatibilityEvent} H3 event or req passed by h3 handler * @param encoding {Encoding} encoding="utf-8" - The character encoding to use. * * @return {String|Buffer} Encoded raw string or raw Buffer of the body */ -export function useRawBody (req: _IncomingMessage, encoding: Encoding = 'utf-8'): Encoding extends false ? Buffer : Promise { +export function useRawBody (event: H3CompatibilityEvent, encoding: Encoding = 'utf-8'): Encoding extends false ? Buffer : Promise { // Ensure using correct HTTP method before attempt to read payload - assertMethod(req, PayloadMethods) + assertMethod(event, PayloadMethods) - if (RawBodySymbol in req) { - const promise = Promise.resolve(req[RawBodySymbol]) - // @ts-ignore + if (RawBodySymbol in event.req) { + const promise = Promise.resolve((event.req as any)[RawBodySymbol]) return encoding ? promise.then(buff => buff.toString(encoding)) : promise } // Workaround for unenv issue https://github.com/unjs/unenv/issues/8 - if ('body' in req) { - return Promise.resolve(req.body) + if ('body' in event.req) { + return Promise.resolve((event.req as any).body) } - const promise = req[RawBodySymbol] = new Promise((resolve, reject) => { + const promise = (event.req as any)[RawBodySymbol] = new Promise((resolve, reject) => { const bodyData: any[] = [] - req + event.req .on('error', (err) => { reject(err) }) .on('data', (chunk) => { bodyData.push(chunk) }) .on('end', () => { resolve(Buffer.concat(bodyData)) }) }) - // @ts-ignore return encoding ? promise.then(buff => buff.toString(encoding)) : promise } /** * Reads request body and try to safely parse using [destr](https://github.com/unjs/destr) - * @param req {IncomingMessage} An IncomingMessage object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server) + * @param event {H3CompatibilityEvent} H3 event or req passed by h3 handler * @param encoding {Encoding} encoding="utf-8" - The character encoding to use. * * @return {*} The `Object`, `Array`, `String`, `Number`, `Boolean`, or `null` value corresponding to the request JSON body @@ -60,18 +52,12 @@ export function useRawBody (req: _IncomingMessage, encoding: Encoding = 'utf-8') * const body = await useBody(req) * ``` */ -export async function useBody (req: _IncomingMessage): Promise { - // @ts-ignore - if (ParsedBodySymbol in req) { - // @ts-ignore - return req[ParsedBodySymbol] +export async function useBody (event: H3CompatibilityEvent): Promise { + if (ParsedBodySymbol in event.req) { + return (event.req as any)[ParsedBodySymbol] } - - const body = await useRawBody(req) - const json = destr(body) - - // @ts-ignore - req[ParsedBodySymbol] = json - + const body = await useRawBody(event) + const json = destr(body) as T + (event.req as any)[ParsedBodySymbol] = json return json } diff --git a/src/utils/cookie.ts b/src/utils/cookie.ts index e9feca19..0057aa62 100644 --- a/src/utils/cookie.ts +++ b/src/utils/cookie.ts @@ -1,36 +1,36 @@ -import type { IncomingMessage, ServerResponse } from 'http' import { parse, serialize } from 'cookie-es' import type { CookieSerializeOptions } from '../types/cookie' +import type { H3CompatibilityEvent } from '../event' import { appendHeader } from './response' /** * Parse the request to get HTTP Cookie header string and returning an object of all cookie name-value pairs. - * @param req {IncomingMessage} An IncomingMessage object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server) + * @param event {H3CompatibilityEvent} H3 event or req passed by h3 handler * @returns Object of cookie name-value pairs * ```ts * const cookies = useCookies(req) * ``` */ -export function useCookies (req: IncomingMessage): Record { - return parse(req.headers.cookie || '') +export function useCookies (event: H3CompatibilityEvent): Record { + return parse(event.req.headers.cookie || '') } /** * Get a cookie value by name. - * @param req {IncomingMessage} An IncomingMessage object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server) + * @param event {H3CompatibilityEvent} H3 event or req passed by h3 handler * @param name Name of the cookie to get * @returns {*} Value of the cookie (String or undefined) * ```ts * const authorization = useCookie(request, 'Authorization') * ``` */ -export function useCookie (req: IncomingMessage, name: string): string | undefined { - return useCookies(req)[name] +export function useCookie (event: H3CompatibilityEvent, name: string): string | undefined { + return useCookies(event)[name] } /** * Set a cookie value by name. - * @param res {ServerResponse} A ServerResponse object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server) + * @param event {H3CompatibilityEvent} H3 event or res passed by h3 handler * @param name Name of the cookie to set * @param value Value of the cookie to set * @param serializeOptions {CookieSerializeOptions} Options for serializing the cookie @@ -38,22 +38,22 @@ export function useCookie (req: IncomingMessage, name: string): string | undefin * setCookie(res, 'Authorization', '1234567') * ``` */ -export function setCookie (res: ServerResponse, name: string, value: string, serializeOptions?: CookieSerializeOptions) { +export function setCookie (event: H3CompatibilityEvent, name: string, value: string, serializeOptions?: CookieSerializeOptions) { const cookieStr = serialize(name, value, serializeOptions) - appendHeader(res, 'Set-Cookie', cookieStr) + appendHeader(event, 'Set-Cookie', cookieStr) } /** * Set a cookie value by name. - * @param res {ServerResponse} A ServerResponse object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server) + * @param event {H3CompatibilityEvent} H3 event or res passed by h3 handler * @param name Name of the cookie to delete * @param serializeOptions {CookieSerializeOptions} Cookie options * ```ts * deleteCookie(res, 'SessionId') * ``` */ -export function deleteCookie (res: ServerResponse, name: string, serializeOptions?: CookieSerializeOptions) { - setCookie(res, name, '', { +export function deleteCookie (event: H3CompatibilityEvent, name: string, serializeOptions?: CookieSerializeOptions) { + setCookie(event, name, '', { ...serializeOptions, maxAge: 0 }) diff --git a/src/utils/request.ts b/src/utils/request.ts index dbd4a4fd..80eb1fd4 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,18 +1,18 @@ -import type { IncomingMessage } from 'http' import { getQuery } from 'ufo' import { createError } from '../error' import type { HTTPMethod } from '../types/http' +import type { H3CompatibilityEvent } from '../event' -export function useQuery (req: IncomingMessage) { - return getQuery(req.url || '') +export function useQuery (event: H3CompatibilityEvent) { + return getQuery(event.req.url || '') } -export function useMethod (req: IncomingMessage, defaultMethod: HTTPMethod = 'GET'): HTTPMethod { - return (req.method || defaultMethod).toUpperCase() as HTTPMethod +export function useMethod (event: H3CompatibilityEvent, defaultMethod: HTTPMethod = 'GET'): HTTPMethod { + return (event.req.method || defaultMethod).toUpperCase() as HTTPMethod } -export function isMethod (req: IncomingMessage, expected: HTTPMethod | HTTPMethod[], allowHead?: boolean) { - const method = useMethod(req) +export function isMethod (event: H3CompatibilityEvent, expected: HTTPMethod | HTTPMethod[], allowHead?: boolean) { + const method = useMethod(event) if (allowHead && method === 'HEAD') { return true @@ -29,8 +29,8 @@ export function isMethod (req: IncomingMessage, expected: HTTPMethod | HTTPMetho return false } -export function assertMethod (req: IncomingMessage, expected: HTTPMethod | HTTPMethod[], allowHead?: boolean) { - if (!isMethod(req, expected, allowHead)) { +export function assertMethod (event: H3CompatibilityEvent, expected: HTTPMethod | HTTPMethod[], allowHead?: boolean) { + if (!isMethod(event, expected, allowHead)) { throw createError({ statusCode: 405, statusMessage: 'HTTP method is not allowed.' diff --git a/src/utils/response.ts b/src/utils/response.ts index 0ff0a19a..387f5e22 100644 --- a/src/utils/response.ts +++ b/src/utils/response.ts @@ -1,38 +1,38 @@ -import type { ServerResponse } from 'http' import { createError } from '../error' +import type { H3CompatibilityEvent } from '../event' import { MIMES } from './consts' const defer = typeof setImmediate !== 'undefined' ? setImmediate : (fn: Function) => fn() -export function send (res: ServerResponse, data: any, type?: string): Promise { +export function send (event: H3CompatibilityEvent, data: any, type?: string): Promise { if (type) { - defaultContentType(res, type) + defaultContentType(event, type) } return new Promise((resolve) => { defer(() => { - res.end(data) + event.res.end(data) resolve(undefined) }) }) } -export function defaultContentType (res: ServerResponse, type?: string) { - if (type && !res.getHeader('Content-Type')) { - res.setHeader('Content-Type', type) +export function defaultContentType (event: H3CompatibilityEvent, type?: string) { + if (type && !event.res.getHeader('Content-Type')) { + event.res.setHeader('Content-Type', type) } } -export function sendRedirect (res: ServerResponse, location: string, code = 302) { - res.statusCode = code - res.setHeader('Location', location) - return send(res, 'Redirecting to ' + location, MIMES.html) +export function sendRedirect (event: H3CompatibilityEvent, location: string, code = 302) { + event.res.statusCode = code + event.res.setHeader('Location', location) + return send(event, 'Redirecting to ' + location, MIMES.html) } -export function appendHeader (res: ServerResponse, name: string, value: string): void { - let current = res.getHeader(name) +export function appendHeader (event: H3CompatibilityEvent, name: string, value: string): void { + let current = event.res.getHeader(name) if (!current) { - res.setHeader(name, value) + event.res.setHeader(name, value) return } @@ -40,16 +40,16 @@ export function appendHeader (res: ServerResponse, name: string, value: string): current = [current.toString()] } - res.setHeader(name, current.concat(value)) + event.res.setHeader(name, current.concat(value)) } export function isStream (data: any) { return typeof data === 'object' && typeof data.pipe === 'function' && typeof data.on === 'function' } -export function sendStream (res: ServerResponse, data: any) { +export function sendStream (event: H3CompatibilityEvent, data: any) { return new Promise((resolve, reject) => { - data.pipe(res) + data.pipe(event.res) data.on('end', () => resolve(undefined)) data.on('error', (error: Error) => reject(createError(error))) }) diff --git a/test/integrations.test.ts b/test/integrations.test.ts index 7a3428c5..0539df17 100644 --- a/test/integrations.test.ts +++ b/test/integrations.test.ts @@ -40,7 +40,6 @@ describe('integrations with other frameworks', () => { it('can wrap a connect instance', async () => { const connectApp = createConnectApp() - // @ts-ignore connectApp.use('/api/connect', (_req, res) => { res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ connect: 'works' })) @@ -53,7 +52,6 @@ describe('integrations with other frameworks', () => { it('can be used as connect middleware', async () => { const connectApp = createConnectApp() - // @ts-ignore - remove when #10 is merged app.use('/api/hello', (_req, res, next) => { ;(res as any).prop = '42' next()