'use strict';
|
|
const assert = require('assert');
|
const fs = require('fs');
|
const KoaApplication = require('koa');
|
const EggConsoleLogger = require('egg-logger').EggConsoleLogger;
|
const debug = require('debug')('egg-core');
|
const is = require('is-type-of');
|
const co = require('co');
|
const BaseContextClass = require('./utils/base_context_class');
|
const utils = require('./utils');
|
const Router = require('@eggjs/router').EggRouter;
|
const Timing = require('./utils/timing');
|
const Lifecycle = require('./lifecycle');
|
|
const DEPRECATE = Symbol('EggCore#deprecate');
|
const ROUTER = Symbol('EggCore#router');
|
const EGG_LOADER = Symbol.for('egg#loader');
|
const CLOSE_PROMISE = Symbol('EggCore#closePromise');
|
|
class EggCore extends KoaApplication {
|
|
/**
|
* @constructor
|
* @param {Object} options - options
|
* @param {String} [options.baseDir=process.cwd()] - the directory of application
|
* @param {String} [options.type=application|agent] - whether it's running in app worker or agent worker
|
* @param {Object} [options.plugins] - custom plugins
|
* @since 1.0.0
|
*/
|
constructor(options = {}) {
|
options.baseDir = options.baseDir || process.cwd();
|
options.type = options.type || 'application';
|
|
assert(typeof options.baseDir === 'string', 'options.baseDir required, and must be a string');
|
assert(fs.existsSync(options.baseDir), `Directory ${options.baseDir} not exists`);
|
assert(fs.statSync(options.baseDir).isDirectory(), `Directory ${options.baseDir} is not a directory`);
|
assert(options.type === 'application' || options.type === 'agent', 'options.type should be application or agent');
|
|
super();
|
|
this.timing = new Timing();
|
|
// cache deprecate object by file
|
this[DEPRECATE] = new Map();
|
|
/**
|
* @member {Object} EggCore#options
|
* @private
|
* @since 1.0.0
|
*/
|
this._options = this.options = options;
|
this.deprecate.property(this, '_options', 'app._options is deprecated, use app.options instead');
|
|
/**
|
* logging for EggCore, avoid using console directly
|
* @member {Logger} EggCore#console
|
* @private
|
* @since 1.0.0
|
*/
|
this.console = new EggConsoleLogger();
|
|
/**
|
* @member {BaseContextClass} EggCore#BaseContextClass
|
* @since 1.0.0
|
*/
|
this.BaseContextClass = BaseContextClass;
|
|
/**
|
* Base controller to be extended by controller in `app.controller`
|
* @class Controller
|
* @extends BaseContextClass
|
* @example
|
* class UserController extends app.Controller {}
|
*/
|
const Controller = this.BaseContextClass;
|
|
/**
|
* Retrieve base controller
|
* @member {Controller} EggCore#Controller
|
* @since 1.0.0
|
*/
|
this.Controller = Controller;
|
|
/**
|
* Base service to be extended by services in `app.service`
|
* @class Service
|
* @extends BaseContextClass
|
* @example
|
* class UserService extends app.Service {}
|
*/
|
const Service = this.BaseContextClass;
|
|
/**
|
* Retrieve base service
|
* @member {Service} EggCore#Service
|
* @since 1.0.0
|
*/
|
this.Service = Service;
|
|
this.lifecycle = new Lifecycle({
|
baseDir: options.baseDir,
|
app: this,
|
logger: this.console,
|
});
|
this.lifecycle.on('error', err => this.emit('error', err));
|
this.lifecycle.on('ready_timeout', id => this.emit('ready_timeout', id));
|
this.lifecycle.on('ready_stat', data => this.emit('ready_stat', data));
|
|
/**
|
* The loader instance, the default class is {@link EggLoader}.
|
* If you want define
|
* @member {EggLoader} EggCore#loader
|
* @since 1.0.0
|
*/
|
const Loader = this[EGG_LOADER];
|
assert(Loader, 'Symbol.for(\'egg#loader\') is required');
|
this.loader = new Loader({
|
baseDir: options.baseDir,
|
app: this,
|
plugins: options.plugins,
|
logger: this.console,
|
serverScope: options.serverScope,
|
env: options.env,
|
});
|
}
|
|
/**
|
* override koa's app.use, support generator function
|
* @param {Function} fn - middleware
|
* @return {Application} app
|
* @since 1.0.0
|
*/
|
use(fn) {
|
assert(is.function(fn), 'app.use() requires a function');
|
debug('use %s', fn._name || fn.name || '-');
|
this.middleware.push(utils.middleware(fn));
|
return this;
|
}
|
|
/**
|
* Whether `application` or `agent`
|
* @member {String}
|
* @since 1.0.0
|
*/
|
get type() {
|
return this.options.type;
|
}
|
|
/**
|
* The current directory of application
|
* @member {String}
|
* @see {@link AppInfo#baseDir}
|
* @since 1.0.0
|
*/
|
get baseDir() {
|
return this.options.baseDir;
|
}
|
|
/**
|
* Alias to {@link https://npmjs.com/package/depd}
|
* @member {Function}
|
* @since 1.0.0
|
*/
|
get deprecate() {
|
const caller = utils.getCalleeFromStack();
|
if (!this[DEPRECATE].has(caller)) {
|
const deprecate = require('depd')('egg');
|
// dynamic set _file to caller
|
deprecate._file = caller;
|
this[DEPRECATE].set(caller, deprecate);
|
}
|
return this[DEPRECATE].get(caller);
|
}
|
|
/**
|
* The name of application
|
* @member {String}
|
* @see {@link AppInfo#name}
|
* @since 1.0.0
|
*/
|
get name() {
|
return this.loader ? this.loader.pkg.name : '';
|
}
|
|
/**
|
* Retrieve enabled plugins
|
* @member {Object}
|
* @since 1.0.0
|
*/
|
get plugins() {
|
return this.loader ? this.loader.plugins : {};
|
}
|
|
/**
|
* The configuration of application
|
* @member {Config}
|
* @since 1.0.0
|
*/
|
get config() {
|
return this.loader ? this.loader.config : {};
|
}
|
|
/**
|
* Execute scope after loaded and before app start.
|
*
|
* Notice:
|
* This method is now NOT recommanded and reguarded as a deprecated one,
|
* For plugin development, we should use `didLoad` instead.
|
* For application development, we should use `willReady` instead.
|
*
|
* @see https://eggjs.org/en/advanced/loader.html#beforestart
|
*
|
* @param {Function|GeneratorFunction|AsyncFunction} scope function will execute before app start
|
*/
|
beforeStart(scope) {
|
this.lifecycle.registerBeforeStart(scope);
|
}
|
|
/**
|
* register an callback function that will be invoked when application is ready.
|
* @see https://github.com/node-modules/ready
|
* @since 1.0.0
|
* @param {boolean|Error|Function} flagOrFunction -
|
* @return {Promise|null} return promise when argument is undefined
|
* @example
|
* const app = new Application(...);
|
* app.ready(err => {
|
* if (err) throw err;
|
* console.log('done');
|
* });
|
*/
|
ready(flagOrFunction) {
|
return this.lifecycle.ready(flagOrFunction);
|
}
|
|
/**
|
* If a client starts asynchronously, you can register `readyCallback`,
|
* then the application will wait for the callback to ready
|
*
|
* It will log when the callback is not invoked after 10s
|
*
|
* Recommend to use {@link EggCore#beforeStart}
|
* @since 1.0.0
|
*
|
* @param {String} name - readyCallback task name
|
* @param {object} opts -
|
* - {Number} [timeout=10000] - emit `ready_timeout` when it doesn't finish but reach the timeout
|
* - {Boolean} [isWeakDep=false] - whether it's a weak dependency
|
* @return {Function} - a callback
|
* @example
|
* const done = app.readyCallback('mysql');
|
* mysql.ready(done);
|
*/
|
readyCallback(name, opts) {
|
return this.lifecycle.legacyReadyCallback(name, opts);
|
}
|
|
/**
|
* Register a function that will be called when app close.
|
*
|
* Notice:
|
* This method is now NOT recommanded directly used,
|
* Developers SHOULDN'T use app.beforeClose directly now,
|
* but in the form of class to implement beforeClose instead.
|
*
|
* @see https://eggjs.org/en/advanced/loader.html#beforeclose
|
*
|
* @param {Function} fn - the function that can be generator function or async function.
|
*/
|
beforeClose(fn) {
|
this.lifecycle.registerBeforeClose(fn);
|
}
|
|
/**
|
* Close all, it will close
|
* - callbacks registered by beforeClose
|
* - emit `close` event
|
* - remove add listeners
|
*
|
* If error is thrown when it's closing, the promise will reject.
|
* It will also reject after following call.
|
* @return {Promise} promise
|
* @since 1.0.0
|
*/
|
async close() {
|
if (this[CLOSE_PROMISE]) return this[CLOSE_PROMISE];
|
this[CLOSE_PROMISE] = this.lifecycle.close();
|
return this[CLOSE_PROMISE];
|
}
|
|
/**
|
* get router
|
* @member {Router} EggCore#router
|
* @since 1.0.0
|
*/
|
get router() {
|
if (this[ROUTER]) {
|
return this[ROUTER];
|
}
|
const router = this[ROUTER] = new Router({ sensitive: true }, this);
|
// register router middleware
|
this.beforeStart(() => {
|
this.use(router.middleware());
|
});
|
return router;
|
}
|
|
/**
|
* Alias to {@link Router#url}
|
* @param {String} name - Router name
|
* @param {Object} params - more parameters
|
* @return {String} url
|
*/
|
url(name, params) {
|
return this.router.url(name, params);
|
}
|
|
del(...args) {
|
this.router.delete(...args);
|
return this;
|
}
|
|
get [EGG_LOADER]() {
|
return require('./loader/egg_loader');
|
}
|
|
/**
|
* Convert a generator function to a promisable one.
|
*
|
* Notice: for other kinds of functions, it directly returns you what it is.
|
*
|
* @param {Function} fn The inputted function.
|
* @return {AsyncFunction} An async promise-based function.
|
* @example
|
```javascript
|
const fn = function* (arg) {
|
return arg;
|
};
|
const wrapped = app.toAsyncFunction(fn);
|
wrapped(true).then((value) => console.log(value));
|
```
|
*/
|
toAsyncFunction(fn) {
|
if (!is.generatorFunction(fn)) return fn;
|
fn = co.wrap(fn);
|
return async function(...args) {
|
return fn.apply(this, args);
|
};
|
}
|
|
/**
|
* Convert an object with generator functions to a Promisable one.
|
* @param {Mixed} obj The inputted object.
|
* @return {Promise} A Promisable result.
|
* @example
|
```javascript
|
const fn = function* (arg) {
|
return arg;
|
};
|
const arr = [ fn(1), fn(2) ];
|
const promise = app.toPromise(arr);
|
promise.then(res => console.log(res));
|
```
|
*/
|
toPromise(obj) {
|
return co(function* () {
|
return yield obj;
|
});
|
}
|
}
|
|
// delegate all router method to application
|
utils.methods.concat([ 'all', 'resources', 'register', 'redirect' ]).forEach(method => {
|
EggCore.prototype[method] = function(...args) {
|
this.router[method](...args);
|
return this;
|
};
|
});
|
|
module.exports = EggCore;
|