diff --git a/README.md b/README.md index 271da401..995df505 100644 --- a/README.md +++ b/README.md @@ -20,53 +20,208 @@ [download-image]: https://img.shields.io/npm/dm/egg-loader.svg?style=flat-square [download-url]: https://npmjs.org/package/egg-loader -egg 文件加载器 +A core Plugable framework based on koa -## 使用说明 +**Don't use it directly, see [egg]** + +## Usage + +Directory structure + +``` +├── package.json +├── app.js (optional) +├── agent.js (optional) +├── app +| ├── router.js +│ ├── controller +│ │ └── home.js +| ├── extend (optional) +│ | ├── helper.js (optional) +│ | ├── filter.js (optional) +│ | ├── request.js (optional) +│ | ├── response.js (optional) +│ | ├── context.js (optional) +│ | ├── application.js (optional) +│ | └── agent.js (optional) +│ ├── service (optional) +│ ├── middleware (optional) +│ │ └── response_time.js +│ └── view (optional) +| ├── layout.html +│ └── home.html +├── config +| ├── config.default.js +│ ├── config.prod.js +| ├── config.test.js (optional) +| ├── config.local.js (optional) +| ├── config.unittest.js (optional) +│ └── plugin.js +``` + +Than you can start with code below ```js -const app = koa(); -const Loader = require('egg-loader'); -const loader = new Loader({ - baseDir: '/path/to/app', - eggPath: '/path/to/framework', - app: app, +const Application = require('egg-core').Application; +const app = new Application({ + baseDir: '/path/to/app' }); -loader.loadPlugin(); -loader.loadConfig(); +app.ready(() => { + app.listen(3000); +}); +``` + +## EggLoader + +EggLoader will load file or directory easily, you can also custom your loader with low level API. + +### constructor + +- {String} baseDir - current directory of application +- {Object} app - instance of egg application +- {Object} plugins - merge plugins for test +- {Logger} logger - logger instance,default is console + +### High Level API + +#### loadPlugin + +Load config/plugin.js + +#### loadConfig + +Load config/config.js and config/{serverEnv}.js + +#### loadController + +Load app/controller + +#### loadMiddleware + +Load app/middleware + +#### loadApplicationExtend + +Load app/extend/application.js + +#### loadContextExtend + +Load app/extend/context.js + +#### loadRequestExtend + +Load app/extend/request.js + +#### loadResponseExtend + +Load app/extend/response.js + +#### loadHelperExtend + +Load app/extend/helper.js + +#### loadCustomApp + +Load app.js + +#### loadCustomAgent + +Load agent.js + +#### loadService + +Load app/service + +### Low Level API + +#### getServerEnv() + +Get serverEnv for application, available serverEnv + +serverEnv | description +--- | --- +default | default environment +test | system integration testing environment +prod | production environment +local | local environment on your own computer +unittest | unit test environment + +You can use this.serverEnv directly after instantiation. + +#### getEggPaths() + +Get the directory of the frameworks, a new framework born by extending egg, then you can use this function to get all frameworks. + +#### getLoadUnits() + +A loadUnit is a directory that can be loaded by EggLoader, it has the same structure. + +This function will get add loadUnits follow the order: + +1. plugin +2. framework +3. app + +loadUnit has a path and a type(app, framework, plugin). + +```js +{ + path: 'path/to/application', + type: 'app', +} ``` -## API +#### getAppname() + +Get appname from package.json + +#### loadFile(filepath) + +Load single file, will invork when export is function. + +#### loadToApp(directory, property, LoaderOptions) + +Load the files in directory to app. -### options +Invoke `this.loadToApp('$baseDir/app/controller', 'controller')`, then you can use it by `app.controller`. -- baseDir: 应用根目录 -- eggPath: egg 本身的路径 -- plugins: 自定义插件配置 -- app: 任何基于 koa 实例化 +#### loadToContext(directory, property, LoaderOptions) -### methods +Load the files in directory to context, it will bind the context. -基础方式 +``` +// define service in app/service/query.js +module.exports = class Query { + constructor(ctx) { + // get the ctx + } + + get() {} +}; + +// use the service in app/controller/home.js +module.exports = function*() { + this.body = this.service.query.get(); +}; +``` -- loadFile: 加载单文件, -- loadDirs: 获取需要加载的所有目录,按照 egg > 插件 > 框架 > 应用的顺序加载。 +#### loadExtend(name, target) + +Loader app/extend/xx.js to target, example + +```js +this.loadExtend('application', app); +``` -业务方法 +### LoaderOptions -- getAppname: 获取应用名 -- loadServerEnv: 加载环境变量 -- loadConfig: 加载: config -- loadPlugin: 加载插件 -- loadApplication: 加载 extend/application.js 到 app -- loadRequest: 加载 extend/request.js 到 app.request -- loadResponse: 加载 extend/response.js 到 app.response -- loadContext: 加载 extend/context.js 到 app.context -- loadHelper: 加载 extend/helper.js,到 app.Helper.prototype,需要定义 app.Helper 才会加载 -- loadService: 加载 app/service 到 app.service -- loadProxy: 加载 app/proxy 到 app.proxy -- loadMiddleware: 加载中间件 -- loadController: 加载 app/controller 到 app.controller -- loadAgent: 加载 agent.js 进行自定义 -- loadApp: 加载 app.js 进行自定义 +- {String|Array} directory - directories to load +- {Object} target: attach object from loaded files, +- {String} ignore - ignore the files when load +- {Function} initializer - custom file exports +- {Boolean} lowercaseFirst - determine whether the fist letter is lowercase +- {Boolean} override: determine whether override the property when get the same name +- {Boolean} call - determine whether invoke when exports is function +- {Object} inject - an object that be the argument when invoke the function +[egg]: https://github.com/eggjs/egg diff --git a/index.js b/index.js index b562eb25..a5bba83e 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,3 @@ 'use strict'; -module.exports = require('./lib/base_loader'); +module.exports = require('./lib/egg_loader'); diff --git a/lib/base_loader.js b/lib/base_loader.js deleted file mode 100644 index 9ab75658..00000000 --- a/lib/base_loader.js +++ /dev/null @@ -1,320 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const assert = require('assert'); -const isFunction = require('is-type-of').function; -const interopRequire = require('interop-require'); -const debug = require('debug')('egg:loader'); -const Loader = require('./loader'); - -class EggLoader { - - /** - * @constructor - * @param {Object} options - * - {String} [baseDir] - 应用根目录 - * - {String} [eggPath] - 使用 egg-loader 的框架的路径,如 egg - * - {String} [customEgg] - 自定义入口框架的路径,每个异构技术 bu 都可以自定义自己的插件集合 - * - {Object} [plugins] - 自定义插件配置,测试用 - * - {Object} [app] - app 实例,如果是 Agent Worker 则传入 agent 实例,可为空 - * - {Logger} [logger] - logger 实例,默认是 console - */ - constructor(options) { - this.options = options || {}; - this.options.logger = this.options.logger || console; - this.app = this.options.app || {}; // master 没有 app - assert(fs.existsSync(this.options.baseDir), `${this.options.baseDir} not exists`); - - /** - * 读取 package.json - * @member {Object} EggLoader#pkg - */ - this.pkg = require(path.join(this.options.baseDir, 'package.json')); - - /** - * 初始化时传入,见 {@link EggLoader} - * @member {String} EggLoader#eggPath - */ - this.eggPath = fs.realpathSync(this.options.eggPath); - debug('Loaded eggPath %j', this.eggPath); - - /** - * 框架可以继承,从入口框架(CustomEgg)找到所有的框架的根目录 - * - * 需要通过配置 getter 来指定 eggPath 才能被加载到 - * - * ``` - * // lib/xx.js - * const egg = require('egg'); - * class XxApplication extends egg.Application { - * constructor(options) { - * super(options); - * } - * - * get [Symbol.for('egg#eggPath')]() { - * return path.join(__dirname, '..'); - * } - * } - * ``` - * @member {Array} EggLoader#frameworkPaths - */ - this.frameworkPaths = this.loadFrameworkPaths(); - debug('Loaded frameworkPaths %j', this.frameworkPaths); - - /** - * = this.eggPath + this.frameworkPaths - * @member {Array} EggLoader#eggPaths - */ - this.eggPaths = [ this.eggPath ].concat(this.frameworkPaths); - debug('Loaded eggPaths %j', this.eggPaths); - - /** - * 获取当前应用所在的机器环境,统一 serverEnv - * ``` - * serverEnv | 说明 - * --- | --- - * default | 默认环境 - * test | 交付测试 - * prod | 主站生产环境,包括预发,线上服务器 - * local | 本地开发环境,就是你的电脑本地启动 - * unittest | 单元测试环境,tnpm test, NODE_ENV=test - * ``` - * - * @member {String} EggLoader#serverEnv - */ - this.serverEnv = this.loadServerEnv(); - debug('Loaded serverEnv %j', this.serverEnv); - } - - /** - * 加载自定义的 app.js,**在 app.js 可做任何操作,但建议尽量减少此操作,做该做的事**。 - * - * 可加载路径查看 {@link EggLoader#loadDirs} - * @example - * ```js - * module.exports = function(app) { - * // 自定义 - * } - * ``` - */ - loadCustomApp() { - this.loadDirs() - .forEach(dir => this.loadFile(path.join(dir, 'app.js'))); - } - - /** - * 同 {@link EggLoader#loadCustomApp},但加载自定义的 agent.js - * - * 可加载路径查看 {@link EggLoader#loadDirs} - * @example - * ```js - * module.exports = function(agent) { - * // 自定义 - * } - * ``` - */ - loadCustomAgent() { - this.loadDirs() - .forEach(dir => this.loadFile(path.join(dir, 'agent.js'))); - } - - /** - * 加载 app/controller 目录下的文件 - * - * @param {Object} opt - loading 参数 - */ - loadController(opt) { - const app = this.app; - opt = Object.assign({ lowercaseFirst: true }, opt); - const controllerBase = path.join(this.options.baseDir, 'app/controller'); - - this.loadToApp(controllerBase, 'controller', opt); - app.controllers = app.controller; - app.coreLogger.info('[egg:loader] Controller loaded: %s', controllerBase); - } - - /** - * 加载指定文件,如果文件返回是函数则返回函数的调用结果,否则直接返回。 - * - * @param {String} filepath - 加载的文件路径 - * @param {Object} inject - 调用函数时的第一个参数,默认为 app - * @return {Object} - 返回加载文件的结果 - * @example - * ```js - * app.loader.loadFile(path.join(app.options.baseDir, 'config/router.js')); - * ``` - */ - loadFile(filepath) { - if (!fs.existsSync(filepath)) { - return null; - } - - let ret; - try { - ret = interopRequire(filepath); - } catch (err) { - err.message = `[egg-loader] load file ${filepath} error: ${err.message}`; - throw err; - } - // 可支持传入多个参数 - // function(arg1, args, ...) {} - let inject = Array.prototype.slice.call(arguments, 1); - if (inject.length === 0) inject = [ this.app ]; - return isFunction(ret) ? ret.apply(null, inject) : ret; - } - - /** - * 返回 egg 需要加载的目录 - * - * 1. 核心框架目录,目录为框架根目录下的 lib/core 目录,框架根目录来自 {@link EggLoader#eggPaths} - * 2. 已开启插件的根目录 - * 3. 应用根目录 - * - * @return {Array} 返回所有目录 - */ - loadDirs() { - // 做一层缓存 - if (this.dirs) { - return this.dirs; - } - - const dirs = this.dirs = []; - - // egg 本身路径,在 lib/core 目录下 - dirs.push(path.join(this.eggPath, 'lib/core')); - - // 插件目录,master 没有 plugin - if (this.orderPlugins) { - for (const plugin of this.orderPlugins) { - dirs.push(plugin.path); - } - } - - // egg 框架路径,在 lib/core 目录下 - for (const frameworkPath of this.frameworkPaths) { - dirs.push(path.join(frameworkPath, 'lib/core')); - } - - // 应用目录 - dirs.push(this.options.baseDir); - - debug('Loaded dirs %j', dirs); - return dirs; - } - - /** - * 获取环境变量 - * - * 1. 从 EGG_SERVER_ENV 获取,一般用于测试 - * 2. 从 `$baseDir/config/serverEnv` 读取,框架可根据实际情况自行设置 - * 3. 默认值 - * - * @return {String} serverEnv - * @see EggLoader#serverEnv - */ - loadServerEnv() { - let serverEnv = process.env.EGG_SERVER_ENV; - - const envPath = path.join(this.options.baseDir, 'config/serverEnv'); - if (fs.existsSync(envPath)) { - serverEnv = fs.readFileSync(envPath, 'utf8').trim(); - } - - if (!serverEnv) { - if (process.env.NODE_ENV === 'test') { - serverEnv = 'unittest'; - } else if (process.env.NODE_ENV === 'production') { - serverEnv = 'default'; - } else { - serverEnv = 'local'; - } - } - - return serverEnv; - } - - /** - * 获取 {@link EggLoader#frameworkPaths} - * @return {Array} 框架目录 - * @private - */ - loadFrameworkPaths() { - const eggPath = this.eggPath; - const frameworkPaths = []; - - addEggPath(this.options.customEgg); - - // 遍历整个原型链,获取原型链上所有的 eggPath - // 越核心的优先级越高 - let proto = this.app; - while (proto) { - proto = Object.getPrototypeOf(proto); - if (proto) { - const eggPath = proto[Symbol.for('egg#eggPath')]; - addEggPath(eggPath); - } - } - - return frameworkPaths; - - function addEggPath(dirpath) { - if (dirpath) { - // 使用 fs.realpathSync 来找到最终路径 - const realpath = fs.realpathSync(dirpath); - if (frameworkPaths.indexOf(realpath) === -1 && realpath !== eggPath) { - frameworkPaths.unshift(realpath); - } - } - } - } - - /** - * 返回应用 appname,默认获取 pkg.name - * - * @return {String} appname - * @private - */ - getAppname() { - if (this.pkg.name) { - debug('Loaded appname(%s) from package.json', this.pkg.name); - return this.pkg.name; - } - throw new Error('Can not get appname from package.json'); - } - - loadTo(directory, target, opt) { - opt = Object.assign({}, { - directory, - target, - inject: this.app, - }, opt); - new Loader(opt).load(); - } - - loadToApp(directory, field, opt) { - const target = this.app[field] = {}; - this.loadTo(directory, target, opt); - } - -} - -/** - * Mixin loader 方法到 BaseLoader,class 不支持多类继承 - * // ES6 Multiple Inheritance - * https://medium.com/@leocavalcante/es6-multiple-inheritance-73a3c66d2b6b - */ -const loaders = [ - require('./plugin_loader'), - require('./config_loader'), - require('./extend_loader'), - require('./proxy_loader'), - require('./service_loader'), - require('./middleware_loader'), -]; - -for (const loader of loaders) { - Object.assign(EggLoader.prototype, loader); -} - -module.exports = EggLoader; diff --git a/lib/config_loader.js b/lib/config_loader.js deleted file mode 100644 index 593fd815..00000000 --- a/lib/config_loader.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict'; - -const debug = require('debug')('egg:loader:config'); -const fs = require('fs'); -const path = require('path'); -const extend = require('extend'); -const getHomedir = require('./utils').getHomedir; - -module.exports = { - - /** - * config/config.js 加载类,封装加载逻辑, - * - * 加载时会合并 config.default.js 和 config.${env}.js - * - * config.middleware 和 config.proxy 的配置会被剔除 - * - * 可加载路径查看 {@link EggLoader#loadDirs} - * @method EggLoader#loadConfig - */ - loadConfig() { - const target = {}; - - const names = [ - 'config.default.js', - `config.${this.serverEnv}.js`, - ]; - - // 先加载一次应用配置,传给框架和插件的配置 - const appConfig = this._preloadAppConfig(); - - // egg config.default - // plugin config.default - // framework config.default - // egg config.{env} - // plugin config.{env} - // framework config.{env} - for (const filename of names) { - for (const dirpath of this.loadDirs()) { - const config = this._loadConfig(dirpath, filename, appConfig); - - if (!config) { - continue; - } - - debug('Loaded config %s/%s, %j', dirpath, filename, config); - extend(true, target, config); - } - } - - // 可以在 app.js 中操作 app.config.coreMiddleware 和 app.config.appMiddleware; - target.coreMiddleware = target.coreMiddlewares = target.coreMiddleware || []; - // 记录应用自定义中间件,后续可以根据此配置让插件在将中间件放在应用自定义中间件之前 - target.appMiddleware = target.appMiddlewares = target.middleware || []; - - /** - * 获取 `{baseDir}/config/config.{env}.js` 下的配置。 - * 包含以下配置: - * - * * `baseDir`: 应用文件基础目录, 如 `/home/admin/demoapp` - * * `pkg`: [package.json] 配置 - */ - this.config = target; - }, - - // 提前加载应用配置,可以传给其他配置 - _preloadAppConfig() { - const names = [ - 'config.default.js', - `config.${this.serverEnv}.js`, - ]; - const target = {}; - for (const filename of names) { - const config = this._loadConfig(this.options.baseDir, filename); - extend(true, target, config); - } - return target; - }, - - _loadConfig(dirpath, filename, extraInject) { - const pluginPaths = this.orderPlugins ? this.orderPlugins.map(plugin => plugin.path) : []; - const isPlugin = pluginPaths.indexOf(dirpath) > -1; - const isApp = dirpath === this.options.baseDir; - - let filepath = path.join(dirpath, 'config', filename); - // 兼容 config.js,config.js 和 config.default 是平级的 - if (filename === 'config.default.js' && !fs.existsSync(filepath)) { - filepath = path.join(dirpath, 'config/config.js'); - } - const name = this.getAppname(); - const config = this.loadFile(filepath, { - name, - baseDir: this.options.baseDir, - env: this.serverEnv, - HOME: getHomedir(), - pkg: this.pkg, - }, extraInject); - - if (!config) { - return null; - } - - // 插件和应用不允许配置 coreMiddleware - if (isPlugin || isApp) { - delete config.coreMiddleware; - } - // 框架和插件不运行配置 middleware 和 proxy 的属性,避免覆盖应用的 - if (!isApp) { - delete config.middleware; - delete config.proxy; - } - - return config; - }, - -}; diff --git a/lib/context_loader.js b/lib/context_loader.js new file mode 100644 index 00000000..36d1e13e --- /dev/null +++ b/lib/context_loader.js @@ -0,0 +1,74 @@ +'use strict'; + +const assert = require('assert'); +const is = require('is-type-of'); +const Loader = require('./loader'); +const classLoader = Symbol('classLoader'); +const EXPORTS = Loader.EXPORTS; + +class ClassLoader { + + constructor(options) { + assert(options.ctx, 'options.ctx is required'); + const properties = options.properties; + this._cache = new Map(); + this._ctx = options.ctx; + + for (const property in properties) { + this.defineProperty(property, properties[property]); + } + } + + defineProperty(property, values) { + Object.defineProperty(this, property, { + get() { + if (!this._cache.has(property)) { + this._cache.set(property, getInstance(values, this._ctx)); + } + return this._cache.get(property); + }, + }); + } +} + +class ContextLoader extends Loader { + + constructor(options) { + assert(options.field, 'options.field is required'); + assert(options.inject, 'options.inject is required'); + const target = options.target = {}; + if (options.fieldClass) { + options.inject[options.fieldClass] = target; + } + super(options); + + const app = this.options.inject; + + Object.defineProperty(app.context, options.field, { + get() { + if (!this[classLoader]) { + this[classLoader] = getInstance(target, this); + } + return this[classLoader]; + }, + }); + } +} + +module.exports = ContextLoader; + + +function getInstance(values, ctx) { + const Class = values[EXPORTS] ? values : null; + let instance; + if (Class) { + if (is.class(Class)) { + instance = new Class(ctx); + } else { + instance = Class; + } + } else { + instance = new ClassLoader({ ctx, properties: values }); + } + return instance; +} diff --git a/lib/egg_loader.js b/lib/egg_loader.js new file mode 100644 index 00000000..27a7c4b5 --- /dev/null +++ b/lib/egg_loader.js @@ -0,0 +1,286 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); +const isFunction = require('is-type-of').function; +const debug = require('debug')('egg-loader'); +const Loader = require('./loader'); +const ContextLoader = require('./context_loader'); +const loadFile = require('./utils').loadFile; +const getHomedir = require('./utils').getHomedir; +const Emitter = require('events').EventEmitter; + +class EggLoader { + + /** + * @constructor + * @param {Object} options + * - {String} [baseDir] - 应用根目录 + * - {Object} [app] - app 实例,如果是 Agent Worker 则传入 agent 实例,可为空 + * - {Object} [plugins] - 自定义插件配置,测试用 + * - {Logger} [logger] - logger 实例,默认是 console + */ + constructor(options) { + options = options || {}; + assert(fs.existsSync(options.baseDir), `${options.baseDir} not exists`); + + this.options = options; + this.options.logger = this.options.logger || console; + this.app = this.options.app || {}; // master 没有 app + + /** + * 读取 package.json + * @member {Object} EggLoader#pkg + */ + this.pkg = require(path.join(this.options.baseDir, 'package.json')); + + /** + * @member {Array} EggLoader#eggPaths + * @see EggLoader#getEggPaths + */ + this.eggPaths = this.getEggPaths(); + debug('Loaded eggPaths %j', this.eggPaths); + + /** + * @member {String} EggLoader#serverEnv + * @see EggLoader#getServerEnv + */ + this.serverEnv = this.getServerEnv(); + debug('Loaded serverEnv %j', this.serverEnv); + + this.appInfo = { + name: this.getAppname(), + baseDir: this.options.baseDir, + env: this.serverEnv, + HOME: getHomedir(), + pkg: this.pkg, + }; + } + + /** + * Get environment of Egg, **it's not NODE_ENV** + * + * 1. from `$baseDir/config/serverEnv` + * 2. from EGG_SERVER_ENV + * 3. from NODE_ENV + * + * serverEnv | description + * --- | --- + * default | default environment + * test | system integration testing + * prod | production + * local | local on your own computer + * unittest | unit test + * + * @return {String} serverEnv + */ + getServerEnv() { + let serverEnv; + + const envPath = path.join(this.options.baseDir, 'config/serverEnv'); + if (fs.existsSync(envPath)) { + serverEnv = fs.readFileSync(envPath, 'utf8').trim(); + } + + if (!serverEnv) { + serverEnv = process.env.EGG_SERVER_ENV; + } + + if (!serverEnv) { + if (process.env.NODE_ENV === 'test') { + serverEnv = 'unittest'; + } else if (process.env.NODE_ENV === 'production') { + serverEnv = 'default'; + } else { + serverEnv = 'local'; + } + } + + return serverEnv; + } + + /** + * Get appname from pkg.name + * + * @return {String} appname + * @private + */ + getAppname() { + if (this.pkg.name) { + debug('Loaded appname(%s) from package.json', this.pkg.name); + return this.pkg.name; + } + const pkg = path.join(this.options.baseDir, 'package.json'); + throw new Error(`name is required from ${pkg}`); + } + + /** + * Get all framework directories. + * + * You can extend Application of egg, the entrypoint is options.app, + * + * loader will find all directories from the prototype of Application, + * you should define `Symbol.for('egg#eggPath')` property. + * + * ``` + * // lib/xx.js + * const egg = require('egg'); + * class XxApplication extends egg.Application { + * constructor(options) { + * super(options); + * } + * + * get [Symbol.for('egg#eggPath')]() { + * return path.join(__dirname, '..'); + * } + * } + * ``` + * + * @return {Array} framework directories + */ + getEggPaths() { + const eggPaths = []; + + let proto = this.app; + while (proto) { + proto = Object.getPrototypeOf(proto); + if (proto) { + if (isKoa(proto)) { + break; + } + const eggPath = proto[Symbol.for('egg#eggPath')]; + assert(eggPath, 'Symbol.for(\'egg#eggPath\') is required on Application'); + // 使用 fs.realpathSync 来找到最终路径 + const realpath = fs.realpathSync(eggPath); + if (eggPaths.indexOf(realpath) === -1) { + eggPaths.unshift(realpath); + } + } + } + + return eggPaths; + } + + // Low Level API + + /** + * Load single file, will invork when export is function + * + * @param {String} filepath - fullpath + * @param {Array} arguments - pass rest arguments into the function when invork + * @return {Object} exports + * @example + * ```js + * app.loader.loadFile(path.join(app.options.baseDir, 'config/router.js')); + * ``` + */ + loadFile(filepath) { + if (!fs.existsSync(filepath)) { + return null; + } + + const ret = loadFile(filepath); + // function(arg1, args, ...) {} + let inject = Array.prototype.slice.call(arguments, 1); + if (inject.length === 0) inject = [ this.app ]; + return isFunction(ret) ? ret.apply(null, inject) : ret; + } + + /** + * Get all loadUnit + * + * loadUnit is a directory that can be loaded by EggLoader, it has the same structure. + * loadUnit has a path and a type(app, framework, plugin). + * + * The order of the loadUnits: + * + * 1. plugin + * 2. framework + * 3. app + * + * @return {Array} loadUnits + */ + getLoadUnits() { + if (this.dirs) { + return this.dirs; + } + + const dirs = this.dirs = []; + + // 插件目录,master 没有 plugin + if (this.orderPlugins) { + for (const plugin of this.orderPlugins) { + dirs.push({ + path: plugin.path, + type: 'plugin', + }); + } + } + + // egg 框架路径 + for (const eggPath of this.eggPaths) { + dirs.push({ + path: eggPath, + type: 'framework', + }); + } + + // 应用目录 + dirs.push({ + path: this.options.baseDir, + type: 'app', + }); + + debug('Loaded dirs %j', dirs); + return dirs; + } + + loadToApp(directory, field, opt) { + const target = this.app[field] = {}; + opt = Object.assign({}, { + directory, + target, + inject: this.app, + }, opt); + new Loader(opt).load(); + } + + loadToContext(directory, field, opt) { + opt = Object.assign({}, { + directory, + field, + inject: this.app, + }, opt); + new ContextLoader(opt).load(); + } + +} + +/** + * Mixin loader 方法到 BaseLoader,class 不支持多类继承 + * // ES6 Multiple Inheritance + * https://medium.com/@leocavalcante/es6-multiple-inheritance-73a3c66d2b6b + */ +const loaders = [ + require('./mixin/plugin'), + require('./mixin/config'), + require('./mixin/extend'), + require('./mixin/custom'), + require('./mixin/proxy'), + require('./mixin/service'), + require('./mixin/middleware'), + require('./mixin/controller'), +]; + +for (const loader of loaders) { + Object.assign(EggLoader.prototype, loader); +} + +module.exports = EggLoader; + +function isKoa(app) { + return app.hasOwnProperty('use') && + app.hasOwnProperty('listen') && + Object.getPrototypeOf(app) === Emitter.prototype; +} diff --git a/lib/loader.js b/lib/loader.js index a0ebf96d..c144d9bc 100644 --- a/lib/loader.js +++ b/lib/loader.js @@ -5,9 +5,10 @@ const fs = require('fs'); const debug = require('debug')('egg-loader:loader'); const path = require('path'); const globby = require('globby'); -const interopRequire = require('interop-require'); const is = require('is-type-of'); +const loadFile = require('./utils').loadFile; const FULLPATH = Symbol('EGG_LOADER_ITEM_FULLPATH'); +const EXPORTS = Symbol('EGG_LOADER_ITEM_EXPORTS'); const defaults = { directory: null, @@ -33,6 +34,8 @@ class Loader { const target = this.options.target; for (const item of items) { debug('loading item %j', item); + // item { properties: [ 'a', 'b', 'c'], exports } + // => target.a.b.c = exports item.properties.reduce((target, property, index) => { let obj; const properties = item.properties.slice(0, index + 1).join('.'); @@ -41,7 +44,10 @@ class Loader { if (!this.options.override) throw new Error(`can't overwrite property '${properties}' from ${target[property][FULLPATH]} by ${item.fullpath}`); } obj = item.exports; - if (obj) obj[FULLPATH] = item.fullpath; + if (obj) { + obj[FULLPATH] = item.fullpath; + obj[EXPORTS] = true; + } } else { obj = target[property] || {}; } @@ -87,19 +93,18 @@ class Loader { } module.exports = Loader; +module.exports.EXPORTS = EXPORTS; // a/b/c.js => ['a', 'b', 'c'] function getProperties(filepath, lowercaseFirst) { return filepath .replace('.js', '') .split('/') - .map(function(property) { + .map(property => { if (!/^[a-z][a-z0-9_-]*$/i.test(property)) { throw new Error(`${property} is not match 'a-z0-9_-' in ${filepath}`); } - let result = property.replace(/[_-][a-z]/ig, function(s) { - return s.substring(1).toUpperCase(); - }); + let result = property.replace(/[_-][a-z]/ig, s => s.substring(1).toUpperCase()); if (lowercaseFirst) { result = result[0].toLowerCase() + result.substring(1); } @@ -107,23 +112,30 @@ function getProperties(filepath, lowercaseFirst) { }); } +// Get exports from filepath +// If exports is null/undefined, it will be ignored function getExports(fullpath, initializer, isCall, inject) { - let exports; - try { - exports = interopRequire(fullpath); - } catch (err) { - err.message = 'load file: ' + fullpath + ', error: ' + err.message; - throw err; - } + let exports = loadFile(fullpath); + // process exports as you like if (initializer) { exports = initializer(exports); } + // return exports when it's a class or generator + // + // module.exports = class Service {}; + // or + // module.exports = function*() {} if (is.class(exports) || is.generatorFunction(exports)) { return exports; } + // return exports after call when it's a function + // + // module.exports = function(app) { + // return {}; + // } if (isCall && is.function(exports)) { exports = exports(inject); if (exports != null) { @@ -131,5 +143,6 @@ function getExports(fullpath, initializer, isCall, inject) { } } + // return exports what is return exports; } diff --git a/lib/mixin/config.js b/lib/mixin/config.js new file mode 100644 index 00000000..fe0f8481 --- /dev/null +++ b/lib/mixin/config.js @@ -0,0 +1,95 @@ +'use strict'; + +const debug = require('debug')('egg-loader:config'); +const fs = require('fs'); +const path = require('path'); +const extend = require('extend'); +const assert = require('assert'); + + +module.exports = { + + /** + * Load config/config.js + * + * Will merge config.default.js 和 config.${env}.js + * + * @method EggLoader#loadConfig + */ + loadConfig() { + const target = {}; + + const names = [ + 'config.default.js', + `config.${this.serverEnv}.js`, + ]; + + // Load Application config first + const appConfig = this._preloadAppConfig(); + + // plugin config.default + // framework config.default + // app config.default + // plugin config.{env} + // framework config.{env} + // app config.{env} + for (const filename of names) { + for (const unit of this.getLoadUnits()) { + const config = this._loadConfig(unit.path, filename, appConfig, unit.type); + + if (!config) { + continue; + } + + debug('Loaded config %s/%s, %j', unit.path, filename, config); + extend(true, target, config); + } + } + + // You can manipulate the order of app.config.coreMiddleware and app.config.appMiddleware in app.js + target.coreMiddleware = target.coreMiddlewares = target.coreMiddleware || []; + target.appMiddleware = target.appMiddlewares = target.middleware || []; + + this.config = target; + }, + + _preloadAppConfig() { + const names = [ + 'config.default.js', + `config.${this.serverEnv}.js`, + ]; + const target = {}; + for (const filename of names) { + const config = this._loadConfig(this.options.baseDir, filename, undefined, 'app'); + extend(true, target, config); + } + return target; + }, + + _loadConfig(dirpath, filename, extraInject, type) { + const isPlugin = type === 'plugin'; + const isApp = type === 'app'; + + let filepath = path.join(dirpath, 'config', filename); + // let config.js compatible + if (filename === 'config.default.js' && !fs.existsSync(filepath)) { + filepath = path.join(dirpath, 'config/config.js'); + } + const config = this.loadFile(filepath, this.appInfo, extraInject); + + if (!config) { + return null; + } + + if (isPlugin || isApp) { + assert(!config.coreMiddleware, 'Can not define coreMiddleware in app or plugin'); + } + if (!isApp) { + assert(!config.middleware, 'Can not define middleware in framework or plugin'); + assert(!config.proxy, 'Can not define proxy in framework or plugin'); + } + + return config; + }, + +}; diff --git a/lib/mixin/controller.js b/lib/mixin/controller.js new file mode 100644 index 00000000..72644490 --- /dev/null +++ b/lib/mixin/controller.js @@ -0,0 +1,21 @@ +'use strict'; + +const path = require('path'); + +module.exports = { + + /** + * load app/controller + * + * @param {Object} opt LoaderOptions + */ + loadController(opt) { + const app = this.app; + opt = Object.assign({ lowercaseFirst: true }, opt); + const controllerBase = path.join(this.options.baseDir, 'app/controller'); + + this.loadToApp(controllerBase, 'controller', opt); + app.coreLogger.info('[egg:loader] Controller loaded: %s', controllerBase); + }, + +}; diff --git a/lib/mixin/custom.js b/lib/mixin/custom.js new file mode 100644 index 00000000..de8968fe --- /dev/null +++ b/lib/mixin/custom.js @@ -0,0 +1,35 @@ +'use strict'; + +const path = require('path'); + +module.exports = { + + /** + * load app.js + * + * @example + * ```js + * module.exports = function(app) { + * // can do everything + * do(); + * + * // if you will invork asynchronous, you can use readyCallback + * const done = app.readyCallback(); + * doAsync(done); + * } + * ``` + */ + loadCustomApp() { + this.getLoadUnits() + .forEach(unit => this.loadFile(path.join(unit.path, 'app.js'))); + }, + + /** + * Load agent.js, same as {@link EggLoader#loadCustomApp} + */ + loadCustomAgent() { + this.getLoadUnits() + .forEach(unit => this.loadFile(path.join(unit.path, 'agent.js'))); + }, + +}; diff --git a/lib/extend_loader.js b/lib/mixin/extend.js similarity index 59% rename from lib/extend_loader.js rename to lib/mixin/extend.js index 109a4ec5..7732cd3d 100644 --- a/lib/extend_loader.js +++ b/lib/mixin/extend.js @@ -3,72 +3,70 @@ const fs = require('fs'); const path = require('path'); const interopRequire = require('interop-require'); -const utils = require('./utils'); -const debug = require('debug')('egg:extend:loader'); - -const loadExtend = Symbol('loadExtend'); +const debug = require('debug')('egg-loader:extend'); +const utils = require('../utils'); module.exports = { /** * 扩展 Agent.prototype 的属性 * - * 可加载路径查看 {@link EggLoader#loadDirs} - * @method EggLoader#loadAgent + * 可加载路径查看 {@link EggLoader#getLoadUnits} + * @method EggLoader#loadAgentExtend */ - loadAgent() { - this[loadExtend]('agent', this.app); + loadAgentExtend() { + this.loadExtend('agent', this.app); }, /** * 扩展 Application.prototype 的属性 * - * 可加载路径查看 {@link EggLoader#loadDirs} - * @method EggLoader#loadApplication + * 可加载路径查看 {@link EggLoader#getLoadUnits} + * @method EggLoader#loadApplicationExtend */ - loadApplication() { - this[loadExtend]('application', this.app); + loadApplicationExtend() { + this.loadExtend('application', this.app); }, /** * 扩展 Request.prototype 的属性 * - * 可加载路径查看 {@link EggLoader#loadDirs} - * @method EggLoader#loadRequest + * 可加载路径查看 {@link EggLoader#getLoadUnits} + * @method EggLoader#loadRequestExtend */ - loadRequest() { - this[loadExtend]('request', this.app.request); + loadRequestExtend() { + this.loadExtend('request', this.app.request); }, /** * 扩展 Response.prototype 的属性 * - * 可加载路径查看 {@link EggLoader#loadDirs} - * @method EggLoader#loadResponse + * 可加载路径查看 {@link EggLoader#getLoadUnits} + * @method EggLoader#loadResponseExtend */ - loadResponse() { - this[loadExtend]('response', this.app.response); + loadResponseExtend() { + this.loadExtend('response', this.app.response); }, /** * 扩展 Context.prototype 的属性 * - * 可加载路径查看 {@link EggLoader#loadDirs} - * @method EggLoader#loadContext + * 可加载路径查看 {@link EggLoader#getLoadUnits} + * @method EggLoader#loadContextExtend */ - loadContext() { - this[loadExtend]('context', this.app.context); + loadContextExtend() { + this.loadExtend('context', this.app.context); }, /** * 扩展 app.Helper.prototype 的属性 * - * 可加载路径查看 {@link EggLoader#loadDirs} - * @method EggLoader#loadHelper + * 可加载路径查看 {@link EggLoader#getLoadUnits} + * @method EggLoader#loadHelperExtend */ - loadHelper() { + loadHelperExtend() { if (this.app && this.app.Helper) { - this[loadExtend]('helper', this.app.Helper.prototype); + this.loadExtend('helper', this.app.Helper.prototype); } }, @@ -80,13 +78,13 @@ module.exports = { * @param {Object} proto - 最终将属性合并到 proto 上 * @private */ - [loadExtend](name, proto) { + loadExtend(name, proto) { // 获取需要加载的文件 - const filepaths = this.loadDirs() - .map(dir => { - let pluginExtendsPath = path.join(dir, 'app/extend'); + const filepaths = this.getLoadUnits() + .map(unit => { + let pluginExtendsPath = path.join(unit.path, 'app/extend'); if (!fs.existsSync(pluginExtendsPath)) { - pluginExtendsPath = path.join(dir, 'app'); + pluginExtendsPath = path.join(unit.path, 'app'); } return path.join(pluginExtendsPath, name); }); diff --git a/lib/middleware_loader.js b/lib/mixin/middleware.js similarity index 73% rename from lib/middleware_loader.js rename to lib/mixin/middleware.js index e530c304..784b2740 100644 --- a/lib/middleware_loader.js +++ b/lib/mixin/middleware.js @@ -2,16 +2,19 @@ const join = require('path').join; const is = require('is-type-of'); -const debug = require('debug')('egg:loader:middleware'); +const debug = require('debug')('egg-loader:middleware'); const inspect = require('util').inspect; module.exports = { + /** - * 加载中间件,将中间件加载到 app.middleware,并根据 config 配置载入到上下文中 + * Load middleware * - * 中间件允许覆盖,优先级依次从上到下 + * app.config.xx is the options of the middleware xx that has same name as config * - * 中间件的规范写法为,options 是同名配置获取的 + * @method EggLoader#loadMiddleware + * @param {Object} opt LoaderOptions + * @example * ```js * // app/middleware/status.js * module.exports = function(options, app) { @@ -21,27 +24,23 @@ module.exports = { * } * } * ``` - * @method EggLoader#loadMiddleware - * @param {Object} opt - loading 参数 */ loadMiddleware(opt) { - const app = this.app; + + // load middleware to app.middleware opt = Object.assign({ - // 加载中间件,但是不调用它 call: false, override: true, lowercaseFirst: true, }, opt); - const middlewarePaths = this.loadDirs().map(dir => join(dir, 'app/middleware')); - + const middlewarePaths = this.getLoadUnits().map(unit => join(unit.path, 'app/middleware')); this.loadToApp(middlewarePaths, 'middlewares', opt); app.coreLogger.info('Use coreMiddleware order: %j', this.config.coreMiddleware); app.coreLogger.info('Use appMiddleware order: %j', this.config.appMiddleware); - // 将中间件加载到 koa 中 - // 通过 app.config.coreMiddleware, app.config.appMiddleware 配置的顺序加载 + // use middleware ordered by app.config.coreMiddleware and app.config.appMiddleware const middlewareNames = this.config.coreMiddleware.concat(this.config.appMiddleware); debug('middlewareNames: %j', middlewareNames); for (const name of middlewareNames) { diff --git a/lib/plugin_loader.js b/lib/mixin/plugin.js similarity index 93% rename from lib/plugin_loader.js rename to lib/mixin/plugin.js index fe9249a1..472af438 100644 --- a/lib/plugin_loader.js +++ b/lib/mixin/plugin.js @@ -2,23 +2,23 @@ const fs = require('fs'); const path = require('path'); -const debug = require('debug')('egg:loader:plugin'); -const interopRequire = require('interop-require'); - -const sequencify = require('./utils/sequencify'); +const debug = require('debug')('egg-loader:plugin'); +const sequencify = require('../utils/sequencify'); +const loadFile = require('../utils').loadFile; module.exports = { /** - * 根据配置加载插件,实现 loadPlugin() 接口 + * Load plugin.js + * * * 插件配置来自三个地方 * * 1. 应用 config/plugin.js,优先级最高 - * 2. egg/lib/core/config/plugin.js,优先级次之。 + * 2. egg/config/plugin.js,优先级次之。 * 3. 插件本身的 package.json => eggPlugin 配置,优先级最低 * - * 具体的插件配置类似 + * plugin.js is written below * * ```js * { @@ -30,7 +30,6 @@ module.exports = { * }, * // 简写 * 'rds': false, - * // 自定义路径,优先级最高 * 'depd': { * enable: true, * path: 'path/to/depd' @@ -42,7 +41,6 @@ module.exports = { * * 1. $APP_BASE/node_modules/${package or name} * 2. $EGG_BASE/node_modules/${package or name} - * 3. $EGG_BASE/lib/plugins/${package or name} * * 加载后可通过 `loader.plugins` 访问已开启的插件 * @@ -66,7 +64,7 @@ module.exports = { debug('Loaded app plugins: %j', Object.keys(appPlugins)); // 读取 eggPlugins,为框架和 egg 配置 - const eggPluginConfigPaths = this.eggPaths.map(eggPath => path.join(eggPath, 'lib/core/config/plugin.js')); + const eggPluginConfigPaths = this.eggPaths.map(eggPath => path.join(eggPath, 'config/plugin.js')); const eggPlugins = this.readPluginConfigs(eggPluginConfigPaths); debug('Loaded egg plugins: %j', Object.keys(eggPlugins)); @@ -164,7 +162,7 @@ module.exports = { continue; } - const config = interopRequire(configPath); + const config = loadFile(configPath); for (const name in config) { this.normalizePluginConfig(config, name); @@ -320,17 +318,13 @@ module.exports = { // 尝试在以下目录找到匹配的插件 // -> {appname}/node_modules // -> {framework}/node_modules - // -> {framework}/lib/plugins (plugin.name) - // -> egg/node_modules - // -> egg/lib/plugins (plugin.name) - // -> $CWD/node_modules + // -> $CWD/node_modules lookupDirs.push(path.join(this.options.baseDir, 'node_modules')); // 到 egg 中查找,优先从外往里查找 for (let i = this.eggPaths.length - 1; i >= 0; i--) { const eggPath = this.eggPaths[i]; lookupDirs.push(path.join(eggPath, 'node_modules')); - lookupDirs.push(path.join(eggPath, 'lib/plugins')); } // npm@3, 插件测试用例,还需要通过 $cwd/node_modules 目录获取 diff --git a/lib/mixin/proxy.js b/lib/mixin/proxy.js new file mode 100644 index 00000000..9929ca97 --- /dev/null +++ b/lib/mixin/proxy.js @@ -0,0 +1,30 @@ +'use strict'; + +const join = require('path').join; + +module.exports = { + + /** + * 加载 app/proxy 目录下的文件 + * + * 1. 加载应用 app/proxy + * 2. 加载插件 app/proxy + * + * @method EggLoader#loadProxy + * @param {Object} opt - loading 参数 + */ + loadProxy(opt) { + const app = this.app; + const arr = this.getLoadUnits().map(unit => join(unit.path, 'app/proxy')); + + opt = Object.assign({ + call: true, + lowercaseFirst: true, + fieldClass: 'proxyClasses', + }, opt); + this.loadToContext(arr, 'proxy', opt); + + app.coreLogger.info('[egg:loader] Proxy loaded from %j', arr); + }, + +}; diff --git a/lib/mixin/service.js b/lib/mixin/service.js new file mode 100644 index 00000000..2bf14b93 --- /dev/null +++ b/lib/mixin/service.js @@ -0,0 +1,30 @@ +'use strict'; + +const path = require('path'); + +module.exports = { + + /** + * 加载 app/service 目录下的文件 + * + * 1. 加载应用 app/service + * 2. 加载插件 app/service + * + * @method EggLoader#loadService + * @param {Object} opt - loading 参数 + */ + loadService(opt) { + const servicePaths = this.getLoadUnits().map(unit => { + return path.join(unit.path, 'app/service'); + }); + + // 载入到 app.serviceClasses + opt = Object.assign({ + call: true, + lowercaseFirst: true, + fieldClass: 'serviceClasses', + }, opt); + this.loadToContext(servicePaths, 'service', opt); + }, + +}; diff --git a/lib/proxy_loader.js b/lib/proxy_loader.js deleted file mode 100644 index c0c07ea7..00000000 --- a/lib/proxy_loader.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -const join = require('path').join; -const classLoader = Symbol('classLoader'); -const utils = require('./utils'); - -module.exports = { - - /** - * 加载 app/proxy 目录下的文件 - * - * 1. 加载应用 app/proxy - * 2. 加载插件 app/proxy - * - * @method EggLoader#loadProxy - * @param {Object} opt - loading 参数 - */ - loadProxy(opt) { - const app = this.app; - opt = Object.assign({ call: true, lowercaseFirst: true }, opt); - const arr = this.loadDirs().map(dir => join(dir, 'app/proxy')); - // load proxy classes to app.proxyClasses - this.loadToApp(arr, 'proxyClasses', opt); - - // this.proxy.demoQuery.getUser(uid) - Object.defineProperty(app.context, 'proxy', { - get() { - let loader = this[classLoader]; - if (!loader) { - this[classLoader] = loader = new this.app.ProxyClassLoader(this); - } - return loader; - }, - }); - - // { - // key1: { - // subkey1: SubProxy1, - // subkey2: { - // subkey21: SubProxy21, - // subkey22: SubProxy22, - // }, - // subkey3: SubProxy3, - // } - // } - app.ProxyClassLoader = utils.getClassLoader(app, 'proxy'); - - app.coreLogger.info('[egg:loader] Proxy loaded from %j', arr); - }, - -}; diff --git a/lib/service_loader.js b/lib/service_loader.js deleted file mode 100644 index a031de78..00000000 --- a/lib/service_loader.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -const path = require('path'); -const utils = require('./utils'); -const classLoader = Symbol('classLoader'); - -module.exports = { - - /** - * 加载 app/service 目录下的文件 - * - * 1. 加载应用 app/service - * 2. 加载插件 app/service - * - * @method EggLoader#loadService - * @param {Object} opt - loading 参数 - */ - loadService(opt) { - const app = this.app; - const servicePaths = this.loadDirs().map(dir => { - const servicePath = path.join(dir, 'app/service'); - return servicePath; - }); - - // 载入到 app.serviceClasses - opt = Object.assign({ call: false, lowercaseFirst: true }, opt); - this.loadToApp(servicePaths, 'serviceClasses', opt); - - /** - * 可以访问到当前应用配置的所有 service, - * service 目录约定在 `${baseDir}/app/service`。 - * @since 1.0.0 - * @member Context#service - */ - Object.defineProperty(app.context, 'service', { - get() { - let loader = this[classLoader]; - if (!loader) { - this[classLoader] = loader = new this.app.ServiceClassLoader(this); - } - return loader; - }, - }); - - // { - // key1: { - // subkey1: SubService1, - // subkey2: { - // subkey21: SubService21, - // subkey22: SubService22, - // }, - // subkey3: SubService3, - // } - // } - app.ServiceClassLoader = utils.getClassLoader(app, 'service'); - }, - -}; diff --git a/lib/utils/class_loader.js b/lib/utils/class_loader.js deleted file mode 100644 index 38519568..00000000 --- a/lib/utils/class_loader.js +++ /dev/null @@ -1,76 +0,0 @@ -'use strict'; - -const is = require('is-type-of'); - -module.exports = function createClassLoader(classes, subClasses) { - class ClassLoader { - constructor(ctx) { - this.ctx = ctx; - this._cache = new Map(); - } - - _getInstance(classname) { - let instance = this._cache.get(classname); - if (!instance) { - const Class = classes[classname]; - if (typeof Class === 'function' && !is.generatorFunction(Class)) { - // module.exports = class SubService extends Serivce - instance = new Class(this.ctx); - } else { - // 兼容模式 - // module.exports = { ... } - instance = Class; - } - this._cache.set(classname, instance); - } - return instance; - } - - // 支持子节点类加载,目前只支持最多2级节点 - // 只能一次性将此节点下的类都实例化出来 - _getSubClassInstance(rootName) { - let obj = this._cache.get(rootName); - if (obj) { - return obj; - } - obj = {}; - const map = subClasses[rootName]; - for (const sub1 in map) { - const Class = map[sub1]; - if (typeof Class === 'function') { - obj[sub1] = new Class(this.ctx); - } else { - for (const sub2 in Class) { - const Class2 = Class[sub2]; - if (!obj[sub1]) { - obj[sub1] = {}; - } - obj[sub1][sub2] = new Class2(this.ctx); - } - } - } - this._cache.set(rootName, obj); - return obj; - } - } - - Object.keys(classes).forEach(function(classname) { - Object.defineProperty(ClassLoader.prototype, classname, { - get() { - return this._getInstance(classname); - }, - }); - }); - - if (subClasses) { - Object.keys(subClasses).forEach(function(rootName) { - Object.defineProperty(ClassLoader.prototype, rootName, { - get() { - return this._getSubClassInstance(rootName); - }, - }); - }); - } - - return ClassLoader; -}; diff --git a/lib/utils/index.js b/lib/utils/index.js index 4f889165..da0fd335 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1,11 +1,25 @@ 'use strict'; -const is = require('is-type-of'); -const deprecate = require('depd')('egg-loader'); -const createClassLoader = require('./class_loader'); +const interopRequire = require('interop-require'); module.exports = exports = { + /** + * require a file + * @param {String} filepath fullpath + * @return {Object} exports + */ + loadFile(filepath) { + let exports; + try { + exports = interopRequire(filepath); + } catch (err) { + err.message = 'load file: ' + filepath + ', error: ' + err.message; + throw err; + } + return exports; + }, + /** * 判断模块是否存在 * @method Util#existsModule @@ -30,96 +44,4 @@ module.exports = exports = { return process.env.HOME || '/home/admin'; }, - // 遍历 - walk(app, classes, fn, path) { - path = path || []; - Object.keys(classes).forEach(key => { - let target = classes[key]; - const keys = [].concat(path, key); - - if (target === undefined || target === null) { - return undefined; - } - - // module.exports = class xxService extends Service {} - if (is.class(target)) { - return fn(target, keys); - } - - // 兼容模式: module.exports = function*() {} - if (is.generatorFunction(target)) { - return fn(target, keys); - } - - // 兼容模式: module.exports = function (app) {} - if (typeof target === 'function') { - // 自动调用一次 - target = target(app); - return fn(target, keys); - } - - // 判断是否是 exports.get = function* () {} 结构 - let hasGenerator = false; - for (const fnName in target) { - if (is.generatorFunction(target[fnName])) { - hasGenerator = true; - break; - } - } - - if (hasGenerator) { - return fn(target, keys); - } - - return module.exports.walk(app, target, fn, keys); - }); - }, - - /** - * 获取对应的 classloader - * @param {Object} app - app对象 - * @param {String} type - 要加载的类型, proxy / service - * @return {Function} 返回对应的 classloader - */ - getClassLoader(app, type) { - const targetClasses = app[type + 'Classes']; - const subClasses = {}; - - // hook to subServiceClasses / subProxyClasses, will be used in mm.mockService - const subClassesName = 'sub' + type[0].toUpperCase() + type.substring(1) + 'Classes'; - app[subClassesName] = subClasses; - - exports.walk(app, targetClasses, (target, keys) => { - const first = keys[0]; - if (keys.length === 1) { - targetClasses[first] = target; - return; - } - - if (keys.length > 3) { - deprecate(`不再支持超过 2 级子目录的 ${type} 加载,最长只到 ${first}.${keys[1]}.${keys[2]}`); - return; - } - - // 有两层或者三层的情况 - delete targetClasses[first]; - - // 最后一层的值是target - const last = keys.pop(); - - // keys为对象路径,首先依次查看subClasses下是否有,如果没有赋值空对象 - let classes = subClasses; - for (const key of keys) { - if (!classes[key]) { - classes[key] = {}; - } - classes = classes[key]; - } - - classes[last] = target; - }); - - return createClassLoader(targetClasses, subClasses); - }, - }; diff --git a/test/egg_loader.test.js b/test/egg_loader.test.js index d1fd5b8a..61ee6017 100644 --- a/test/egg_loader.test.js +++ b/test/egg_loader.test.js @@ -1,118 +1,10 @@ 'use strict'; require('should'); -const path = require('path'); const mm = require('mm'); -const utils = require('./utils'); -const BaseLoader = require('../index'); describe('test/egg_loader.test.js', function() { afterEach(mm.restore); - describe('.getServerEnv', function() { - - it('should get from env EGG_SERVER_ENV', function() { - mm(process.env, 'EGG_SERVER_ENV', 'prod'); - const app = utils.createApp('serverenv'); - app.loader.serverEnv.should.equal('prod'); - }); - - it('should use unittest when NODE_ENV = test', function() { - mm(process.env, 'NODE_ENV', 'test'); - const app = utils.createApp('serverenv'); - app.loader.serverEnv.should.equal('unittest'); - }); - - it('should use default when NODE_ENV = production', function() { - mm(process.env, 'NODE_ENV', 'production'); - const app = utils.createApp('serverenv'); - app.loader.serverEnv.should.equal('default'); - }); - - it('should use local when NODE_ENV is other', function() { - mm(process.env, 'NODE_ENV', 'development'); - const app = utils.createApp('serverenv'); - app.loader.serverEnv.should.equal('local'); - }); - - it('should get from config/serverEnv', function() { - mm(process.env, 'NODE_ENV', 'production'); - mm(process.env, 'EGG_SERVER_ENV', 'test'); - const app = utils.createApp('serverenv-file'); - app.loader.serverEnv.should.equal('prod'); - }); - }); - - - describe('eggPaths', function() { - - it('should get from paramter', function() { - const app = utils.createApp('eggpath'); - app.loader.eggPath.should.equal(utils.getFilepath('egg')); - }); - - it('should get from framework', function() { - const Application = require('./fixtures/framework'); - const app = new Application(); - app.coreLogger = console; - app.loader = new utils.Loader('eggpath', { app }); - app.loader.loadConfig(); - app.loader.eggPaths.should.eql([ - utils.getFilepath('egg'), - utils.getFilepath('framework/node_modules/framework2'), - utils.getFilepath('framework'), - ]); - return app; - }); - - it('should get from framework using symbol', function() { - const Application = require('./fixtures/framework-symbol'); - const app = new Application(); - app.coreLogger = console; - app.loader = new utils.Loader('eggpath', { app }); - app.loader.loadConfig(); - app.loader.eggPaths.should.eql([ - utils.getFilepath('egg'), - utils.getFilepath('framework-symbol/node_modules/framework2'), - utils.getFilepath('framework-symbol'), - ]); - return app; - }); - - it('frameworkPaths should not container eggPath', function() { - const eggPath = path.join(__dirname, 'fixtures/egg'); - const loader = new BaseLoader({ - baseDir: path.join(__dirname, 'fixtures/eggpath'), - eggPath, - customEgg: eggPath, - }); - loader.frameworkPaths.should.not.containEql(eggPath); - }); - }); - - - describe('loadDirs', function() { - - it('should get plugin dir', function() { - const app = utils.createApp('plugin'); - const dirs = app.loader.loadDirs(); - dirs.length.should.eql(10); - }); - - it('should not get plugin dir', function() { - const loader = new utils.Loader('plugin'); - const dirs = loader.loadDirs(); - dirs.length.should.eql(2); - }); - }); - - - describe('loadFile', function() { - it('should throw with filepath when file syntax error', function() { - (function() { - utils.createApp('syntaxerror'); - }).should.throw(/test\/fixtures\/syntaxerror\/app\.js error:/); - }); - }); }); diff --git a/test/fixtures/app-core-middleware/config/config.default.js b/test/fixtures/app-core-middleware/config/config.default.js new file mode 100644 index 00000000..cfa46aeb --- /dev/null +++ b/test/fixtures/app-core-middleware/config/config.default.js @@ -0,0 +1 @@ +exports.coreMiddleware = []; diff --git a/test/fixtures/app-core-middleware/package.json b/test/fixtures/app-core-middleware/package.json new file mode 100644 index 00000000..0b5f2497 --- /dev/null +++ b/test/fixtures/app-core-middleware/package.json @@ -0,0 +1,3 @@ +{ + "name": "coreMiddleware" +} diff --git a/test/fixtures/app-noname/package.json b/test/fixtures/app-noname/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/test/fixtures/app-noname/package.json @@ -0,0 +1 @@ +{} diff --git a/test/fixtures/appname/package.json b/test/fixtures/appname/package.json new file mode 100644 index 00000000..5a642d78 --- /dev/null +++ b/test/fixtures/appname/package.json @@ -0,0 +1,3 @@ +{ + "name": "appname" +} diff --git a/test/fixtures/custom-app/app/router.js b/test/fixtures/custom-app/app/router.js deleted file mode 100644 index 6c206ce5..00000000 --- a/test/fixtures/custom-app/app/router.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -module.exports = function(app) { - app.get('/', function*() { - this.body = { - customFoo: app.customFoo, - env: app.config.env, - eggPaths: app.loader.eggPaths, - frameworkPaths: app.loader.frameworkPaths, - eggPath: app.loader.eggPath, - }; - }); -}; diff --git a/test/fixtures/custom-app/package.json b/test/fixtures/custom-app/package.json deleted file mode 100644 index 42e91811..00000000 --- a/test/fixtures/custom-app/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "custom-app" -} diff --git a/test/fixtures/custom-framework/index.js b/test/fixtures/custom-framework/index.js deleted file mode 100644 index 174cbddd..00000000 --- a/test/fixtures/custom-framework/index.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -module.exports = require('../../../'); -const startCluster = module.exports.startCluster; - -module.exports.startCluster = function(options) { - options.customEgg = __dirname; - return startCluster(options); -}; diff --git a/test/fixtures/custom-framework/lib/core/config/plugin.js b/test/fixtures/custom-framework/lib/core/config/plugin.js deleted file mode 100644 index 6dc7cea3..00000000 --- a/test/fixtures/custom-framework/lib/core/config/plugin.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -exports.foo = { - enable: true, - name: 'foo', -}; - -exports.depd = { - enable: false, -}; diff --git a/test/fixtures/custom-framework/lib/plugins/foo/app.js b/test/fixtures/custom-framework/lib/plugins/foo/app.js deleted file mode 100644 index 358f65b7..00000000 --- a/test/fixtures/custom-framework/lib/plugins/foo/app.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = function(app) { - app.customFoo = true; -}; diff --git a/test/fixtures/custom-framework/lib/plugins/foo/package.json b/test/fixtures/custom-framework/lib/plugins/foo/package.json deleted file mode 100644 index bde99de9..00000000 --- a/test/fixtures/custom-framework/lib/plugins/foo/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "foo" -} diff --git a/test/fixtures/custom-framework/package.json b/test/fixtures/custom-framework/package.json deleted file mode 100644 index 811cc964..00000000 --- a/test/fixtures/custom-framework/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "custom-framework" -} diff --git a/test/fixtures/egg/app/extend/application.js b/test/fixtures/egg/app/extend/application.js new file mode 100644 index 00000000..70acefc4 --- /dev/null +++ b/test/fixtures/egg/app/extend/application.js @@ -0,0 +1,11 @@ +'use strict'; + +const symbol = require('../../../../utils').symbol; + +module.exports = { + Proxy: require('../../proxy'), + Service: require('../../service'), + get [symbol.view]() { + return 'egg'; + } +}; diff --git a/test/fixtures/egg/lib/core/app/middleware/status.js b/test/fixtures/egg/app/middleware/status.js similarity index 100% rename from test/fixtures/egg/lib/core/app/middleware/status.js rename to test/fixtures/egg/app/middleware/status.js diff --git a/test/fixtures/egg/lib/core/config/config.default.js b/test/fixtures/egg/config/config.default.js similarity index 100% rename from test/fixtures/egg/lib/core/config/config.default.js rename to test/fixtures/egg/config/config.default.js diff --git a/test/fixtures/egg/lib/core/config/config.unittest.js b/test/fixtures/egg/config/config.unittest.js similarity index 100% rename from test/fixtures/egg/lib/core/config/config.unittest.js rename to test/fixtures/egg/config/config.unittest.js diff --git a/test/fixtures/egg/config/plugin.js b/test/fixtures/egg/config/plugin.js new file mode 100644 index 00000000..47ea0a01 --- /dev/null +++ b/test/fixtures/egg/config/plugin.js @@ -0,0 +1,30 @@ +'use strict'; + +const path = require('path'); + +module.exports = { + session: { + enable: true, + path: path.join(__dirname, '../node_modules/session'), + }, + + hsfclient: { + enable: false, + path: path.join(__dirname, '../plugins/hsfclient'), + }, + + configclient: { + enable: false, + path: path.join(__dirname, '../plugins/configclient'), + }, + + eagleeye: { + enable: false, + path: path.join(__dirname, '../plugins/eagleeye'), + }, + + diamond: { + enable: false, + path: path.join(__dirname, '../plugins/diamond'), + }, +}; diff --git a/test/fixtures/egg/index.js b/test/fixtures/egg/index.js new file mode 100644 index 00000000..a08983aa --- /dev/null +++ b/test/fixtures/egg/index.js @@ -0,0 +1,12 @@ +'use strict'; + +const path = require('path'); +const KoaApplication = require('koa'); + +class EggApplication extends KoaApplication { + get [Symbol.for('egg#eggPath')]() { + return __dirname; + } +} + +module.exports = EggApplication; diff --git a/test/fixtures/egg/lib/core/app/extend/application.js b/test/fixtures/egg/lib/core/app/extend/application.js deleted file mode 100644 index 93b2c6dd..00000000 --- a/test/fixtures/egg/lib/core/app/extend/application.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -const symbol = require('../../../../../../utils').symbol; - -module.exports = { - Proxy: require('../../../../proxy'), - Service: require('../../../../service'), - get [symbol.view]() { - return 'egg'; - } -}; diff --git a/test/fixtures/egg/lib/core/config/plugin.js b/test/fixtures/egg/lib/core/config/plugin.js deleted file mode 100644 index 1c0d246c..00000000 --- a/test/fixtures/egg/lib/core/config/plugin.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -const path = require('path'); - -module.exports = { - session: { - enable: true, - path: path.join(__dirname, '../../../node_modules/session'), - }, - - hsfclient: { - enable: false, - path: path.join(__dirname, '../../../plugins/hsfclient'), - }, - - configclient: { - enable: false, - path: path.join(__dirname, '../../../plugins/configclient'), - }, - - eagleeye: { - enable: false, - path: path.join(__dirname, '../../../plugins/eagleeye'), - }, - - diamond: { - enable: false, - path: path.join(__dirname, '../../../plugins/diamond'), - }, -}; diff --git a/test/fixtures/egg/package.json b/test/fixtures/egg/package.json new file mode 100644 index 00000000..6697ad3f --- /dev/null +++ b/test/fixtures/egg/package.json @@ -0,0 +1,3 @@ +{ + "name": "egg" +} diff --git a/test/fixtures/framework-symbol/lib/core/config/config.js b/test/fixtures/framework-symbol/config/config.js similarity index 100% rename from test/fixtures/framework-symbol/lib/core/config/config.js rename to test/fixtures/framework-symbol/config/config.js diff --git a/test/fixtures/framework-symbol/node_modules/framework2/lib/core/config/config.js b/test/fixtures/framework-symbol/node_modules/framework2/config/config.js similarity index 100% rename from test/fixtures/framework-symbol/node_modules/framework2/lib/core/config/config.js rename to test/fixtures/framework-symbol/node_modules/framework2/config/config.js diff --git a/test/fixtures/framework/index.js b/test/fixtures/framework/index.js deleted file mode 100644 index 19d91e50..00000000 --- a/test/fixtures/framework/index.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -const Application = require('framework2'); - -class Framework extends Application { - - constructor(options) { - super(options); - } - - get [Symbol.for('egg#eggPath')]() { - return __dirname; - } -} - -module.exports = Framework; diff --git a/test/fixtures/framework/lib/core/config/config.js b/test/fixtures/framework/lib/core/config/config.js deleted file mode 100644 index 1e2c0832..00000000 --- a/test/fixtures/framework/lib/core/config/config.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - framework1: 'framework1', -}; diff --git a/test/fixtures/framework/node_modules/framework2/index.js b/test/fixtures/framework/node_modules/framework2/index.js deleted file mode 100644 index 7a2763c2..00000000 --- a/test/fixtures/framework/node_modules/framework2/index.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -const Application = require('koa'); - -class Framework2 extends Application { - - constructor(options) { - super(options); - } - - get [Symbol.for('egg#eggPath')]() { - return __dirname; - } -} - -module.exports = Framework2; diff --git a/test/fixtures/framework/node_modules/framework2/lib/core/config/config.js b/test/fixtures/framework/node_modules/framework2/lib/core/config/config.js deleted file mode 100644 index b024831a..00000000 --- a/test/fixtures/framework/node_modules/framework2/lib/core/config/config.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - framework2: 'framework2', -}; diff --git a/test/fixtures/framework/node_modules/framework2/package.json b/test/fixtures/framework/node_modules/framework2/package.json deleted file mode 100644 index eca4a5fe..00000000 --- a/test/fixtures/framework/node_modules/framework2/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "framework2" -} diff --git a/test/fixtures/framework/package.json b/test/fixtures/framework/package.json deleted file mode 100644 index b5d2dde7..00000000 --- a/test/fixtures/framework/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "framework" -} diff --git a/test/fixtures/load_file/function.js b/test/fixtures/load_file/function.js new file mode 100644 index 00000000..b1157b9a --- /dev/null +++ b/test/fixtures/load_file/function.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = (a, b) => [ a, b ]; diff --git a/test/fixtures/load_file/obj.js b/test/fixtures/load_file/obj.js new file mode 100644 index 00000000..d0bcc4db --- /dev/null +++ b/test/fixtures/load_file/obj.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = { a: 1 }; diff --git a/test/fixtures/load_file/package.json b/test/fixtures/load_file/package.json new file mode 100644 index 00000000..f22b07a8 --- /dev/null +++ b/test/fixtures/load_file/package.json @@ -0,0 +1,3 @@ +{ + "name": "load_file" +} diff --git a/test/fixtures/plugin/node_modules/d/config/config.js b/test/fixtures/plugin/node_modules/d/config/config.js index 1e2f2ee9..e69de29b 100644 --- a/test/fixtures/plugin/node_modules/d/config/config.js +++ b/test/fixtures/plugin/node_modules/d/config/config.js @@ -1,5 +0,0 @@ -exports.proxy = { - -}; - -exports.middleware = ['d']; diff --git a/test/fixtures/plugin/plugin-middleware/config/config.default.js b/test/fixtures/plugin/plugin-middleware/config/config.default.js new file mode 100644 index 00000000..6a96b0a1 --- /dev/null +++ b/test/fixtures/plugin/plugin-middleware/config/config.default.js @@ -0,0 +1 @@ +exports.middleware = []; diff --git a/test/fixtures/plugin/plugin-middleware/package.json b/test/fixtures/plugin/plugin-middleware/package.json new file mode 100644 index 00000000..421531de --- /dev/null +++ b/test/fixtures/plugin/plugin-middleware/package.json @@ -0,0 +1,5 @@ +{ + "eggPlugin": { + "name": "middleware" + } +} diff --git a/test/fixtures/plugin/plugin-proxy/config/config.default.js b/test/fixtures/plugin/plugin-proxy/config/config.default.js new file mode 100644 index 00000000..547bf0c9 --- /dev/null +++ b/test/fixtures/plugin/plugin-proxy/config/config.default.js @@ -0,0 +1 @@ +exports.proxy = {}; diff --git a/test/fixtures/plugin/plugin-proxy/package.json b/test/fixtures/plugin/plugin-proxy/package.json new file mode 100644 index 00000000..9f9b14e3 --- /dev/null +++ b/test/fixtures/plugin/plugin-proxy/package.json @@ -0,0 +1,5 @@ +{ + "eggPlugin": { + "name": "proxy" + } +} diff --git a/test/fixtures/subdir-proxy/app/router.js b/test/fixtures/subdir-proxy/app/router.js index c0bd5f62..ce1e3339 100644 --- a/test/fixtures/subdir-proxy/app/router.js +++ b/test/fixtures/subdir-proxy/app/router.js @@ -13,4 +13,4 @@ module.exports = function (app) { oldStyle: yield this.proxy.oldStyle.url(this), }; }); -} \ No newline at end of file +} diff --git a/test/fixtures/subdir-services/app/controller/home.js b/test/fixtures/subdir-services/app/controller/home.js index 2c551a74..7a625bf1 100644 --- a/test/fixtures/subdir-services/app/controller/home.js +++ b/test/fixtures/subdir-services/app/controller/home.js @@ -5,7 +5,7 @@ module.exports = function* () { bar1: yield this.service.foo.bar.get('bar1name'), bar2: yield this.service.foo.subdir.bar.get('bar2name'), 'foo.subdir2.sub2': yield this.service.foo.subdir2.sub2.get('bar3name'), - subdir11bar: !!this.service.foo.subdir1, + subdir11bar: yield this.service.foo.subdir1.subdir11.bar.get(), ok: yield this.service.ok.get(), cmd: yield this.service.certifyPersonal.mobileHi.doCertify.exec('hihi'), serviceIsSame: this.service.certifyPersonal === this.service.certifyPersonal, diff --git a/test/get_appname.test.js b/test/get_appname.test.js new file mode 100644 index 00000000..5d96fd25 --- /dev/null +++ b/test/get_appname.test.js @@ -0,0 +1,30 @@ +'use strict'; + +require('should'); +const mm = require('mm'); +const utils = require('./utils'); +const Loader = require('../lib/egg_loader'); +const EggApplication = require('./fixtures/egg'); + +describe('test/get_appname.test.js', function() { + + afterEach(mm.restore); + + it('should get appname', function() { + const loader = new Loader({ + baseDir: utils.getFilepath('appname'), + app: new EggApplication(), + }); + loader.getAppname().should.eql('appname'); + }); + + it('should throw when appname is not found', function() { + const pkg = utils.getFilepath('app-noname/package.json'); + (function() { + new Loader({ + baseDir: utils.getFilepath('app-noname'), + app: new EggApplication(), + }); + }).should.throw(`name is required from ${pkg}`); + }); +}); diff --git a/test/get_framework_paths.test.js b/test/get_framework_paths.test.js new file mode 100644 index 00000000..0b68bfa0 --- /dev/null +++ b/test/get_framework_paths.test.js @@ -0,0 +1,62 @@ +'use strict'; + +require('should'); +const mm = require('mm'); +const utils = require('./utils'); +const Loader = require('../lib/egg_loader'); +const EggApplication = require('./fixtures/egg'); +const KoaApplication = require('koa'); + +describe('test/get_framework_paths.test.js', function() { + + afterEach(mm.restore); + + it('should get from paramter', function() { + const loader = new Loader({ + baseDir: utils.getFilepath('eggpath'), + app: new EggApplication(), + }); + loader.eggPaths.should.eql([ utils.getFilepath('egg') ]); + }); + + it('should get from framework using symbol', function() { + const Application = require('./fixtures/framework-symbol'); + const loader = new Loader({ + baseDir: utils.getFilepath('eggpath'), + app: new Application(), + }); + loader.eggPaths.should.eql([ + utils.getFilepath('framework-symbol/node_modules/framework2'), + utils.getFilepath('framework-symbol'), + ]); + }); + + it('should throw when Application not extends koa', () => { + class Application { + get [Symbol.for('egg#eggPath')]() { + return __dirname; + } + } + (function() { + new Loader({ + baseDir: utils.getFilepath('eggpath'), + app: new Application(), + }); + }).should.throw('Symbol.for(\'egg#eggPath\') is required on Application'); + }); + + it('should throw when one of the Application do not specify symbol', () => { + class Application extends KoaApplication {} + class Application2 extends Application { + get [Symbol.for('egg#eggPath')]() { + return __dirname; + } + } + (function() { + new Loader({ + baseDir: utils.getFilepath('eggpath'), + app: new Application2(), + }); + }).should.throw('Symbol.for(\'egg#eggPath\') is required on Application'); + }); +}); diff --git a/test/get_load_units.test.js b/test/get_load_units.test.js new file mode 100644 index 00000000..a132be65 --- /dev/null +++ b/test/get_load_units.test.js @@ -0,0 +1,36 @@ +'use strict'; + +require('should'); +const mm = require('mm'); +const utils = require('./utils'); +const Loader = require('../lib/egg_loader'); +const EggApplication = require('./fixtures/egg'); + +describe('test/get_load_units.test.js', function() { + + afterEach(mm.restore); + + it('should get plugin dir', function() { + const loader = new Loader({ + baseDir: utils.getFilepath('plugin'), + app: new EggApplication(), + }); + loader.loadPlugin(); + const units = loader.getLoadUnits(); + units.length.should.eql(10); + units[8].type.should.eql('framework'); + units[8].path.should.eql(utils.getFilepath('egg')); + units[9].type.should.eql('app'); + units[9].path.should.eql(utils.getFilepath('plugin')); + }); + + it('should not get plugin dir', function() { + const loader = new Loader({ + baseDir: utils.getFilepath('plugin'), + app: new EggApplication(), + }); + const units = loader.getLoadUnits(); + units.length.should.eql(2); + }); + +}); diff --git a/test/get_server_env.test.js b/test/get_server_env.test.js new file mode 100644 index 00000000..a6c99fc1 --- /dev/null +++ b/test/get_server_env.test.js @@ -0,0 +1,42 @@ +'use strict'; + +require('should'); +const mm = require('mm'); +const utils = require('./utils'); + +describe('test/get_server_env.test.js', function() { + + afterEach(mm.restore); + + it('should get from env EGG_SERVER_ENV', function() { + mm(process.env, 'EGG_SERVER_ENV', 'prod'); + const app = utils.createApp('serverenv'); + app.loader.serverEnv.should.equal('prod'); + }); + + it('should use unittest when NODE_ENV = test', function() { + mm(process.env, 'NODE_ENV', 'test'); + const app = utils.createApp('serverenv'); + app.loader.serverEnv.should.equal('unittest'); + }); + + it('should use default when NODE_ENV = production', function() { + mm(process.env, 'NODE_ENV', 'production'); + const app = utils.createApp('serverenv'); + app.loader.serverEnv.should.equal('default'); + }); + + it('should use local when NODE_ENV is other', function() { + mm(process.env, 'NODE_ENV', 'development'); + const app = utils.createApp('serverenv'); + app.loader.serverEnv.should.equal('local'); + }); + + it('should get from config/serverEnv', function() { + mm(process.env, 'NODE_ENV', 'production'); + mm(process.env, 'EGG_SERVER_ENV', 'test'); + const app = utils.createApp('serverenv-file'); + app.loader.serverEnv.should.equal('prod'); + }); + +}); diff --git a/test/load_config.test.js b/test/load_config.test.js index f07b241d..cf7886dc 100644 --- a/test/load_config.test.js +++ b/test/load_config.test.js @@ -1,6 +1,7 @@ 'use strict'; const should = require('should'); +const utils = require('./utils'); const Loader = require('./utils').Loader; describe('test/load_config.test.js', function() { @@ -48,12 +49,39 @@ describe('test/load_config.test.js', function() { should.not.exists(loader.config.pluginA); }); - it('should delete config.middleware and config.proxy', function() { - const loader = new Loader('plugin'); - loader.loadConfig(); - should.not.exists(loader.config.proxy); - loader.config.coreMiddleware.should.not.containEql('d'); - loader.config.appMiddleware.should.not.containEql('d'); + it('should throw when plugin define middleware', function() { + const loader = new Loader('plugin', { + plugins: { + middleware: { + enable: true, + path: utils.getFilepath('plugin/plugin-middleware'), + }, + }, + }); + (function() { + loader.loadConfig(); + }).should.throw('Can not define middleware in framework or plugin'); + }); + + it('should throw when plugin define proxy', function() { + const loader = new Loader('plugin', { + plugins: { + proxy: { + enable: true, + path: utils.getFilepath('plugin/plugin-proxy'), + }, + }, + }); + (function() { + loader.loadConfig(); + }).should.throw('Can not define proxy in framework or plugin'); + }); + + it('should throw when app define coreMiddleware', function() { + const loader = new Loader('app-core-middleware'); + (function() { + loader.loadConfig(); + }).should.throw('Can not define coreMiddleware in app or plugin'); }); it('should read appinfo from the function of config', function() { diff --git a/test/load_custom_application.test.js b/test/load_custom_app.test.js similarity index 100% rename from test/load_custom_application.test.js rename to test/load_custom_app.test.js diff --git a/test/load_extend.test.js b/test/load_extend.test.js index 4b842486..be58a0fd 100644 --- a/test/load_extend.test.js +++ b/test/load_extend.test.js @@ -90,7 +90,7 @@ describe('test/load_extend.test.js', function() { }); loader.loadConfig(); loader.load(); - }).should.throw(/load_context_syntax_error\/app\/extend\/context\.js error: Unexpected token \)/); + }).should.throw(/ error: Unexpected token/); }); it('should extend symbol', function() { diff --git a/test/load_file.test.js b/test/load_file.test.js new file mode 100644 index 00000000..1174ba62 --- /dev/null +++ b/test/load_file.test.js @@ -0,0 +1,36 @@ +'use strict'; + +require('should'); +const mm = require('mm'); +const utils = require('./utils'); +const Loader = require('../lib/egg_loader'); +const EggApplication = require('./fixtures/egg'); + +describe('test/load_file.test.js', function() { + + afterEach(mm.restore); + + it('should load file', function() { + const loader = new Loader({ + baseDir: utils.getFilepath('load_file'), + app: new EggApplication(), + }); + loader.loadFile(utils.getFilepath('load_file/obj.js')).should.eql({ a: 1 }); + }); + + it('should load file when exports is function', function() { + const loader = new Loader({ + baseDir: utils.getFilepath('load_file'), + app: new EggApplication(), + }); + loader.loadFile(utils.getFilepath('load_file/function.js'), 1, 2).should.eql([ 1, 2 ]); + }); + + it('should throw with filepath when file syntax error', function() { + const filepath = utils.getFilepath('syntaxerror/app.js'); + (function() { + utils.createApp('syntaxerror'); + }).should.throw(`load file: ${filepath}, error: Unexpected token )`); + }); + +}); diff --git a/test/load_middleware.test.js b/test/load_middleware.test.js index cb801241..7a50fc4d 100644 --- a/test/load_middleware.test.js +++ b/test/load_middleware.test.js @@ -19,10 +19,10 @@ describe('test/load_middleware.test.js', function() { app.middlewares.should.not.have.property('a'); }); - it('should override middlewares of egg by plugin', function(done) { + it('should override middlewares of plugin by framework', function(done) { request(app.callback()) .get('/status') - .expect('status') + .expect('egg status') .end(done); }); diff --git a/test/load_plugin.test.js b/test/load_plugin.test.js index 8c159e73..7bc87e63 100644 --- a/test/load_plugin.test.js +++ b/test/load_plugin.test.js @@ -35,13 +35,6 @@ describe('test/load_plugin.test.js', function() { env: [], path: path.join(baseDir, 'plugins/e'), }); - // loader.plugins.onerror.should.eql({ - // enable: true, - // name: 'onerror', - // dep: [], - // env: [], - // path: path.join(utils.eggPath, 'lib/plugins/onerror'), - // }); loader.orderPlugins.should.be.an.Array; }); @@ -309,25 +302,6 @@ describe('test/load_plugin.test.js', function() { }); }); - it('should load multi framework', function() { - const customEgg = utils.getFilepath('custom-framework'); - const loader = new Loader('custom-app', { - customEgg, - }); - loader.loadConfig(); - - console.log(loader.plugins); - loader.plugins.foo.should.eql({ - name: 'foo', - enable: true, - dep: [], - env: [], - path: path.join(customEgg, 'lib/plugins/foo'), - }); - - should.not.exists(loader.plugins.depd); - }); - it('should load when all plugins are disabled', function() { const loader = new Loader('noplugin'); loader.loadConfig(); diff --git a/test/load_proxy.test.js b/test/load_proxy.test.js index f1a00e4c..7d0112be 100644 --- a/test/load_proxy.test.js +++ b/test/load_proxy.test.js @@ -58,7 +58,7 @@ describe('test/load_proxy.test.js', function() { name: 'bar3name', bar: 'bar3', }, - subdir11bar: false, + subdir11bar: true, ok: { ok: true, }, diff --git a/test/load_service.test.js b/test/load_service.test.js index a0fa0350..6d55760e 100644 --- a/test/load_service.test.js +++ b/test/load_service.test.js @@ -10,7 +10,6 @@ describe('test/load_service.test.js', function() { it('should load from application and plugin', function(done) { const app = utils.createApp('plugin'); - console.log(app.serviceClasses); should.exists(app.serviceClasses.foo); should.exists(app.serviceClasses.foo2); should.not.exists(app.serviceClasses.bar1); @@ -79,7 +78,9 @@ describe('test/load_service.test.js', function() { name: 'bar3name', bar: 'bar3', }, - subdir11bar: false, + subdir11bar: { + bar: 'bar111', + }, ok: { ok: true, }, diff --git a/test/loader.test.js b/test/loader.test.js index 506fa5df..4a600fc3 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -184,7 +184,7 @@ describe('test/loader.test.js', () => { directory: path.join(dirBase, 'syntax_error'), target: app.model, }).load(); - }).should.throw(/load file: .*?test\/fixtures\/load_dirs\/syntax_error\/error\.js, error:/); + }).should.throw(/ error: Unexpected identifier/); }); it('should throw when directory contains dot', () => { diff --git a/test/utils.js b/test/utils.js index aec7af1b..bf4a67d5 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,16 +1,24 @@ 'use strict'; const path = require('path'); -const koa = require('koa'); +const KoaApplication = require('koa'); const Router = require('koa-router'); const BaseLoader = require('..'); +class EggApplication extends KoaApplication { + get [Symbol.for('egg#eggPath')]() { + return path.join(__dirname, 'fixtures/egg'); + } +} + class TestLoader extends BaseLoader { constructor(name, options) { options = options || {}; + if (!options.app) { + options.app = new EggApplication(); + } options.baseDir = path.join(__dirname, 'fixtures', name); - options.eggPath = path.join(__dirname, 'fixtures/egg'); super(options); } @@ -20,11 +28,11 @@ class TestLoader extends BaseLoader { } load() { - this.loadApplication(); - this.loadRequest(); - this.loadResponse(); - this.loadContext(); - this.loadHelper(); + this.loadApplicationExtend(); + this.loadRequestExtend(); + this.loadResponseExtend(); + this.loadContextExtend(); + this.loadHelperExtend(); this.loadCustomApp(); this.loadProxy(); @@ -51,7 +59,7 @@ module.exports = { createApp(name, options) { options = options || {}; - const app = koa(); + const app = new EggApplication(); options.app = app; app.coreLogger = console; app.loader = new this.Loader(name, options); @@ -64,14 +72,14 @@ module.exports = { createAgent(name, options) { options = options || {}; - const agent = {}; + const agent = new EggApplication(); options.app = agent; agent.coreLogger = console; agent.loader = new this.Loader(name, options); agent.loader.loadConfig(); agent.config = agent.loader.config; agent.antx = agent.loader.antx; - agent.loader.loadAgent(); + agent.loader.loadAgentExtend(); agent.loader.loadCustomAgent(); return agent; },