323 lines
12 KiB
JavaScript
323 lines
12 KiB
JavaScript
|
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
||
|
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
||
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
||
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
||
|
};
|
||
|
var _App_instances, _App_find;
|
||
|
import { createServer } from 'node:http';
|
||
|
import { getPathname } from '@tinyhttp/req';
|
||
|
import { Router, pushMiddleware } from '@tinyhttp/router';
|
||
|
import { parse as rg } from 'regexparam';
|
||
|
import { extendMiddleware } from './extend.js';
|
||
|
import { onErrorHandler } from './onError.js';
|
||
|
import { getURLParams } from './request.js';
|
||
|
import { View } from './view.js';
|
||
|
/**
|
||
|
* Add leading slash if not present (e.g. path -> /path, /path -> /path)
|
||
|
* @param x
|
||
|
*/
|
||
|
const lead = (x) => (x.charCodeAt(0) === 47 ? x : `/${x}`);
|
||
|
const trail = (x) => (x.charCodeAt(x.length - 1) === 47 ? x.substring(0, x.length - 1) : x);
|
||
|
const mount = (fn) => (fn instanceof App ? fn.attach : fn);
|
||
|
const applyHandler = (h) => async (req, res, next) => {
|
||
|
try {
|
||
|
if (h[Symbol.toStringTag] === 'AsyncFunction') {
|
||
|
await h(req, res, next);
|
||
|
}
|
||
|
else
|
||
|
h(req, res, next);
|
||
|
}
|
||
|
catch (e) {
|
||
|
next === null || next === void 0 ? void 0 : next(e);
|
||
|
}
|
||
|
};
|
||
|
/**
|
||
|
* `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 types to this class because it is also a generic class.
|
||
|
*
|
||
|
* Example:
|
||
|
*
|
||
|
* ```ts
|
||
|
* interface CoolReq extends Request {
|
||
|
* genericsAreDope: boolean
|
||
|
* }
|
||
|
*
|
||
|
* const app = App<any, CoolReq, Response>()
|
||
|
* ```
|
||
|
*/
|
||
|
export class App extends Router {
|
||
|
constructor(options = {}) {
|
||
|
super();
|
||
|
_App_instances.add(this);
|
||
|
this.middleware = [];
|
||
|
this.locals = {};
|
||
|
this.engines = {};
|
||
|
this.onError = (options === null || options === void 0 ? void 0 : options.onError) || onErrorHandler;
|
||
|
// @ts-expect-error typescript is not smart enough to understand "this" ts(2345)
|
||
|
this.noMatchHandler = (options === null || options === void 0 ? void 0 : options.noMatchHandler) || this.onError.bind(this, { code: 404 });
|
||
|
this.settings = {
|
||
|
view: View,
|
||
|
xPoweredBy: true,
|
||
|
views: `${process.cwd()}/views`,
|
||
|
'view cache': process.env.NODE_ENV === 'production',
|
||
|
'trust proxy': 0,
|
||
|
...options.settings
|
||
|
};
|
||
|
if (options.applyExtensions)
|
||
|
this.applyExtensions = options === null || options === void 0 ? void 0 : options.applyExtensions;
|
||
|
const boundHandler = this.handler.bind(this);
|
||
|
this.attach = (req, res, next) => setImmediate(boundHandler, req, res, next);
|
||
|
this.cache = {};
|
||
|
}
|
||
|
set(setting, value) {
|
||
|
this.settings[setting] = value;
|
||
|
return this;
|
||
|
}
|
||
|
enable(setting) {
|
||
|
this.settings[setting] = true;
|
||
|
return this;
|
||
|
}
|
||
|
enabled(setting) {
|
||
|
return Boolean(this.settings[setting]);
|
||
|
}
|
||
|
disable(setting) {
|
||
|
this.settings[setting] = false;
|
||
|
return this;
|
||
|
}
|
||
|
path() {
|
||
|
return this.parent ? this.parent.path() + this.mountpath : '';
|
||
|
}
|
||
|
engine(ext, fn) {
|
||
|
this.engines[ext[0] === '.' ? ext : `.${ext}`] = fn;
|
||
|
return this;
|
||
|
}
|
||
|
render(name, data = {}, options = {}, cb = () => { }) {
|
||
|
let view;
|
||
|
const { _locals, ...opts } = options;
|
||
|
let locals = this.locals;
|
||
|
if (_locals)
|
||
|
locals = { ...locals, ..._locals };
|
||
|
locals = { ...locals, ...data };
|
||
|
if (opts.cache == null)
|
||
|
opts.cache = this.enabled('view cache');
|
||
|
if (opts.cache) {
|
||
|
view = this.cache[name];
|
||
|
}
|
||
|
if (!view) {
|
||
|
const ViewClass = this.settings.view || View;
|
||
|
try {
|
||
|
view = new ViewClass(name, {
|
||
|
defaultEngine: this.settings['view engine'],
|
||
|
root: this.settings.views,
|
||
|
engines: this.engines
|
||
|
});
|
||
|
}
|
||
|
catch (err) {
|
||
|
return cb(err);
|
||
|
}
|
||
|
if (opts.cache) {
|
||
|
this.cache[name] = view;
|
||
|
}
|
||
|
}
|
||
|
try {
|
||
|
view.render(opts, locals, cb);
|
||
|
}
|
||
|
catch (err) {
|
||
|
cb(err);
|
||
|
}
|
||
|
}
|
||
|
use(...args) {
|
||
|
var _a;
|
||
|
const base = args[0];
|
||
|
const fns = args.slice(1).flat();
|
||
|
let pathArray = [];
|
||
|
if (typeof base === 'function' || base instanceof App) {
|
||
|
fns.unshift(base);
|
||
|
}
|
||
|
else {
|
||
|
// if base is not an array of paths, then convert it to an array.
|
||
|
let basePaths = [];
|
||
|
if (Array.isArray(base))
|
||
|
basePaths = base;
|
||
|
else if (typeof base === 'string')
|
||
|
basePaths = [base];
|
||
|
basePaths = basePaths.filter((element) => {
|
||
|
if (typeof element === 'string') {
|
||
|
pathArray.push(element);
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
});
|
||
|
fns.unshift(...basePaths);
|
||
|
}
|
||
|
pathArray = pathArray.length ? pathArray.map((path) => lead(path)) : ['/'];
|
||
|
const mountpath = pathArray.join(', ');
|
||
|
let regex;
|
||
|
for (const fn of fns) {
|
||
|
if (fn instanceof App) {
|
||
|
for (const path of pathArray) {
|
||
|
regex = rg(path, true);
|
||
|
fn.mountpath = mountpath;
|
||
|
this.apps[path] = fn;
|
||
|
// @ts-expect-error typescript is not smart enough to understand "this" ts(2345)
|
||
|
fn.parent = this;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
for (const path of pathArray) {
|
||
|
const handlerPaths = [];
|
||
|
const handlerFunctions = [];
|
||
|
const handlerPathBase = path === '/' ? '' : lead(path);
|
||
|
for (const fn of fns) {
|
||
|
if (fn instanceof App && ((_a = fn.middleware) === null || _a === void 0 ? void 0 : _a.length)) {
|
||
|
for (const mw of fn.middleware) {
|
||
|
handlerPaths.push(handlerPathBase + lead(mw.path));
|
||
|
handlerFunctions.push(fn);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
handlerPaths.push('');
|
||
|
handlerFunctions.push(fn);
|
||
|
}
|
||
|
}
|
||
|
pushMiddleware(this.middleware)({
|
||
|
path,
|
||
|
regex,
|
||
|
type: 'mw',
|
||
|
handler: mount(handlerFunctions[0]),
|
||
|
handlers: handlerFunctions.slice(1).map(mount),
|
||
|
fullPaths: handlerPaths
|
||
|
});
|
||
|
}
|
||
|
return this;
|
||
|
}
|
||
|
route(path) {
|
||
|
const app = new App({ settings: this.settings });
|
||
|
this.use(path, app);
|
||
|
return app;
|
||
|
}
|
||
|
handler(req, res, next) {
|
||
|
/* Set X-Powered-By header */
|
||
|
const { xPoweredBy } = this.settings;
|
||
|
if (xPoweredBy)
|
||
|
res.setHeader('X-Powered-By', typeof xPoweredBy === 'string' ? xPoweredBy : 'tinyhttp');
|
||
|
// @ts-expect-error typescript is not smart enough to understand "this" ts(2345)
|
||
|
const exts = this.applyExtensions || extendMiddleware(this);
|
||
|
let mw = [
|
||
|
{
|
||
|
handler: exts,
|
||
|
type: 'mw',
|
||
|
path: '/'
|
||
|
}
|
||
|
];
|
||
|
req.baseUrl = '';
|
||
|
const handle = (mw, pathname) => async (req, res, next) => {
|
||
|
var _a;
|
||
|
const { path, handler, regex } = mw;
|
||
|
let params;
|
||
|
try {
|
||
|
params = regex ? getURLParams(regex, pathname) : {};
|
||
|
}
|
||
|
catch (e) {
|
||
|
console.error(e);
|
||
|
if (e instanceof URIError)
|
||
|
return res.sendStatus(400);
|
||
|
throw e;
|
||
|
}
|
||
|
let prefix = path;
|
||
|
if (regex) {
|
||
|
for (const key of regex.keys) {
|
||
|
if (key === 'wild') {
|
||
|
prefix = prefix.replace('*', params.wild);
|
||
|
}
|
||
|
else {
|
||
|
prefix = prefix.replace(`:${key}`, params[key]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
req.params = { ...req.params, ...params };
|
||
|
if (mw.type === 'mw') {
|
||
|
req.url = lead(req.originalUrl.substring(prefix.length));
|
||
|
req.baseUrl = trail(req.originalUrl.substring(0, prefix.length));
|
||
|
}
|
||
|
if (!req.path)
|
||
|
req.path = pathname;
|
||
|
if ((_a = this.settings) === null || _a === void 0 ? void 0 : _a.enableReqRoute)
|
||
|
req.route = mw;
|
||
|
await applyHandler(handler)(req, res, next);
|
||
|
};
|
||
|
let idx = 0;
|
||
|
const loop = () => {
|
||
|
req.originalUrl = req.baseUrl + req.url;
|
||
|
const pathname = getPathname(req.url);
|
||
|
const matched = __classPrivateFieldGet(this, _App_instances, "m", _App_find).call(this, pathname).filter((x) => (req.method === 'HEAD' || (x.method ? x.method === req.method : true)) && !mw.includes(x));
|
||
|
if (matched.length && matched[0] !== null) {
|
||
|
if (idx !== 0) {
|
||
|
idx = mw.length;
|
||
|
req.params = {};
|
||
|
}
|
||
|
mw = [
|
||
|
...mw,
|
||
|
...matched,
|
||
|
{
|
||
|
type: 'mw',
|
||
|
handler: (req, res, next) => {
|
||
|
if (req.method === 'HEAD') {
|
||
|
res.statusCode = 204;
|
||
|
return res.end('');
|
||
|
}
|
||
|
next === null || next === void 0 ? void 0 : next();
|
||
|
},
|
||
|
path: '/'
|
||
|
}
|
||
|
];
|
||
|
}
|
||
|
else if (this.parent == null) {
|
||
|
mw.push({
|
||
|
handler: this.noMatchHandler,
|
||
|
type: 'route',
|
||
|
path: '/'
|
||
|
});
|
||
|
}
|
||
|
void handle(mw[idx++], pathname)(req, res, next);
|
||
|
};
|
||
|
const parentNext = next;
|
||
|
next = (err) => {
|
||
|
if (err != null) {
|
||
|
// @ts-expect-error The 'this' context of type 'this' is not assignable to method's 'this' of type 'App<Request, Response<unknown>>' ts(2345)
|
||
|
return this.onError(err, req, res);
|
||
|
}
|
||
|
if (res.writableEnded)
|
||
|
return;
|
||
|
if (idx >= mw.length) {
|
||
|
if (parentNext != null)
|
||
|
parentNext();
|
||
|
return;
|
||
|
}
|
||
|
loop();
|
||
|
};
|
||
|
loop();
|
||
|
}
|
||
|
listen(port, cb, host) {
|
||
|
return createServer().on('request', this.attach).listen(port, host, cb);
|
||
|
}
|
||
|
}
|
||
|
_App_instances = new WeakSet(), _App_find = function _App_find(url) {
|
||
|
return this.middleware.filter((m) => {
|
||
|
m.regex = m.regex || rg(m.path, m.type === 'mw');
|
||
|
let fullPathRegex;
|
||
|
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 ? fullPathRegex.pattern.test(url) : true);
|
||
|
});
|
||
|
};
|
||
|
//# sourceMappingURL=app.js.map
|