src/nanoexpress.js
import uWS from 'uWebSockets.js';
import fs from 'fs';
import { resolve } from 'path';
import util from 'util';
import { getMime, sendFile } from './helpers/sifrr-server';
import { http, ws } from './middlewares';
import logger from './helpers/logger';
const readFile = util.promisify(fs.readFile);
let Ajv;
try {
Ajv = require('ajv');
} catch (e) {
logger.error(
'`Ajv` was not found in your dependencies list' +
', please install yourself for this feature working properly'
);
}
const nanoexpress = (options = {}) => {
const time = Date.now(); // For better managing start-time / lags
let app;
let ajv;
if (options.https) {
app = uWS.SSLApp(options.https);
} else {
app = uWS.App();
}
const httpMethods = [
'get',
'post',
'put',
'patch',
'del',
'any',
'head',
'options',
'trace'
];
// App configuration
let middlewares = [];
const pathMiddlewares = {};
const config = {
host: null,
port: null
};
config.https = !!options.https;
config.configureAjv = options.configureAjv;
config.setAjv = () => {
if (typeof Ajv !== 'function') {
logger.error('`Ajv` was not initialized properly');
return;
}
ajv = new Ajv(options.ajv);
if (options.configureAjv) {
ajv = options.configureAjv(ajv);
}
config.ajv = ajv;
};
config.swagger = options.swagger;
const _app = {
config: {
set: (key, value) => {
config[key] = value;
},
get: (key) => config[key]
},
get host() {
return config.host;
},
get port() {
return config.port;
},
get address() {
let address = '';
if (config.host) {
address += config.https ? 'https://' : 'http://';
address += config.host;
address += ':' + config.port;
}
return address;
},
listen: (port, host) =>
new Promise((resolve, reject) => {
if (typeof port === 'string' && port.indexOf('.') !== -1) {
const _port = host;
host = port;
if (_port) {
port = _port;
} else {
port = undefined;
}
}
if (port === undefined) {
logger.error('[Server]: PORT is required');
return undefined;
}
if (middlewares && middlewares.length > 0 && !middlewares.called) {
_app.any('/*', ...middlewares);
middlewares.called = true;
}
for (const path in pathMiddlewares) {
const middleware = pathMiddlewares[path];
if (middleware && middleware.length > 0 && !middleware.called) {
_app.any(path, ...middleware);
middleware.called = true;
}
}
// Set not found handler
if (!_app.anyApplied) {
_app.get(
'/*',
config._notFoundHandler ||
((req, res) => {
res.end(
'{"middleware_type":"sync","error":"The route handler not found"}'
);
})
);
}
port = Number(port);
const onListen = (token) => {
if (typeof host === 'string') {
config.host = host;
} else {
config.host = 'localhost';
}
if (typeof port === 'number') {
config.port = port;
}
if (token) {
_app._instance = token;
logger.log(
`[Server]: started successfully at [${config.host}:${port}] in [${
Date.now() - time
}ms]`
);
resolve(_app);
} else {
logger.error(
`[Server]: failed to host at [${config.host}:${port}]`
);
reject(
new Error(`[Server]: failed to host at [${config.host}:${port}]`)
);
config.host = null;
config.port = null;
}
};
if (host) {
app.listen(host, port, onListen);
} else {
app.listen(port, onListen);
}
}),
close: () => {
if (_app._instance) {
config.host = null;
config.port = null;
uWS.us_listen_socket_close(_app._instance);
_app._instance = null;
logger.log('[Server]: stopped successfully');
return true;
} else {
logger.error('[Server]: Error, failed while stopping');
return false;
}
},
setErrorHandler: (fn) => {
config._errorHandler = fn;
return _app;
},
setNotFoundHandler: (fn) => {
config._notFoundHandler = fn;
return _app;
},
setValidationErrorHandler: (fn) => {
config._validationErrorHandler = fn;
return _app;
},
register: (fn) => {
fn(_app);
return _app;
},
use: (path, ...fns) => {
if (typeof path === 'function') {
fns.unshift(path);
middlewares.push(...fns);
// Avoid duplicates if contains for performance
middlewares = middlewares.filter(
(item, i, self) => self.indexOf(item) === i
);
} else if (typeof path === 'string') {
if (!pathMiddlewares[path]) {
pathMiddlewares[path] = [];
}
// Avoid duplicates if contains for performance
pathMiddlewares[path].push(...fns);
pathMiddlewares[path] = pathMiddlewares[path].filter(
(item, i, self) => self.indexOf(item) === i
);
}
return _app;
},
ws: (path, options, fn) => {
app.ws(
path,
options && options.isRaw
? (ws, req) => fn(req, ws)
: ws(path, options, fn, config, ajv)
);
return _app;
},
static: function staticRoute(
route,
path,
{ index = 'index.html', addPrettyUrl = true, streamConfig } = {}
) {
if (path === undefined) {
path = route;
route = '/';
} else if (!route.endsWith('/')) {
route += '/';
}
const staticFilesPath = fs.readdirSync(path);
for (const fileName of staticFilesPath) {
const pathNormalisedFileName = resolve(path, fileName);
const lstatInfo = fs.lstatSync(pathNormalisedFileName);
if (lstatInfo && lstatInfo.isDirectory()) {
staticRoute(route + fileName, pathNormalisedFileName, {
index,
addPrettyUrl,
streamConfig
});
continue;
}
const isStreamableResource = getMime(fileName);
const routeNormalised = route + fileName;
const handler = async (res, req) => {
if (res.__streaming || res.__called) {
return;
}
if (isStreamableResource) {
await sendFile(res, req, pathNormalisedFileName, streamConfig);
return res;
} else {
const sendFile = await readFile(pathNormalisedFileName, 'utf-8');
res.end(sendFile);
res.__called = true;
return res;
}
};
if (addPrettyUrl && fileName === index) {
app.get(route, handler);
}
app.get(routeNormalised, handler);
}
return _app;
}
};
httpMethods.forEach((method) => {
_app[method] = (path, ...fns) => {
let isPrefix;
let isDirect;
if (fns.length > 0) {
const isRaw = fns.find((fn) => fn.isRaw === true);
isPrefix = fns.find((fn) => fn.isPrefix);
isDirect = fns.find((fn) => fn.direct);
if (isRaw) {
const fn = fns.pop();
app[method](
isPrefix ? isPrefix + path : path,
isDirect ? fn : (res, req) => fn(req, res)
);
return _app;
}
}
const pathMiddleware = pathMiddlewares[path];
if (pathMiddleware && pathMiddleware.length > 0) {
pathMiddleware.called = true;
}
if (middlewares && middlewares.length > 0) {
middlewares.called = true;
}
const handler = http(
isPrefix ? isPrefix + path : path,
middlewares.concat(pathMiddleware || []).concat(fns),
config,
ajv,
method,
_app
);
if (!_app.anyApplied && method !== 'options') {
_app.anyApplied = path === '/*';
}
app[method](
typeof path === 'string' ? path : '/*',
typeof path === 'function' && !handler ? path : handler
);
return _app;
};
});
_app.publish = (channel, message) => app.publish(channel, message);
return _app;
};
export { nanoexpress as default };