diff --git a/src/app.ts b/src/app.ts index e2988d7e..5de7001f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,16 +1,16 @@ +import type http from 'http' import { withoutTrailingSlash } from 'ufo' -import { lazyHandle, promisifyHandle } from './handle' -import { toEventHandler, createEvent } from './event' +import { defineLazyHandler } from './handler' +import { toEventHandler, createEvent, isEventHandler } from './event' import { createError, sendError } from './error' import { send, sendStream, isStream, MIMES } from './utils' -import type { IncomingMessage, ServerResponse } from './types/node' -import type { Handle, LazyHandle, Middleware, PHandle } from './handle' -import type { H3EventHandler } from './event' +import type { Handler, LazyHandler, Middleware, PromisifiedHandler } from './types' +import type { EventHandler, CompatibilityEvent } from './event' export interface Layer { route: string match?: Matcher - handler: H3EventHandler + handler: EventHandler } export type Stack = Layer[] @@ -18,52 +18,56 @@ export type Stack = Layer[] export interface InputLayer { route?: string match?: Matcher - handle: Handle | LazyHandle + handler: Handler | LazyHandler lazy?: boolean + /** + * @deprecated + */ promisify?: boolean } export type InputStack = InputLayer[] -export type Matcher = (url: string, req?: IncomingMessage) => boolean +export type Matcher = (url: string, event?: CompatibilityEvent) => boolean + +export type RequestHandler = EventHandler | Handler | Middleware export interface AppUse { - (route: string | string [], handle: Middleware | Middleware[], options?: Partial): App - (route: string | string[], handle: Handle | Handle[], options?: Partial): App - (handle: Middleware | Middleware[], options?: Partial): App - (handle: Handle | Handle[], options?: Partial): App + (route: string | string [], handler: RequestHandler | RequestHandler[], options?: Partial): App + (handler: RequestHandler | Handler[], options?: Partial): App (options: InputLayer): App } -export interface App { - (req: IncomingMessage, res: ServerResponse): Promise +export type ApPromisifiedHandlerr = (req: http.IncomingMessage, res: http.ServerResponse) => Promise + +export interface App extends ApPromisifiedHandlerr { stack: Stack - _handle: PHandle + _handler: PromisifiedHandler use: AppUse } export interface AppOptions { debug?: boolean - onError?: (error: Error, req: IncomingMessage, res: ServerResponse) => any + onError?: (error: Error, event: CompatibilityEvent) => any } export function createApp (options: AppOptions = {}): App { const stack: Stack = [] - const _handle = createHandle(stack, options) + const _handler = createHandler(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 _handler(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 + app._handler = _handler // @ts-ignore app.use = (arg1, arg2, arg3) => use(app as App, arg1, arg2, arg3) @@ -73,8 +77,8 @@ export function createApp (options: AppOptions = {}): App { export function use ( app: App, - arg1: string | Handle | InputLayer | InputLayer[], - arg2?: Handle | Partial | Handle[] | Middleware | Middleware[], + arg1: string | Handler | InputLayer | InputLayer[], + arg2?: Handler | Partial | Handler[] | Middleware | Middleware[], arg3?: Partial ) { if (Array.isArray(arg1)) { @@ -82,72 +86,70 @@ export function use ( } 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, handle: arg2 as Handle })) + app.stack.push(normalizeLayer({ ...arg3, route: arg1, handler: arg2 as Handler })) } else if (typeof arg1 === 'function') { - app.stack.push(normalizeLayer({ ...arg2, route: '/', handle: arg1 as Handle })) + app.stack.push(normalizeLayer({ ...arg2, route: '/', handler: arg1 as Handler })) } else { app.stack.push(normalizeLayer({ ...arg1 })) } return app } -export function createHandle (stack: Stack, options: AppOptions): PHandle { +export function createHandler (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: CompatibilityEvent) { + 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' }) } } } function normalizeLayer (input: InputLayer) { - if (input.promisify === undefined) { - input.promisify = input.handle.length > 2 /* req, res, next */ + let handler = input.handler + if (!isEventHandler(handler)) { + if (input.lazy) { + handler = defineLazyHandler(handler as LazyHandler) + } + handler = toEventHandler(handler) } - const handle = input.lazy - ? lazyHandle(input.handle as LazyHandle, input.promisify) - : (input.promisify ? promisifyHandle(input.handle) : input.handle) - return { route: withoutTrailingSlash(input.route), match: input.match, - handler: toEventHandler(handle) - } + handler + } as Layer } diff --git a/src/error.ts b/src/error.ts index 294de4bc..8b181b70 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,4 +1,4 @@ -import type { ServerResponse } from './types/node' +import type { CompatibilityEvent } 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 {CompatibilityEvent} 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: CompatibilityEvent, 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..8b2d5aa9 100644 --- a/src/event.ts +++ b/src/event.ts @@ -1,48 +1,97 @@ -import type { IncomingMessage, ServerResponse } from './types/node' -import type { Handle, Middleware } from './handle' +import type http from 'http' +import type { IncomingMessage, ServerResponse, Handler, Middleware } from './types' +import { callHandler } from './handler' export interface H3Event { + '__is_event__': true + event: H3Event req: IncomingMessage - res: ServerResponse, - next?: (err?: Error) => void + res: ServerResponse + /** + * Request params only filled with h3 Router handlers + */ + params?: Record } +export type CompatibilityEvent = 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 +export interface EventHandler { + '__is_handler__'?: true + (event: CompatibilityEvent): H3Response| Promise } -export function defineEventHandler (handler: H3EventHandler) { +export function defineEventHandler (handler: EventHandler) { handler.__is_handler__ = true return handler } -export function isEventHandler (handler: H3EventHandler | Handle | Middleware): handler is H3EventHandler { - return '__is_handler__' in handler +export function defineLazyEventHandler (factory: () => EventHandler | Promise): EventHandler { + let _promise: Promise + let _resolved: EventHandler + const resolveHandler = () => { + if (_resolved) { return Promise.resolve(_resolved) } + if (!_promise) { + _promise = Promise.resolve(factory()).then((r: any) => { + _resolved = r.default || r + return _resolved + }) + } + return _promise + } + return defineEventHandler((event) => { + if (_resolved) { + return _resolved(event) + } + return resolveHandler().then(handler => handler(event)) + }) } -export function toEventHandler (handler: H3EventHandler | Handle | Middleware): H3EventHandler { +export function isEventHandler (input: any): input is EventHandler { + return '__is_handler__' in input +} + +export function toEventHandler (handler: EventHandler | Handler | Middleware): EventHandler { if (isEventHandler(handler)) { return handler } - if (handler.length > 2) { - return defineEventHandler((event) => { - return (handler as Middleware)(event.req, event.res, event.next!) - }) - } else { - return defineEventHandler((event) => { - return (handler as Handle)(event.req, event.res) - }) - } + return defineEventHandler((event) => { + return callHandler(handler, event.req as IncomingMessage, event.res) as Promise + }) } -export function createEvent (req: IncomingMessage, res: ServerResponse): H3Event { - return { +export function createEvent (req: http.IncomingMessage, res: http.ServerResponse): CompatibilityEvent { + const event = { + __is_event__: true, req, res - } + } as H3Event + + // Backward comatibility for interchangable usage of {event,req,res}.{req,res} + // TODO: Remove in future versions + // @ts-ignore + event.event = event + // @ts-ignore + req.event = event + // @ts-ignore + req.req = req + // @ts-ignore + req.res = res + // @ts-ignore + res.event = event + // @ts-ignore + res.res = res + // @ts-ignore + res.req.res = res + // @ts-ignore + res.req.req = req + + 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 deleted file mode 100644 index 0425db82..00000000 --- a/src/handle.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { withoutTrailingSlash, withoutBase } from 'ufo' -import type { IncomingMessage, ServerResponse } from './types/node' - -export type Handle = (req: IncomingMessage & ReqT, res: ServerResponse) => T -export type PHandle = Handle> -export type Middleware = (req: IncomingMessage, res: ServerResponse, next: (err?: Error) => any) => any -export type LazyHandle = () => Handle | Promise - -export const defineHandle = (handler: Handle) => handler -export const defineMiddleware = (middleware: Middleware) => middleware - -export function promisifyHandle (handle: Handle | Middleware): PHandle { - return function (req: IncomingMessage, res: ServerResponse) { - return callHandle(handle, req, res) - } -} - -export function callHandle (handle: Middleware, req: IncomingMessage, res: ServerResponse) { - return new Promise((resolve, reject) => { - const next = (err?: Error) => err ? reject(err) : resolve(undefined) - try { - const returned = handle(req, res, next) - if (returned !== undefined) { - resolve(returned) - } else { - res.once('close', next) - res.once('error', next) - } - } catch (err) { - next(err as Error) - } - }) -} - -export function lazyHandle (handle: LazyHandle, promisify?: boolean): PHandle { - let _promise: Promise - const resolve = () => { - if (!_promise) { - _promise = Promise.resolve(handle()) - .then((r: any) => promisify ? promisifyHandle(r.default || r) : (r.default || r)) - } - return _promise - } - return function (req: IncomingMessage, res: ServerResponse) { - return resolve().then(h => h(req, res)) - } -} - -export function useBase (base: string, handle: PHandle): PHandle { - base = withoutTrailingSlash(base) - if (!base) { return handle } - return function (req, res) { - (req as any).originalUrl = (req as any).originalUrl || req.url || '/' - req.url = withoutBase(req.url || '/', base) - return handle(req, res) - } -} diff --git a/src/handler.ts b/src/handler.ts new file mode 100644 index 00000000..f403f53e --- /dev/null +++ b/src/handler.ts @@ -0,0 +1,53 @@ +import { withoutTrailingSlash, withoutBase } from 'ufo' +import type { Handler, PromisifiedHandler, Middleware, IncomingMessage, ServerResponse, LazyHandler } from './types' + +export const defineHandler = (handler: Handler) => handler +/** @deprecated Use defineHandler */ +export const defineHandle = defineHandler +export const defineMiddleware = (middleware: Middleware) => middleware + +export function promisifyHandler (handler: Handler | Middleware): PromisifiedHandler { + return function (req: IncomingMessage, res: ServerResponse) { + return callHandler(handler, req, res) + } +} + +export function callHandler (handler: Middleware, req: IncomingMessage, res: ServerResponse) { + return new Promise((resolve, reject) => { + const next = (err?: Error) => err ? reject(err) : resolve(undefined) + try { + return resolve(handler(req, res, next)) + } catch (err) { + next(err as Error) + } + }) +} + +export function defineLazyHandler (handler: LazyHandler, promisify?: boolean): PromisifiedHandler { + let _promise: Promise + const resolve = () => { + if (!_promise) { + _promise = Promise.resolve(handler()) + .then((r: any) => promisify ? promisifyHandler(r.default || r) : (r.default || r)) + } + return _promise + } + return function (req: IncomingMessage, res: ServerResponse) { + return resolve().then(h => h(req, res)) + } +} + +/** + * @deprecated Use new events API or defineLazyHandler + */ +export const lazyHandle = defineLazyHandler + +export function useBase (base: string, handler: PromisifiedHandler): PromisifiedHandler { + base = withoutTrailingSlash(base) + if (!base) { return handler } + return function (req, res) { + (req as any).originalUrl = (req as any).originalUrl || req.url || '/' + req.url = withoutBase(req.url || '/', base) + return handler(req, res) + } +} diff --git a/src/index.ts b/src/index.ts index 22a698be..098bc3a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ export * from './app' export * from './error' export * from './event' -export * from './handle' +export * from './handler' export * from './utils' export * from './router' diff --git a/src/router.ts b/src/router.ts index bee1179e..0064de85 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,23 +1,22 @@ import { createRouter as _createRouter } from 'radix3' -import type { Handle } from './handle' -import type { HTTPMethod } from './types/http' +import type { HTTPMethod } from './types' import { createError } from './error' +import { defineEventHandler, EventHandler, toEventHandler } from './event' +import type { RequestHandler } from './app' export type RouterMethod = Lowercase const RouterMethods: Lowercase[] = ['connect', 'delete', 'get', 'head', 'options', 'post', 'put', 'trace'] -export type HandleWithParams = Handle }> - -export type AddWithMethod = (path: string, handle: HandleWithParams) => Router +export type AddWithMethod = (path: string, handler: RequestHandler) => Router export type AddRouteShortcuts = Record, AddWithMethod> export interface Router extends AddRouteShortcuts { - add: (path: string, handle: HandleWithParams, method?: RouterMethod | 'all') => Router - handle: Handle + add: (path: string, handler: RequestHandler, method?: RouterMethod | 'all') => Router + handler: RequestHandler } interface RouteNode { - handlers: Partial> + handlers: Partial> } export function createRouter (): Router { @@ -27,13 +26,13 @@ export function createRouter (): Router { const router: Router = {} as Router // Utilities to add a new route - router.add = (path, handle, method = 'all') => { + router.add = (path, handler, method = 'all') => { let route = routes[path] if (!route) { routes[path] = route = { handlers: {} } _router.insert(path, route) } - route.handlers[method] = handle + route.handlers[method] = toEventHandler(handler) return router } for (const method of RouterMethods) { @@ -41,20 +40,20 @@ export function createRouter (): Router { } // Main handle - router.handle = (req, res) => { + router.handler = defineEventHandler((event) => { // Match route - const matched = _router.lookup(req.url || '/') + const matched = _router.lookup(event.req.url || '/') if (!matched) { throw createError({ statusCode: 404, name: 'Not Found', - statusMessage: `Cannot find any route matching ${req.url || '/'}.` + statusMessage: `Cannot find any route matching ${event.req.url || '/'}.` }) } // Match method - const method = (req.method || 'get').toLowerCase() as RouterMethod - const handler: HandleWithParams | undefined = matched.handlers[method] || matched.handlers.all + const method = (event.req.method || 'get').toLowerCase() as RouterMethod + const handler = matched.handlers[method] || matched.handlers.all if (!handler) { throw createError({ statusCode: 405, @@ -64,13 +63,11 @@ export function createRouter (): Router { } // Add params - // @ts-ignore - req.params = matched.params || {} + event.event.params = matched.params || {} // Call handler - // @ts-ignore - return handler(req, res) - } + return handler(event) + }) return router } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..37b6aa16 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,28 @@ +import type http from 'http' +import type { H3Event } from './event' + +export interface IncomingMessage extends http.IncomingMessage { + originalUrl?: string // Connect and Express + event: H3Event, + req: H3Event['req'], + res: H3Event['res'] +} +export interface ServerResponse extends http.ServerResponse{ + event: H3Event, + res: H3Event['res'] + req: http.ServerResponse['req'] & { + event: H3Event + originalUrl?: string // Connect and Express + } +} + +export type Handler = (req: IncomingMessage & ReqT, res: ServerResponse) => T +export type PromisifiedHandler = Handler> +export type Middleware = (req: IncomingMessage, res: ServerResponse, next: (err?: Error) => any) => any +export type LazyHandler = () => Handler | Promise + +// Node.js +export type Encoding = false | 'ascii' | 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'base64' | 'latin1' | 'binary' | 'hex' + +// https://www.rfc-editor.org/rfc/rfc7231#section-4.1 +export type HTTPMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' diff --git a/src/types/cookie.ts b/src/types/cookie.ts deleted file mode 100644 index 1ce5c3ce..00000000 --- a/src/types/cookie.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* Redistributed from @types/cookie as we bundle deps */ - -/** - * Additional serialization options - */ -export interface CookieSerializeOptions { - /** - * Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.3|Domain Set-Cookie attribute}. By default, no - * domain is set, and most clients will consider the cookie to apply to only - * the current domain. - */ - domain?: string; - - /** - * Specifies a function that will be used to encode a cookie's value. Since - * value of a cookie has a limited character set (and must be a simple - * string), this function can be used to encode a value into a string suited - * for a cookie's value. - * - * The default function is the global `encodeURIComponent`, which will - * encode a JavaScript string into UTF-8 byte sequences and then URL-encode - * any that fall outside of the cookie range. - */ - encode?(value: string): string; - - /** - * Specifies the `Date` object to be the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.1|`Expires` `Set-Cookie` attribute}. By default, - * no expiration is set, and most clients will consider this a "non-persistent cookie" and will delete - * it on a condition like exiting a web browser application. - * - * *Note* the {@link https://tools.ietf.org/html/rfc6265#section-5.3|cookie storage model specification} - * states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is - * possible not all clients by obey this, so if both are set, they should - * point to the same date and time. - */ - expires?: Date; - /** - * Specifies the boolean value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.6|`HttpOnly` `Set-Cookie` attribute}. - * When truthy, the `HttpOnly` attribute is set, otherwise it is not. By - * default, the `HttpOnly` attribute is not set. - * - * *Note* be careful when setting this to true, as compliant clients will - * not allow client-side JavaScript to see the cookie in `document.cookie`. - */ - httpOnly?: boolean; - /** - * Specifies the number (in seconds) to be the value for the `Max-Age` - * `Set-Cookie` attribute. The given number will be converted to an integer - * by rounding down. By default, no maximum age is set. - * - * *Note* the {@link https://tools.ietf.org/html/rfc6265#section-5.3|cookie storage model specification} - * states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is - * possible not all clients by obey this, so if both are set, they should - * point to the same date and time. - */ - maxAge?: number; - /** - * Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.4|`Path` `Set-Cookie` attribute}. - * By default, the path is considered the "default path". - */ - path?: string; - /** - * Specifies the boolean or string to be the value for the {@link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7|`SameSite` `Set-Cookie` attribute}. - * - * - `true` will set the `SameSite` attribute to `Strict` for strict same - * site enforcement. - * - `false` will not set the `SameSite` attribute. - * - `'lax'` will set the `SameSite` attribute to Lax for lax same site - * enforcement. - * - `'strict'` will set the `SameSite` attribute to Strict for strict same - * site enforcement. - * - `'none'` will set the SameSite attribute to None for an explicit - * cross-site cookie. - * - * More information about the different enforcement levels can be found in {@link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7|the specification}. - * - * *note* This is an attribute that has not yet been fully standardized, and may change in the future. This also means many clients may ignore this attribute until they understand it. - */ - sameSite?: true | false | 'lax' | 'strict' | 'none'; - /** - * Specifies the boolean value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.5|`Secure` `Set-Cookie` attribute}. When truthy, the - * `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set. - * - * *Note* be careful when setting this to `true`, as compliant clients will - * not send the cookie back to the server in the future if the browser does - * not have an HTTPS connection. - */ - secure?: boolean; -} diff --git a/src/types/http.ts b/src/types/http.ts deleted file mode 100644 index 8fa06a98..00000000 --- a/src/types/http.ts +++ /dev/null @@ -1,2 +0,0 @@ -// https://www.rfc-editor.org/rfc/rfc7231#section-4.1 -export type HTTPMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' diff --git a/src/types/node.ts b/src/types/node.ts deleted file mode 100644 index ba6e5399..00000000 --- a/src/types/node.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { IncomingMessage, ServerResponse } from 'http' - -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..658e53aa 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -1,7 +1,6 @@ -import type { IncomingMessage } from 'http' import destr from 'destr' -import type { Encoding } from '../types/node' -import type { HTTPMethod } from '../types/http' +import type { Encoding, HTTPMethod } from '../types' +import type { CompatibilityEvent } from '../event' import { assertMethod } from './request' const RawBodySymbol = Symbol('h3RawBody') @@ -9,49 +8,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 {CompatibilityEvent} 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: CompatibilityEvent, 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 {CompatibilityEvent} 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 +51,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: CompatibilityEvent): 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..f22c6459 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 { CookieSerializeOptions } from 'cookie-es' +import type { CompatibilityEvent } 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 {CompatibilityEvent} 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: CompatibilityEvent): 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 {CompatibilityEvent} 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: CompatibilityEvent, 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 {CompatibilityEvent} 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: CompatibilityEvent, 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 {CompatibilityEvent} 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: CompatibilityEvent, name: string, serializeOptions?: CookieSerializeOptions) { + setCookie(event, name, '', { ...serializeOptions, maxAge: 0 }) diff --git a/src/utils/request.ts b/src/utils/request.ts index dbd4a4fd..60b6983f 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 { HTTPMethod } from '../types' +import type { CompatibilityEvent } from '../event' -export function useQuery (req: IncomingMessage) { - return getQuery(req.url || '') +export function useQuery (event: CompatibilityEvent) { + 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: CompatibilityEvent, 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: CompatibilityEvent, 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: CompatibilityEvent, 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..2dc3784b 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 { CompatibilityEvent } 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: CompatibilityEvent, 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: CompatibilityEvent, 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: CompatibilityEvent, 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: CompatibilityEvent, 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: CompatibilityEvent, 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/app.test.ts b/test/app.test.ts index 14f9d5e3..4ba4b778 100644 --- a/test/app.test.ts +++ b/test/app.test.ts @@ -144,7 +144,7 @@ describe('app', () => { }) it('can take an object', async () => { - app.use({ route: '/', handle: () => 'valid' }) + app.use({ route: '/', handler: () => 'valid' }) const response = await request.get('/') expect(response.text).toEqual('valid') 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()