@kitmi/jacaranda
Version:
JavaScript application framework
218 lines (180 loc) • 7.14 kB
JavaScript
import path from 'node:path';
import { _, batchAsync_, isPlainObject, isRunAsEsm } from '@kitmi/utils';
import { InvalidConfiguration } from '@kitmi/types';
import Runnable from './Runnable';
import Routable from './Routable';
import ServiceContainer from './ServiceContainer';
import defaultServerOpts from './defaultServerOpts';
import * as builtinMiddlewares from './middlewares';
const isEsm = isRunAsEsm();
export function createWebServer(Base) {
/**
* Web server class.
* @class
* @extends Routable(App)
*/
return class extends Routable(Base) {
/**
* @param {string} [name='server'] - The name of the server.
* @param {object} [options] - The app module's extra options defined in its parent's configuration.
* @property {object} [options.logger] - Logger options
* @property {bool} [options.verbose=false] - Flag to output trivial information for diagnostics
* @property {string} [options.workingPath] - App's working path, default to process.cwd()
* @property {string} [options.configPath] - App's config path, default to "conf" under workingPath
* @property {string} [options.configName] - App's config basename, default to "app"
* @property {string} [options.sourcePath='./src'] - Relative path of back-end server source files
* @property {string} [options.appModulesPath=app_modules] - Relative path of child modules
*/
constructor(name, options) {
if (typeof options === 'undefined' && isPlainObject(name)) {
options = name;
name = undefined;
}
super(name || 'server', {
...defaultServerOpts,
sourcePath: isEsm ? './src' : './cjs',
...options,
});
/**
* Whether it is running as ESM.
* @member {boolean}
*/
this.isEsm = isEsm;
/**
* Hosting server.
* @member {WebServer}
**/
this.server = this;
/**
* Whether it is a server.
* @member {boolean}
**/
this.isServer = true;
/**
* App modules path.
* @member {string}
*/
this.appModulesPath = this.toAbsolutePath(this.options.appModulesPath);
/**
* Base route.
* @member {string}
*/
this.route = '/';
// add built-in middlewares
this.registry.middlewares = {
...builtinMiddlewares,
...this.registry.middlewares,
};
this.once('after:Initial', () => {
if (this.engine == null) {
throw new InvalidConfiguration('Missing server engine feature, e.g. koa or hono.', this);
}
});
process.once('SIGINT', () => {
this.stop_()
.then(() => {})
// eslint-disable-next-line no-undef
.catch((error) => console.error(error.message || error));
});
}
async stop_() {
let stopByThis = false;
if (this.started) {
stopByThis = true;
if (this.appModules) {
await batchAsync_(this.appModules, (app) => app.stop_());
delete this.appModules;
delete this.appModulesByAlias;
}
}
if (stopByThis && this.httpServer) {
await new Promise((resolve, reject) => {
this.httpServer.close((err) => {
if (err) return reject(err);
resolve();
});
});
delete this.httpServer;
this.log('info', `The http service is stopped.`);
}
return super.stop_();
}
visitChildModules(vistor) {
super.visitChildModules(vistor);
if (this.appModules) {
_.each(this.appModules, vistor);
}
}
/**
* Mount an app at specified route.
* @param {WebModule} app
*/
mountApp(app) {
if (!this.appModules) {
this.appModules = {};
this.appModulesByAlias = {};
}
if (app.route in this.appModules) {
throw new Error(`The route "${app.route}" is already mounted by another app.`);
}
this.engine.mount(app.route, app.router);
this.appModules[app.route] = app;
if (app.name in this.appModulesByAlias) {
const existingApp = this.appModulesByAlias[app.name];
existingApp.alias = `${existingApp.name}@${existingApp.route}]`;
//move bucket
this.appModulesByAlias[existingApp.alias] = existingApp;
delete this.appModulesByAlias[app.name];
app.alias = `${app.name}@${app.route}]`;
} else {
app.alias = app.name;
}
this.appModulesByAlias[app.alias] = app;
this.log('verbose', `All routes from app [${app.alias}] are mounted under "${app.route}".`);
}
/**
* Get the app module object by base route
* @param {string} p - App module base route started with "/"
*/
getAppByRoute(p) {
return this.appModules[p];
}
/**
* Get the app module object by app alias, usually the app name if no duplicate entry
* @param {string} a - App module alias
*/
getAppByAlias(a) {
return this.appModulesByAlias[a];
}
/**
* Get a registered service
* @param {string} name
*
* @example
* // Get service from a lib module
* const service = app.getService('<lib name>/<service name>');
* // e.g const service = app.getService('data/mysql.mydb');
*
* // Get service from a web app module
* const service = app.getService('<app name>:<service name>');
* // e.g const service = app.getService('admin:mysql.mydb');
*/
getService(name) {
let pos = name.indexOf(':');
if (pos === -1) {
return super.getService(name);
}
let modAlias = name.substring(0, pos);
name = name.substring(pos + 1);
let app = this.getAppByAlias(modAlias);
return app && app.getService(name, true);
}
_getFeatureFallbackPath() {
let pathArray = super._getFeatureFallbackPath();
pathArray.splice(1, 0, path.resolve(__dirname, 'serverFeatures'));
return pathArray;
}
};
}
const WebServer = createWebServer(Runnable(ServiceContainer));
export default WebServer;