Skip to main content
Module

x/opine/src/application.ts

Fast, minimalist web framework for Deno ported from ExpressJS.
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
// deno-lint-ignore-fileimport { fromFileUrl, resolve, Server } from "../deps.ts";import { methods } from "./methods.ts";import { Router } from "./router/index.ts";import { init } from "./middleware/init.ts";import { query } from "./middleware/query.ts";import { finalHandler } from "./utils/finalHandler.ts";import { compileETag } from "./utils/compileETag.ts";import { compileQueryParser } from "./utils/compileQueryParser.ts";import { compileTrust } from "./utils/compileTrust.ts";import { merge } from "./utils/merge.ts";import { View } from "./view.ts";import { WrappedRequest } from "./request.ts";import type { Application, HTTPOptions, HTTPSOptions, IRoute, NextFunction, Opine, OpineRequest, OpineResponse, PathParams,} from "../src/types.ts";
const create = Object.create;const setPrototypeOf = Object.setPrototypeOf;const slice = Array.prototype.slice;
/** * Variable for trust proxy inheritance * @private */const trustProxyDefaultSymbol = "@@symbol:trust_proxy_default";
/** * Application prototype. * * @public */export const app: Application = {} as Application;
/** * Initialize the server. * * - setup default configuration * - setup default middleware * - setup route reflection methods * * @private */app.init = function init(): void { this.cache = {}; this.engines = {}; this.settings = {};
this.defaultConfiguration();};
/** * Initialize application configuration. * @private */app.defaultConfiguration = function defaultConfiguration(): void { this.enable("x-powered-by"); this.set("etag", "weak"); this.set("query parser", "extended"); this.set("subdomain offset", 2); this.set("trust proxy", false);
// trust proxy inherit Object.defineProperty(this.settings, trustProxyDefaultSymbol, { configurable: true, value: true, });
const self: Opine = this as Opine; this.on("mount", function onmount(parent: Opine) { // inherit trust proxy if ( self.settings[trustProxyDefaultSymbol] === true && typeof parent.settings["trust proxy fn"] === "function" ) { delete self.settings["trust proxy"]; delete self.settings["trust proxy fn"]; }
// inherit prototypes setPrototypeOf(self.request, parent.request); setPrototypeOf(self.response, parent.response); setPrototypeOf(self.engines, parent.engines); setPrototypeOf(self.settings, parent.settings); });
// setup locals this.locals = create(null);
// top-most app is mounted at / this.mountpath = "/";
// default locals this.locals.settings = this.settings;
// default configuration this.set("view", View); this.set("views", resolve("views")); this.set("jsonp callback name", "callback"); this.enable("view cache");};
/** * Lazily adds the base router if it has not yet been added. * * We cannot add the base router in the defaultConfiguration because * it reads app settings which might be set after that has run. * * @private */app.lazyrouter = function lazyrouter(): void { if (!this._router) { this._router = new Router({ caseSensitive: this.enabled("case sensitive routing"), strict: this.enabled("strict routing"), });
this._router.use(query(this.get("query parser fn"))); this._router.use(init(this as Opine)); }};
/** * Dispatch a req, res pair into the application. Starts pipeline processing. * * If no callback is provided, then default error handlers will respond * in the event of an error bubbling through the stack. * * @private */app.handle = function handle( req: OpineRequest, res: OpineResponse, next: NextFunction,): void { const router = this._router;
next = next || finalHandler(req, res);
if (!router) { return next(); }
router.handle(req, res, next);};
const isPath = (thing: unknown): thing is string | RegExp => typeof thing === "string" || thing instanceof RegExp;
/** * Proxy `Router#use()` to add middleware to the app router. * See Router#use() documentation for details. * * If the _fn_ parameter is an opine app, then it will be * mounted at the _route_ specified. * * @returns {Application} for chaining * @public */app.use = function use(...args: any[]): Application { const firstArg = args[0]; const [path, ...nonPathArgs] = (Array.isArray(firstArg) ? isPath(firstArg[0]) : isPath(firstArg)) ? args : ["/", ...args]; const fns = nonPathArgs.flat(Infinity);
if (fns.length === 0) { throw new TypeError("app.use() requires a middleware function"); }
// setup router this.lazyrouter(); const router = this._router;
fns.forEach(function (this: Application, fn: any) { // non-opine app if (!fn || !fn.handle || !fn.set) { return router.use(path, fn); }
fn.mountpath = path; fn.parent = this;
router.use( path, function mounted_app( req: OpineRequest, res: OpineResponse, next: NextFunction, ): void { const orig = req.app as Opine;
fn.handle(req, res, (err?: Error) => { setPrototypeOf(req, orig.request); setPrototypeOf(res, orig.response); next(err); }); }, );
// mounted an app fn.emit("mount", this); }, this);
return this;};
/** * Proxy to the app `Router#route()` * Returns a new `Route` instance for the _path_. * * Routes are isolated middleware stacks for specific paths. * See the Route api docs for details. * * @param {PathParams} prefix * @returns {Route} * @public */app.route = function route(prefix: PathParams): IRoute { this.lazyrouter(); return this._router.route(prefix);};
/** * Register the given template engine callback `fn` for the * provided extension `ext`. * * @param {string} ext * @param {Function} fn * @return {Application} for chaining * @public */app.engine = function engine(ext: string, fn: Function) { const extension = ext[0] !== "." ? `.${ext}` : ext; this.engines[extension] = fn;
return this;};
/** * Proxy to `Router#param()` with one added api feature. The _name_ parameter * can be an array of names. * * See the Router#param() docs for more details. * * @param {String|Array} name * @param {Function} fn * @return {app} for chaining * @public */
app.param = function param(name, fn) { this.lazyrouter(); if (Array.isArray(name)) { for (var i = 0; i < name.length; i++) { this.param(name[i], fn); } return this; } this._router.param(name, fn); return this;};
/** * Assign `setting` to `val`, or return `setting`'s value. * * app.set('foo', 'bar'); * app.set('foo'); * // => "bar" * * Mounted servers inherit their parent server's settings. * * @param {string} setting * @param {any} value * @return {Application} for chaining * @public */app.set = function set(setting: string, value?: any): Application { if (arguments.length === 1) { // app.get(setting) return this.settings[setting]; }
this.settings[setting] = value;
// trigger matched settings switch (setting) { case "etag": this.set("etag fn", compileETag(value)); break; case "query parser": this.set("query parser fn", compileQueryParser(value)); break; case "trust proxy": this.set("trust proxy fn", compileTrust(value));
// trust proxy inherit Object.defineProperty(this.settings, trustProxyDefaultSymbol, { configurable: true, value: false, });
break; }
return this;};
/** * Return the app's absolute pathname * based on the parent(s) that have * mounted it. * * For example if the application was * mounted as "/admin", which itself * was mounted as "/blog" then the * return value would be "/blog/admin". * * @return {string} * @private */app.path = function path(): string { return this.parent ? this.parent.path() + this.mountpath : "";};
/** * Check if `setting` is enabled (truthy). * * app.enabled('foo') * // => false * * app.enable('foo') * app.enabled('foo') * // => true * * @param {string} setting * @return {boolean} * @public */app.enabled = function enabled(setting: string): boolean { return Boolean(this.set(setting));};
/** * Check if `setting` is disabled. * * app.disabled('foo') * // => true * * app.enable('foo') * app.disabled('foo') * // => false * * @param {string} setting * @return {boolean} * @public */app.disabled = function disabled(setting: string): boolean { return !this.set(setting);};
/** * Enable `setting`. * * @param {string} setting * @return {Application} for chaining * @public */app.enable = function enable(setting: string): Application { return this.set(setting, true);};
/** * Disable `setting`. * * @param {string} setting * @return {Application} for chaining * @public */app.disable = function disable(setting: string): Application { return this.set(setting, false);};
/** * Delegate `.VERB(...)` calls to `router.VERB(...)`. */methods.forEach((method: string): void => { (app as any)[method] = function (path: string): Application { if (method === "get" && arguments.length === 1) { // app.get(setting) return this.set(path); }
this.lazyrouter();
const route = this._router.route(path); route[method].apply(route, slice.call(arguments, 1));
return this; };});
/** * Special-cased "all" method, applying the given route `path`, * middleware, and callback to _every_ HTTP method. * * @return {Application} for chaining * @public */app.all = function all(path: PathParams): Application { this.lazyrouter();
const route = this._router.route(path); const args = slice.call(arguments, 1);
for (let i = 0; i < methods.length; i++) { (route as any)[methods[i]].apply(route, args); }
return this;};
/** * Try rendering a view. * @private */async function tryRender(view: any, options: any, callback: Function) { try { await view.render(options, callback); } catch (err) { callback(err); }}
/** * Render the given view `name` name with `options` * and a callback accepting an error and the * rendered template string. * * Example: * * app.render('email', { name: 'Deno' }, function(err, html) { * // ... * }) * * @param {string} name * @param {Object|Function} options or callback * @param {Function} callback * @public */app.render = function render( name: string, options: any, callback: Function = () => {},) { const cache = this.cache; const engines = this.engines; const renderOptions: any = {}; let done = callback; let view;
name = name.startsWith("file:") ? fromFileUrl(name) : name;
// support callback function as second arg if (typeof options === "function") { done = options; options = {}; }
// merge app.locals merge(renderOptions, this.locals);
// merge options._locals if (options._locals) { merge(renderOptions, options._locals); }
// merge options merge(renderOptions, options);
// set .cache unless explicitly provided if (renderOptions.cache == null) { renderOptions.cache = this.enabled("view cache"); }
// primed cache if (renderOptions.cache) { view = cache[name]; }
// view if (!view) { const View = this.get("view");
view = new View(name, { defaultEngine: this.get("view engine"), engines, root: this.get("views"), });
if (!view.path) { const dirs = Array.isArray(view.root) && view.root.length > 1 ? `directories "${view.root.slice(0, -1).join('", "')}" or "${ view.root[view.root.length - 1] }"` : `directory "${view.root}"`;
const err = new Error( `Failed to lookup view "${name}" in views ${dirs}`, );
(err as any).view = view;
return done(err); }
// prime the cache if (renderOptions.cache) { cache[name] = view; } }
// render tryRender(view, renderOptions, done);};
const isTlsOptions = ( options?: number | string | HTTPOptions | HTTPSOptions,): options is HTTPSOptions => options !== null && typeof options === "object" && "certFile" in options && "keyFile" in options;
/** * Listen for connections. * * @param {number|string|HTTPOptions|HTTPSOptions} options * @returns {Server} Configured Deno server * @public */app.listen = function listen( options?: number | string | HTTPOptions | HTTPSOptions, callback?: () => void,): Server { let port = 0; let hostname = "";
if (typeof options === "number") { port = options; } else if (typeof options === "string") { const addr = options.split(":"); hostname = addr[0]; port = parseInt(addr[1]); } else { hostname = options?.hostname ?? ""; port = options?.port ?? 0; }
const isTls = isTlsOptions(options);
const server = new Server({ port, hostname, handler: async (request, connInfo) => { const opineRequest = new WrappedRequest(request, connInfo); this(opineRequest);
return await opineRequest.finalResponse; }, });
const start = async () => { while (!server.closed) { try { if (isTls) { await server.listenAndServeTls(options.certFile, options.keyFile); } else { await server.listenAndServe(); } } catch (e) { // Ignore closed connections / servers if ( !(e instanceof Deno.errors.BadResource || e instanceof Deno.errors.BrokenPipe) ) { this.emit("error", e); } } } };
start();
if (callback && typeof callback === "function") { callback(); }
return server;};