'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 { /** * @class * @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;