'use strict'; const fs = require('fs'); const path = require('path'); const assert = require('assert'); const is = require('is-type-of'); const debug = require('debug')('ee-core:EeLoader'); const FileLoader = require('./file_loader'); const ContextLoader = require('./context_loader'); const Utils = require('../utils'); const Timing = require('../utils/timing'); const Ps = require('../../../ps'); const REQUIRE_COUNT = Symbol('EeLoader#requireCount'); class EeLoader { /** * @class * @param {Object} options - options * @param {String} options.baseDir - the directory of application * @param {EeCore} options.app - Application instance * @param {Logger} options.logger - logger * @since 1.0.0 */ constructor(options) { this.options = options; // assert(fs.existsSync(this.options.baseDir), `${this.options.baseDir} not exists`); assert(this.options.app, 'options.app is required'); assert(this.options.logger, 'options.logger is required'); this.app = this.options.app; this.timing = this.app.timing || new Timing(); this[REQUIRE_COUNT] = 0; /** * @member {Object} EeLoader#pkg * @see {@link AppInfo#pkg} * @since 1.0.0 */ this.pkg = this.getPkg(); /** * Framework directories * * @member {Array} EeLoader#EePaths * @see EeLoader#getEePaths * @since 1.0.0 */ this.EePaths = this.getEePaths(); debug('Loaded EePaths %j', this.EePaths); /** * @member {String} EeLoader#serverEnv * @see AppInfo#env * @since 1.0.0 */ this.serverEnv = this.getServerEnv(); debug('Loaded serverEnv %j', this.serverEnv); /** * @member {AppInfo} EeLoader#appInfo * @since 1.0.0 */ this.appInfo = this.getAppInfo(); /** * @member {String} EeLoader#serverScope * @see AppInfo#serverScope */ this.serverScope = options.serverScope !== undefined ? options.serverScope : this.getServerScope(); } /** * Get {@link AppInfo#env} * @return {String} env * @see AppInfo#env * @private * @since 1.0.0 */ getServerEnv() { let serverEnv = this.options.env; if (!serverEnv) { throw new Error('[core] [lib] [loader] getServerEnv serverEnv can not be empty!'); } return serverEnv; } /** * Get {@link AppInfo#scope} * @return {String} serverScope * @private */ getServerScope() { return process.env.EE_SERVER_SCOPE || ''; } /** * Get {@link AppInfo#name} * @return {String} appname * @private * @since 1.0.0 */ getAppname() { if (this.pkg.name) { debug('Loaded appname(%s) from package.json', this.pkg.name); return this.pkg.name; } throw new Error(`name is required from ${pkg}`); } /** * Get home directory * @return {String} home directory * @since 3.4.0 */ getHomedir() { return this.options.homeDir; } /** * Get app info * @return {AppInfo} appInfo * @since 1.0.0 */ getAppInfo() { const env = this.serverEnv; /** * Meta information of the application * @class AppInfo */ return { /** * The name of the application, retrieve from the name property in `package.json`. * @member {String} AppInfo#name */ name: this.getAppname(), /** * The current directory, where the application code is. * @member {String} AppInfo#baseDir */ baseDir: this.options.baseDir, /** * The environment of the application, **it's not NODE_ENV** * * 1. from `$baseDir/config/env` * 2. from EE_SERVER_ENV * 3. from NODE_ENV * * env | description * --- | --- * test | system integration testing * prod | production * local | local on your own computer * unittest | unit test * * @member {String} AppInfo#env * @see https://Eejs.org/zh-cn/basics/env.html */ env: env, /** * @member {String} AppInfo#scope */ scope: this.serverScope, /** * The use directory, same as `process.env.HOME` * @member {String} AppInfo#HOME */ home: this.getHomedir(), /** * The directory whether is homeDir or appUserData depend on env. * @member {String} AppInfo#root */ root: Ps.getRootDir(), /** * electron application data dir * @member {String} AppInfo#appUserDataDir */ appUserDataDir: this.options.appUserData, /** * system user home dir * @member {String} AppInfo#userHome */ userHome: this.options.userHome, /** * application version * @member {String} AppInfo#appVersion */ appVersion: this.options.appVersion, /** * application package status * @member {boolean} AppInfo#isPackaged */ isPackaged: this.options.isPackaged, /** * application exec file dir * @member {String} AppInfo#execDir */ execDir: this.options.execDir }; } /** * Get {@link EeLoader#EePaths} * @return {Array} framework directories * @see {@link EeLoader#EePaths} * @private * @since 1.0.0 */ getEePaths() { // avoid require recursively const EePaths = []; EePaths.push(this.app[Symbol.for('ee#eePath')]); return EePaths; } // Low Level API /** * Load single file, will invoke when export is function * * @param {String} filepath - fullpath * @param {Array} inject - pass rest arguments into the function when invoke * @return {Object} exports * @example * ```js * app.loader.loadFile(path.join(app.options.baseDir, 'config/router.js')); * ``` * @since 1.0.0 */ loadFile(filepath, ...inject) { filepath = filepath && this.resolveModule(filepath); if (!filepath) { return null; } // function(arg1, args, ...) {} if (inject.length === 0) inject = [ this.app ]; let ret = this.requireFile(filepath); if (is.function(ret) && !is.class(ret) && !Utils.isBytecodeClass(ret)) { ret = ret(...inject); } return ret; } /** * @param {String} filepath - fullpath * @return {Object} exports * @private */ requireFile(filepath) { const timingKey = `Require(${this[REQUIRE_COUNT]++}) ${Utils.getResolvedFilename(filepath, this.options.baseDir)}`; this.timing.start(timingKey); const ret = Utils.loadFile(filepath); this.timing.end(timingKey); return ret; } /** * Get all loadUnit * * loadUnit is a directory that can be loaded by EeLoader, 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 * @since 1.0.0 */ getLoadUnits() { if (this.dirs) { return this.dirs; } const dirs = this.dirs = []; // framework or Ee path for (const EePath of this.EePaths) { dirs.push({ path: EePath, type: 'framework', }); } // application dirs.push({ path: this.options.baseDir, type: 'app', }); debug('Loaded dirs %j', dirs); return dirs; } /** * Load files using {@link FileLoader}, inject to {@link Application} * @param {String|Array} directory - see {@link FileLoader} * @param {String} property - see {@link FileLoader} * @param {Object} opt - see {@link FileLoader} * @since 1.0.0 */ loadToApp(directory, property, opt) { const target = this.app[property] = {}; opt = Object.assign({}, { directory, target, inject: this.app, }, opt); const timingKey = `Load "${String(property)}" to Application`; this.timing.start(timingKey); new FileLoader(opt).load(); this.timing.end(timingKey); } /** * Load files using {@link ContextLoader} * @param {String|Array} directory - see {@link ContextLoader} * @param {String} property - see {@link ContextLoader} * @param {Object} opt - see {@link ContextLoader} * @since 1.0.0 */ loadToContext(directory, property, opt) { opt = Object.assign({}, { directory, property, inject: this.app, loader: this }, opt); const timingKey = `Load "${String(property)}" to Context`; this.timing.start(timingKey); if (['addon'].includes(property)) { new ContextLoader(opt).loadAddons(); } else { new ContextLoader(opt).load(); } this.timing.end(timingKey); } /** * @member {FileLoader} EeLoader#FileLoader * @since 1.0.0 */ get FileLoader() { return FileLoader; } /** * @member {ContextLoader} EeLoader#ContextLoader * @since 1.0.0 */ get ContextLoader() { return ContextLoader; } getTypeFiles(filename) { const files = [ `${filename}.default` ]; files.push(`${filename}.${this.serverEnv}`); return files; } resolveModule(filepath) { let fullpath; try { fullpath = require.resolve(filepath); } catch (e) { // 特殊后缀处理 if (filepath && (filepath.endsWith('.defalut') || filepath.endsWith('.prod'))) { fullpath = filepath + '.jsc'; } else if (filepath && filepath.endsWith('.js')) { fullpath = filepath + 'c'; } if (!fs.existsSync(filepath) && !fs.existsSync(fullpath)) { //this.options.logger.warn(`[ee-core] [core/lib/loader/ee_loader] resolveModule unknow filepath: ${filepath}`) return undefined; } } return fullpath; } getPkg() { const filePath = path.join(this.options.homeDir, 'package.json'); if (!fs.existsSync(filePath)) { throw new Error(filePath + ' is not found'); } const json = JSON.parse(fs.readFileSync(filePath)); return json; } } /** * Mixin methods to EeLoader * // ES6 Multiple Inheritance * https://medium.com/@leocavalcante/es6-multiple-inheritance-73a3c66d2b6b */ const loaders = [ require('./mixin/config'), require('./mixin/service'), require('./mixin/controller'), require('./mixin/addon'), ]; for (const loader of loaders) { Object.assign(EeLoader.prototype, loader); } module.exports = EeLoader;