UNPKG

@kitmi/jacaranda

Version:

JavaScript application framework

667 lines (666 loc) 25.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: all[name] }); } _export(exports, { default: function() { return _default; }, getNodeEnv: function() { return getNodeEnv; } }); const _config = /*#__PURE__*/ _interop_require_wildcard(require("@kitmi/config")); const _utils = require("@kitmi/utils"); const _sys = require("@kitmi/sys"); const _types = require("@kitmi/types"); const _allSync = require("@kitmi/validators/allSync"); const _algo = require("@kitmi/algo"); const _nodepath = /*#__PURE__*/ _interop_require_default(require("node:path")); const _Feature = /*#__PURE__*/ _interop_require_default(require("./Feature")); const _defaultOpts = /*#__PURE__*/ _interop_require_default(require("./defaultOpts")); const _AsyncEmitter = /*#__PURE__*/ _interop_require_default(require("./helpers/AsyncEmitter")); const _logger = require("./helpers/logger"); const _runtime = /*#__PURE__*/ _interop_require_wildcard(require("./runtime")); function _define_property(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _interop_require_default(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interop_require_wildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = { __proto__: null }; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for(var key in obj){ if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } const FILE_EXT = [ '.js', '.mjs', '.cjs', '.ts' ]; function getNodeEnv() { return process.env.NODE_ENV || 'development'; } const configOverrider = (defConf, envConf)=>{ const { serviceGroup: defServiceGroup, ..._def } = defConf ?? {}; const { serviceGroup: envServiceGroup, ..._env } = envConf ?? {}; const serviceGroup = {}; if (defServiceGroup || envServiceGroup) { defServiceGroup && _utils._.each(defServiceGroup, (servicesMap, serviceName)=>{ serviceGroup[serviceName] = servicesMap; }); envServiceGroup && _utils._.each(envServiceGroup, (servicesMap, serviceName)=>{ serviceGroup[serviceName] = { ...serviceGroup[serviceName], ...servicesMap }; }); } const ret = { ..._def, ..._env }; if (!_utils._.isEmpty(serviceGroup)) { ret.serviceGroup = serviceGroup; } return ret; }; /** * Service container class. * @class */ class ServiceContainer extends _AsyncEmitter.default { /** * Start the container. * @fires ServiceContainer#configLoaded * @fires ServiceContainer#ready * @returns {Promise.<ServiceContainer>} */ async start_() { if (this.options.logWithAppName || this.host?.options.logWithAppName) { this.logName = this.server ? this.isServer ? `srv:${this.name}` : this.routable ? `mod:${this.server.name}>${this.name}` : `lib:${this.server.name}>${this.name}` : this.runnable ? `cli:${this.name}` : `lib:${this.host.name}>${this.name}`; } this.log('verbose', `Starting app [${this.name}] ...`); await this.emit_('starting', this); this._featureRegistry = { //firstly look up "features" under current working path, and then try the builtin features path '*': this._getFeatureFallbackPath() }; /** * Loaded features, name => feature object * @member {object} */ this.features = {}; /** * Loaded services * @member {object} */ this.services = {}; if (this.options.loadConfigFromOptions) { this.config = this.options.config; } else { let configLoader; if (this.options.configType === 'yaml') { configLoader = this.options.disableEnvAwareConfig ? new _config.default(new _config.YamlConfigProvider(_nodepath.default.join(this.configPath, this.options.configName + '.yaml')), this) : _config.default.createEnvAwareYamlLoader(this.configPath, this.options.configName, getNodeEnv(), this, configOverrider); } else { configLoader = this.options.disableEnvAwareConfig ? new _config.default(new _config.JsonConfigProvider(_nodepath.default.join(this.configPath, this.options.configName + '.json')), this) : _config.default.createEnvAwareJsonLoader(this.configPath, this.options.configName, getNodeEnv(), this, configOverrider); } /** * Configuration loader instance * @member {ConfigLoader} */ this.configLoader = configLoader; await this.loadConfig_(); } /** * Config loaded event. * @event ServiceContainer#configLoaded */ await this.emit_('configLoaded', this.config); if (!_utils._.isEmpty(this.config)) { await this._loadFeatures_(); } else { this.log('verbose', `Empty configuration! Config path: ${this.configPath}`); this.flushLogCache(); } /** * App ready * @event ServiceContainer#ready */ await this.emit_('ready', this); /** * Flag showing the app is started or not. * @member {bool} */ this.started = true; return this; } /** * Stop the container * @fires ServiceContainer#stopping * @returns {Promise.<ServiceContainer>} */ async stop_() { /** * App stopping * @event ServiceContainer#stopping */ await this.emit_('stopping', this); this.log('verbose', `Stopping app [${this.name}] ...`); this.started = false; delete this.services; delete this.features; delete this._featureRegistry; delete this.config; delete this.configLoader; await this.emit_('stopped', this); this.allOff(); } /** * @returns {ServiceContainer} */ async loadConfig_() { let configVariables = this.getRuntimeVariables(); /** * App configuration * @member {object} */ this.config = await this.configLoader.load_(configVariables); return this; } /** * Translate a relative path of this app module to an absolute path * @param {array} args - Array of path parts * @returns {string} */ toAbsolutePath(...args) { args = args.filter((arg)=>arg != null); if (args.length === 0) { return this.workingPath; } return _nodepath.default.resolve(this.workingPath, ...args); } /** * Require a module from the runtime registry * @param {*} moduleName * @returns {*} */ requireModule(moduleName, noThrow) { const m = _runtime.default.get(_runtime.NS_MODULE, moduleName); if (m == null && !noThrow) { throw new _types.ApplicationError(`Module "${moduleName}" not found in runtime registry.`); } return m; } tryRequire(pkgName) { return (0, _utils.esmCheck)((0, _sys.tryRequire)(pkgName, this.workingPath)); } /** * Try to require a package, if it's an esm module, import it. * @param {*} pkgName * @param {*} useDefault * @returns {*} */ async tryRequire_(pkgName, useDefault, noThrow) { let _module; try { _module = (0, _sys.tryRequire)(pkgName, this.workingPath); } catch (error) { if (error.code === 'ERR_REQUIRE_ESM') { _module = await import(pkgName); } else if (noThrow && error.code === 'MODULE_NOT_FOUND') { return undefined; } else { throw error; } } if (_module && useDefault) { return (0, _utils.esmCheck)(_module); } return _module; } /** * Register a service * @param {string} name * @param {object} serviceObject * @param {boolean} override * @returns {ServiceContainer} */ registerService(name, serviceObject, override) { if (name in this.services && !override) { throw new Error('Service "' + name + '" already registered!'); } this.services[name] = serviceObject; this.log('verbose', `Service "${name}" registered.`); return this; } /** * Check whether a service exists * @param {*} name * @returns {boolean} */ hasService(name) { return name in this.services; } /** * Get a service from module hierarchy * @param name * @returns {object} */ getService(name) { return this.services[name]; } /** * Check whether a feature is enabled in the app. * @param {string} feature * @returns {bool} */ enabled(feature) { return this.features[feature]?.enabled || this.host?.enabled(feature) || false; } /** * Add more or overide current feature registry * @param {object} registry */ addFeatureRegistry(registry) { // * is used as the fallback location to find a feature if (registry.hasOwnProperty('*')) { (0, _utils.pushIntoBucket)(this._featureRegistry, '*', registry['*']); } Object.assign(this._featureRegistry, _utils._.omit(registry, [ '*' ])); } /** * Helper method to log an exception * @param {*} level * @param {*} error * @param {*} summary * @returns {ServiceContainer} */ logException(level, error, summary, payload) { this.log(level, (summary ? summary + '\n' : '') + (error.message || error), { ...typeof error === 'object' ? _utils._.pick(error, [ 'name', 'status', 'code', 'info', 'stack', 'request' ]) : null, ...payload }); return this; } trace(...args) { return this.log('trace', ...args); } debug(...args) { return this.log('debug', ...args); } verbose(...args) { return this.log('verbose', ...args); } info(...args) { return this.log('info', ...args); } warn(...args) { return this.log('warn', ...args); } error(...args) { return this.log('error', ...args); } featureConfig(config, typeInfo, name) { return this.sanitize(config, typeInfo, name); } sanitize(config, typeInfo, name, category) { try { return _allSync.Types.OBJECT.sanitize(config, { type: 'object', ...typeInfo }, this.i18n, name); } catch (err) { let message; if (err instanceof _types.ValidationError) { message = _types.ValidationError.formatError(err); } else { message = err.message; } throw new _types.InvalidConfiguration(message, this, category ? `${category}::${name}` : name); } } requireServices(services) { const notRegisterred = _utils._.find(_utils._.castArray(services), (service)=>!this.hasService(service)); if (notRegisterred) { throw new _types.ApplicationError(`Service "${notRegisterred}" is required.`); } } getRuntime(...args) { if (args.length === 0) { return _runtime.default; } return _runtime.default.get(...args); } getRuntimeVariables() { const processInfo = { arch: process.arch, cwd: process.cwd(), pid: process.pid, platform: process.platform }; return { app: { name: this.name, workingPath: this.workingPath, configPath: this.configPath, sourcePath: this.sourcePath, featuresPath: this.featuresPath, options: this.options }, env: _runtime.default.get(_runtime.K_ENV) ?? {}, process: processInfo }; } _getFeatureFallbackPath() { return [ _nodepath.default.resolve(__dirname, 'features'), this.featuresPath ]; } _sortFeatures(features) { if (features.length === 0) { return features; } const topoSort = new _algo.TopoSort(); features.forEach(([feature])=>{ topoSort.depends(feature.name, feature.depends); }); const groups = (0, _utils.arrayToObject)(features, ([feature])=>feature.name); const keys = topoSort.sort(); const sorted = []; keys.forEach((key)=>{ const feature = groups[key]; if (feature) { sorted.push(feature); } else { if (!this.enabled(key)) { throw new _types.InvalidConfiguration(`A prerequisite feature "${key}" is not enabled.`, this); } } }); return sorted; } flushLogCache() { if (this.runnable && !('logger' in this.config)) { const _makeLogger = (logLevel, channel)=>({ log: (0, _logger.makeLogger)(_logger.consoleLogger, logLevel, channel), child: (arg1, arg2)=>_makeLogger(arg2?.level || logLevel, arg1?.module), flush: (cb)=>{ if (cb) cb(); } }); this.logger = _makeLogger(this.options.logLevel); this._logCache.forEach((log)=>this.logger.log(...log)); this._logCache.length = 0; } } /** * Load features * @private * @returns {bool} */ async _loadFeatures_() { try { // run config stage separately first let configStageFeatures = []; // load features await (0, _utils.batchAsync_)(this.config, async (featureOptions, name)=>{ if (this.options.allowedFeatures && this.options.allowedFeatures.indexOf(name) === -1) { //skip disabled features return; } if (this.options.ignoreFeatures && this.options.ignoreFeatures.indexOf(name) !== -1) { //ignore features, useful for worker to use the same config with server return; } let feature; try { feature = await this._loadFeature_(name); } catch (err) { //ignore the first trial //this.log('warn', err.message, { err }); } if (feature && feature.stage === _Feature.default.CONF) { this.alreadyLoadedFeatures || (this.alreadyLoadedFeatures = new Set()); if (!this.alreadyLoadedFeatures.has(name)) { configStageFeatures.push([ feature, featureOptions ]); this.alreadyLoadedFeatures.add(name); } delete this.config[name]; } }); if (configStageFeatures.length > 0) { await this._loadFeatureGroup_(configStageFeatures, _Feature.default.CONF); //reload all features if any type of configuration feature exists return this._loadFeatures_(); } } finally{ // if no logger in config, use console logger this.flushLogCache(); } await this.emit_('configFinalized', this.config); if (this.options.logConfig && (this.options.logLevel === 'debug' || this.options.logLevel === 'verbose')) { this.log('verbose', 'Finalized config:', this.config); } let featureGroups = { [_Feature.default.INIT]: [], [_Feature.default.SERVICE]: [], [_Feature.default.PLUGIN]: [], [_Feature.default.FINAL]: [] }; // load features await (0, _utils.batchAsync_)(this.config, async (featureOptions, name)=>{ if (this.options.allowedFeatures && this.options.allowedFeatures.indexOf(name) === -1) { //skip disabled features return; } let feature = await this._loadFeature_(name); if (!(feature.stage in featureGroups)) { throw new Error(`Invalid feature stage. Feature: ${name}, type: ${feature.stage}`); } featureGroups[feature.stage].push([ feature, featureOptions ]); }); return (0, _utils.eachAsync_)(featureGroups, (group, stage)=>this._loadFeatureGroup_(group, stage)); } async _loadFeatureGroup_(featureGroup, groupStage) { featureGroup = this._sortFeatures(featureGroup); await this.emit_('before:' + groupStage); this.log('verbose', `Loading "${groupStage}" feature group ...`); await (0, _utils.eachAsync_)(featureGroup, async ([feature, options])=>{ const { name, depends } = feature; await this.emit_('before:load:' + name); this.log('verbose', `Loading feature "${name}" ...`); depends && this._dependsOn(depends, name); await feature.load_(this, options, name); this.features[name].enabled = true; this.log('verbose', `Feature "${name}" loaded. [OK]`); await this.emit_('after:load:' + name); }); this.log('verbose', `Finished loading "${groupStage}" feature group. [OK]`); await this.emit_('after:' + groupStage); } _dependsOn(features, fromFeature) { let hasNotEnabled = _utils._.find(_utils._.castArray(features), (feature)=>!this.enabled(feature)); if (hasNotEnabled) { throw new Error(`The "${hasNotEnabled}" feature depended by "${fromFeature}" feature is not enabled.`); } } /** * Load a feature object by name. * @private * @param {string} feature * @returns {object} */ async _loadFeature_(feature) { let featureObject = this.features[feature]; if (featureObject) return featureObject; let featurePath; // first try locale registry, then runtime registry featureObject = (0, _utils.get)(this.registry, 'features.' + feature); if (featureObject == null) { if (this._featureRegistry.hasOwnProperty(feature)) { //load by registry entry let loadOption = this._featureRegistry[feature]; if (Array.isArray(loadOption)) { if (loadOption.length === 0) { throw new Error(`Invalid registry value for feature "${feature}".`); } featurePath = loadOption[0]; featureObject = await this.tryRequire_(featurePath, true); if (loadOption.length > 1) { //one module may contains more than one feature featureObject = _utils._.get(featureObject, loadOption[1]); } } else { featurePath = loadOption; featureObject = await this.tryRequire_(featurePath, true); } } else { //load by fallback paths let searchingPath = this._featureRegistry['*']; //reverse fallback stack let found = _utils._.findLast(searchingPath, (p)=>FILE_EXT.find((ext)=>{ featurePath = _nodepath.default.join(p, feature + ext); return _sys.fs.existsSync(featurePath); })); if (!found) { throw new _types.InvalidConfiguration(`Don't know where to load feature "${feature}".`, this, { feature, searchingPath: searchingPath.join('\n'), passedInRegistry: Object.keys(this.registry?.features ?? {}).join(', '), config: this.config }); } featureObject = await this.tryRequire_(featurePath, true); } } if (!_Feature.default.validate(featureObject)) { throw new Error(`Invalid feature object loaded from "${featurePath}".`); } if (!featureObject.stage) { featureObject.stage = _Feature.default.SERVICE; } featureObject.name = feature; this.features[feature] = featureObject; return featureObject; } /** * @param {string} name - The name of the container instance. * @param {object} [options] - Container options * @property {string} [options.workingPath] - App's working path, default to process.cwd() * @property {string} [options.configPath="conf"] - App's config path, default to "conf" under workingPath * @property {string} [options.configName="app"] - App's config basename, default to "app" * @property {string} [options.configType="json"] - App's config type, default to "json" * @property {string} [options.disableEnvAwareConfig=false] - Don't use environment-aware config * @property {array} [options.allowedFeatures] - A list of enabled feature names * @property {array} [options.logWithAppName=false] - Log with app name * @property {boolean} [options.loadConfigFromOptions=false] - Whether to load config from passed-in options * @property {object} [options.config] - Config in options, used only when loadConfigFromOptions * @property {object} [options.registry] - Preloaded modules */ constructor(name, options){ super(); _define_property(this, "logError", (error, message, payload)=>{ return this.logException('error', error, message, payload); }); _define_property(this, "logErrorAsWarning", (error, message, payload)=>{ return this.logException('warn', error, message, payload); }); /** * Name of the app * @member {object} **/ this.name = name; /** * App options * @member {object} */ this.options = { ..._defaultOpts.default, ...options }; /** * Working directory of this cli app * @member {string} */ this.workingPath = this.options.workingPath ? _nodepath.default.resolve(this.options.workingPath) : process.cwd(); /** * Config path * @member {string} */ this.configPath = this.toAbsolutePath(this.options.configPath); /** * Source files path. * @member {string} **/ this.sourcePath = this.toAbsolutePath(this.options.sourcePath); /** * Feature path */ this.featuresPath = _nodepath.default.resolve(this.sourcePath, this.options.featuresPath); // preloaded modules // { apps, libs, models } this.registry = { ...this.options.registry }; this._logCache = []; // dummy this.logger = { log: (...args)=>this._logCache.push(args) }; this.log = (level, message, payload)=>{ if (this.logName) { message = `[${this.logName}] ${message}`; } if (payload) { this.logger.log(level, message, payload); } else { this.logger.log(level, message); } return this; }; } } const _default = ServiceContainer; //# sourceMappingURL=ServiceContainer.js.map