// Loaded from https://deno.land/x/tinyhttp@0.1.18/app.ts // deno-lint-ignore-file import { Router, serve, Server, rg, pushMiddleware } from './deps.ts' import { NextFunction, RHandler as Handler, Middleware, UseMethodParams } from './types.ts' import { onErrorHandler, ErrorHandler } from './onError.ts' import { setImmediate } from 'https://deno.land/std@0.101.0/node/timers.ts' import type { Request } from './request.ts' import type { Response } from './response.ts' import { getURLParams, getPathname } from './utils/parseUrl.ts' import { extendMiddleware } from './extend.ts' import * as path from 'https://deno.land/std@0.101.0/path/mod.ts' const lead = (x: string) => (x.charCodeAt(0) === 47 ? x : '/' + x) const mount = (fn: any) => (fn instanceof App ? fn.attach : fn) declare global { namespace tinyhttp { // These open interfaces may be extended in an application-specific manner via declaration merging. interface Request {} interface Response {} interface Application {} } } export const renderTemplate = (res: Res, app: App) => (file: string, data?: Record, options?: TemplateEngineOptions): Response => { app.render( file, data, (err: unknown, html: unknown) => { if (err) throw err res.send(html) }, options ) return res } /** * Execute handler with passed `req` and `res`. Catches errors and resolves async handlers. * @param h */ export const applyHandler = (h: Handler) => async (req: Req, res: Res, next: NextFunction) => { try { if (h.constructor.name === 'AsyncFunction') { await h(req, res, next) } else h(req, res, next) } catch (e) { next(e) } } /** * tinyhttp App has a few settings for toggling features */ export type AppSettings = Partial< Record<'networkExtensions' | 'bindAppToReqRes' | 'enableReqRoute', boolean> & Record<'subdomainOffset', number> & Record<'xPoweredBy', string | boolean> > /** * Function that processes the template */ export type TemplateFunc = ( path: string, locals: Record, opts: TemplateEngineOptions, cb: (err: Error | null, html: unknown) => void ) => void export type TemplateEngineOptions = Partial<{ cache: boolean ext: string renderOptions: Partial viewsFolder: string _locals: Record }> export const getRouteFromApp = ({ middleware }: App, h: Handler) => middleware.find(({ handler }) => typeof handler === 'function' && handler.name === h.name) export type AppConstructor = Partial<{ noMatchHandler: Handler onError: ErrorHandler settings: AppSettings applyExtensions: (req: Req, res: Res, next: NextFunction) => void }> /** * `App` class - the starting point of tinyhttp app. * * With the `App` you can: * * use routing methods and `.use(...)` * * set no match (404) and error (500) handlers * * configure template engines * * store data in locals * * listen the http server on a specified port * * In case you use TypeScript, you can pass custom Request and Response interfaces as generic parameters. * * Example: * * ```ts * interface CoolReq extends Request { * genericsAreDope: boolean * } * * const app = App() * ``` */ export class App< RenderOptions = any, Req extends Request = Request, Res extends Response = Response > extends Router implements tinyhttp.Application { middleware: Middleware[] = [] locals: Record = {} noMatchHandler: Handler onError: ErrorHandler settings: AppSettings & Record engines: Record> = {} applyExtensions?: (req: Req, res: Res, next: NextFunction) => void attach: (req: Req) => void mountpath = '/' apps: Record = {} constructor(options: AppConstructor = {}) { super() this.onError = options?.onError || onErrorHandler this.noMatchHandler = options?.noMatchHandler || this.onError.bind(null, { code: 404 }) this.settings = options.settings || { xPoweredBy: true } this.applyExtensions = options?.applyExtensions this.attach = (req) => setImmediate(this.handler.bind(this, req, undefined), req) // this.#eventHandler = options.eventHandler } set(setting: string, value: any) { this.settings[setting] = value return this } enable(setting: string) { this.settings[setting] = true return this } disable(setting: string) { this.settings[setting] = false return this } /** * Register a template engine with extension */ engine(ext: string, fn: TemplateFunc) { this.engines[ext] = fn return this } /** * Render a template * @param file What to render * @param data data that is passed to a template * @param options Template engine options * @param cb Callback that consumes error and html */ render( file: string, data: Record = {}, cb: (err: unknown, html: unknown) => void, options: TemplateEngineOptions = {} ) { options.viewsFolder = options.viewsFolder || `${Deno.cwd()}/views` options.ext = options.ext || file.slice(file.lastIndexOf('.') + 1) || 'ejs' options._locals = options._locals || {} let locals = { ...data, ...this.locals } if (options._locals) locals = { ...locals, ...options._locals } if (!file.endsWith(`.${options.ext}`)) file = `${file}.${options.ext}` const dest = options.viewsFolder ? path.join(options.viewsFolder, file) : file this.engines[options.ext](dest, locals, options.renderOptions || {}, cb) return this } route(path: string): App { const app = new App() this.use(path, app) return app } use(...args: UseMethodParams) { const base = args[0] const fns = args.slice(1).flat() if (base instanceof App) { // Set App parent to current App // @ts-ignore base.parent = this // Mount on root base.mountpath = '/' this.apps['/'] = base } const path = typeof base === 'string' ? base : '/' let regex: any for (const fn of fns) { if (fn instanceof App) { regex = rg(path, true) fn.mountpath = path this.apps[path] = fn // @ts-ignore fn.parent = this } } if (base === '/') { for (const fn of fns) super.use(base, mount(fn as Handler)) } else if (typeof base === 'function' || base instanceof App) { super.use('/', [base, ...fns].map(mount)) } else if (Array.isArray(base)) { super.use('/', [...base, ...fns].map(mount)) } else { const handlerPaths = [] const handlerFunctions = [] for (const fn of fns) { if (fn instanceof App && fn.middleware?.length) { for (const mw of fn.middleware) { handlerPaths.push(lead(base as string) + lead(mw.path!)) handlerFunctions.push(fn) } } else { handlerPaths.push('') handlerFunctions.push(fn) } } pushMiddleware(this.middleware)({ path: base as string, regex, type: 'mw', handler: mount(handlerFunctions[0] as Handler), handlers: handlerFunctions.slice(1).map(mount), fullPaths: handlerPaths }) } return this // chainable } find(url: string) { return this.middleware.filter((m) => { m.regex = m.regex || (rg(m.path, m.type === 'mw') as { keys: string[]; pattern: RegExp }) let fullPathRegex: { keys: string[] | boolean; pattern: RegExp } | null m.fullPath && typeof m.fullPath === 'string' ? (fullPathRegex = rg(m.fullPath, m.type === 'mw')) : (fullPathRegex = null) return ( m.regex.pattern.test(url) && (m.type === 'mw' && fullPathRegex?.keys ? fullPathRegex.pattern.test(url) : true) ) }) } /** * Extends Req / Res objects, pushes 404 and 500 handlers, dispatches middleware * @param req Req object */ handler(req: Req, next?: NextFunction) { let res = { headers: new Headers({}) } /* Set X-Powered-By header */ const { xPoweredBy } = this.settings if (xPoweredBy) res.headers.set('X-Powered-By', typeof xPoweredBy === 'string' ? xPoweredBy : 'tinyhttp') const exts = this.applyExtensions || extendMiddleware(this as any) req.originalUrl = req.url || req.originalUrl const pathname = getPathname(req.originalUrl) const matched = this.find(pathname) const mw: Middleware[] = [ { handler: exts, type: 'mw', path: '/' }, ...matched.filter((x) => req.method === 'HEAD' || (x.method ? x.method === req.method : true)) ] if (matched[0] != null) { mw.push({ type: 'mw', handler: (req, res, next) => { if (req.method === 'HEAD') { res.statusCode = 204 return res.end('') } next() }, path: '/' }) } mw.push({ handler: this.noMatchHandler, type: 'mw', path: '/' }) const handle = (mw: Middleware) => async (req: Req, res: Res, next: NextFunction) => { const { path = '/', handler, type, regex } = mw const params = regex ? getURLParams(regex, pathname) : {} if (type === 'route') req.params = params if (path.includes(':')) { const first = Object.values(params)[0] const url = req.url.slice(req.url.indexOf(first) + first?.length) req.url = lead(url) } else { req.url = lead(req.url.substring(path.length)) } if (!req.path) req.path = getPathname(req.url) if (this.settings?.enableReqRoute) req.route = getRouteFromApp(this as any, handler) if (type === 'route') req.params = getURLParams(regex!, pathname) await applyHandler(handler as unknown as Handler)(req, res, next) } let idx = 0 next = next || ((err: any) => (err ? this.onError(err, req) : loop())) const loop = () => idx < mw.length && handle(mw[idx++])(req, res as unknown as Res, next as NextFunction) loop() return res as Res } /** * Creates HTTP server and dispatches middleware * @param port server listening port * @param Server callback after server starts listening * @param host server listening host */ async listen(port: number, cb?: () => void, hostname = '0.0.0.0'): Promise { const server = serve({ port, hostname }) cb?.() for await (const req of server) { this.attach(req as any) } return server } }