egg_loader.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. 'use strict';
  2. const fs = require('fs');
  3. const path = require('path');
  4. const assert = require('assert');
  5. const is = require('is-type-of');
  6. const debug = require('debug')('egg-core');
  7. const homedir = require('node-homedir');
  8. const FileLoader = require('./file_loader');
  9. const ContextLoader = require('./context_loader');
  10. const utility = require('utility');
  11. const utils = require('../utils');
  12. const Timing = require('../utils/timing');
  13. const REQUIRE_COUNT = Symbol('EggLoader#requireCount');
  14. class EggLoader {
  15. /**
  16. * @class
  17. * @param {Object} options - options
  18. * @param {String} options.baseDir - the directory of application
  19. * @param {EggCore} options.app - Application instance
  20. * @param {Logger} options.logger - logger
  21. * @param {Object} [options.plugins] - custom plugins
  22. * @since 1.0.0
  23. */
  24. constructor(options) {
  25. this.options = options;
  26. assert(fs.existsSync(this.options.baseDir), `${this.options.baseDir} not exists`);
  27. assert(this.options.app, 'options.app is required');
  28. assert(this.options.logger, 'options.logger is required');
  29. this.app = this.options.app;
  30. this.lifecycle = this.app.lifecycle;
  31. this.timing = this.app.timing || new Timing();
  32. this[REQUIRE_COUNT] = 0;
  33. /**
  34. * @member {Object} EggLoader#pkg
  35. * @see {@link AppInfo#pkg}
  36. * @since 1.0.0
  37. */
  38. this.pkg = utility.readJSONSync(path.join(this.options.baseDir, 'package.json'));
  39. /**
  40. * All framework directories.
  41. *
  42. * You can extend Application of egg, the entry point is options.app,
  43. *
  44. * loader will find all directories from the prototype of Application,
  45. * you should define `Symbol.for('egg#eggPath')` property.
  46. *
  47. * ```
  48. * // lib/example.js
  49. * const egg = require('egg');
  50. * class ExampleApplication extends egg.Application {
  51. * constructor(options) {
  52. * super(options);
  53. * }
  54. *
  55. * get [Symbol.for('egg#eggPath')]() {
  56. * return path.join(__dirname, '..');
  57. * }
  58. * }
  59. * ```
  60. * @member {Array} EggLoader#eggPaths
  61. * @see EggLoader#getEggPaths
  62. * @since 1.0.0
  63. */
  64. this.eggPaths = this.getEggPaths();
  65. debug('Loaded eggPaths %j', this.eggPaths);
  66. /**
  67. * @member {String} EggLoader#serverEnv
  68. * @see AppInfo#env
  69. * @since 1.0.0
  70. */
  71. this.serverEnv = this.getServerEnv();
  72. debug('Loaded serverEnv %j', this.serverEnv);
  73. /**
  74. * @member {AppInfo} EggLoader#appInfo
  75. * @since 1.0.0
  76. */
  77. this.appInfo = this.getAppInfo();
  78. /**
  79. * @member {String} EggLoader#serverScope
  80. * @see AppInfo#serverScope
  81. */
  82. this.serverScope = options.serverScope !== undefined
  83. ? options.serverScope
  84. : this.getServerScope();
  85. }
  86. /**
  87. * Get {@link AppInfo#env}
  88. * @return {String} env
  89. * @see AppInfo#env
  90. * @private
  91. * @since 1.0.0
  92. */
  93. getServerEnv() {
  94. let serverEnv = this.options.env;
  95. const envPath = path.join(this.options.baseDir, 'config/env');
  96. if (!serverEnv && fs.existsSync(envPath)) {
  97. serverEnv = fs.readFileSync(envPath, 'utf8').trim();
  98. }
  99. if (!serverEnv) {
  100. serverEnv = process.env.EGG_SERVER_ENV;
  101. }
  102. if (!serverEnv) {
  103. if (process.env.NODE_ENV === 'test') {
  104. serverEnv = 'unittest';
  105. } else if (process.env.NODE_ENV === 'production') {
  106. serverEnv = 'prod';
  107. } else {
  108. serverEnv = 'local';
  109. }
  110. } else {
  111. serverEnv = serverEnv.trim();
  112. }
  113. return serverEnv;
  114. }
  115. /**
  116. * Get {@link AppInfo#scope}
  117. * @return {String} serverScope
  118. * @private
  119. */
  120. getServerScope() {
  121. return process.env.EGG_SERVER_SCOPE || '';
  122. }
  123. /**
  124. * Get {@link AppInfo#name}
  125. * @return {String} appname
  126. * @private
  127. * @since 1.0.0
  128. */
  129. getAppname() {
  130. if (this.pkg.name) {
  131. debug('Loaded appname(%s) from package.json', this.pkg.name);
  132. return this.pkg.name;
  133. }
  134. const pkg = path.join(this.options.baseDir, 'package.json');
  135. throw new Error(`name is required from ${pkg}`);
  136. }
  137. /**
  138. * Get home directory
  139. * @return {String} home directory
  140. * @since 3.4.0
  141. */
  142. getHomedir() {
  143. // EGG_HOME for test
  144. return process.env.EGG_HOME || homedir() || '/home/admin';
  145. }
  146. /**
  147. * Get app info
  148. * @return {AppInfo} appInfo
  149. * @since 1.0.0
  150. */
  151. getAppInfo() {
  152. const env = this.serverEnv;
  153. const scope = this.serverScope;
  154. const home = this.getHomedir();
  155. const baseDir = this.options.baseDir;
  156. /**
  157. * Meta information of the application
  158. * @class AppInfo
  159. */
  160. return {
  161. /**
  162. * The name of the application, retrieve from the name property in `package.json`.
  163. * @member {String} AppInfo#name
  164. */
  165. name: this.getAppname(),
  166. /**
  167. * The current directory, where the application code is.
  168. * @member {String} AppInfo#baseDir
  169. */
  170. baseDir,
  171. /**
  172. * The environment of the application, **it's not NODE_ENV**
  173. *
  174. * 1. from `$baseDir/config/env`
  175. * 2. from EGG_SERVER_ENV
  176. * 3. from NODE_ENV
  177. *
  178. * env | description
  179. * --- | ---
  180. * test | system integration testing
  181. * prod | production
  182. * local | local on your own computer
  183. * unittest | unit test
  184. *
  185. * @member {String} AppInfo#env
  186. * @see https://eggjs.org/zh-cn/basics/env.html
  187. */
  188. env,
  189. /**
  190. * @member {String} AppInfo#scope
  191. */
  192. scope,
  193. /**
  194. * The use directory, same as `process.env.HOME`
  195. * @member {String} AppInfo#HOME
  196. */
  197. HOME: home,
  198. /**
  199. * parsed from `package.json`
  200. * @member {Object} AppInfo#pkg
  201. */
  202. pkg: this.pkg,
  203. /**
  204. * The directory whether is baseDir or HOME depend on env.
  205. * it's good for test when you want to write some file to HOME,
  206. * but don't want to write to the real directory,
  207. * so use root to write file to baseDir instead of HOME when unittest.
  208. * keep root directory in baseDir when local and unittest
  209. * @member {String} AppInfo#root
  210. */
  211. root: env === 'local' || env === 'unittest' ? baseDir : home,
  212. };
  213. }
  214. /**
  215. * Get {@link EggLoader#eggPaths}
  216. * @return {Array} framework directories
  217. * @see {@link EggLoader#eggPaths}
  218. * @private
  219. * @since 1.0.0
  220. */
  221. getEggPaths() {
  222. // avoid require recursively
  223. const EggCore = require('../egg');
  224. const eggPaths = [];
  225. let proto = this.app;
  226. // Loop for the prototype chain
  227. while (proto) {
  228. proto = Object.getPrototypeOf(proto);
  229. // stop the loop if
  230. // - object extends Object
  231. // - object extends EggCore
  232. if (proto === Object.prototype || proto === EggCore.prototype) {
  233. break;
  234. }
  235. assert(proto.hasOwnProperty(Symbol.for('egg#eggPath')), 'Symbol.for(\'egg#eggPath\') is required on Application');
  236. const eggPath = proto[Symbol.for('egg#eggPath')];
  237. assert(eggPath && typeof eggPath === 'string', 'Symbol.for(\'egg#eggPath\') should be string');
  238. assert(fs.existsSync(eggPath), `${eggPath} not exists`);
  239. const realpath = fs.realpathSync(eggPath);
  240. if (!eggPaths.includes(realpath)) {
  241. eggPaths.unshift(realpath);
  242. }
  243. }
  244. return eggPaths;
  245. }
  246. // Low Level API
  247. /**
  248. * Load single file, will invoke when export is function
  249. *
  250. * @param {String} filepath - fullpath
  251. * @param {Array} inject - pass rest arguments into the function when invoke
  252. * @return {Object} exports
  253. * @example
  254. * ```js
  255. * app.loader.loadFile(path.join(app.options.baseDir, 'config/router.js'));
  256. * ```
  257. * @since 1.0.0
  258. */
  259. loadFile(filepath, ...inject) {
  260. filepath = filepath && this.resolveModule(filepath);
  261. if (!filepath) {
  262. return null;
  263. }
  264. // function(arg1, args, ...) {}
  265. if (inject.length === 0) inject = [ this.app ];
  266. let ret = this.requireFile(filepath);
  267. if (is.function(ret) && !is.class(ret)) {
  268. ret = ret(...inject);
  269. }
  270. return ret;
  271. }
  272. /**
  273. * @param {String} filepath - fullpath
  274. * @return {Object} exports
  275. * @private
  276. */
  277. requireFile(filepath) {
  278. const timingKey = `Require(${this[REQUIRE_COUNT]++}) ${utils.getResolvedFilename(filepath, this.options.baseDir)}`;
  279. this.timing.start(timingKey);
  280. const ret = utils.loadFile(filepath);
  281. this.timing.end(timingKey);
  282. return ret;
  283. }
  284. /**
  285. * Get all loadUnit
  286. *
  287. * loadUnit is a directory that can be loaded by EggLoader, it has the same structure.
  288. * loadUnit has a path and a type(app, framework, plugin).
  289. *
  290. * The order of the loadUnits:
  291. *
  292. * 1. plugin
  293. * 2. framework
  294. * 3. app
  295. *
  296. * @return {Array} loadUnits
  297. * @since 1.0.0
  298. */
  299. getLoadUnits() {
  300. if (this.dirs) {
  301. return this.dirs;
  302. }
  303. const dirs = this.dirs = [];
  304. if (this.orderPlugins) {
  305. for (const plugin of this.orderPlugins) {
  306. dirs.push({
  307. path: plugin.path,
  308. type: 'plugin',
  309. });
  310. }
  311. }
  312. // framework or egg path
  313. for (const eggPath of this.eggPaths) {
  314. dirs.push({
  315. path: eggPath,
  316. type: 'framework',
  317. });
  318. }
  319. // application
  320. dirs.push({
  321. path: this.options.baseDir,
  322. type: 'app',
  323. });
  324. debug('Loaded dirs %j', dirs);
  325. return dirs;
  326. }
  327. /**
  328. * Load files using {@link FileLoader}, inject to {@link Application}
  329. * @param {String|Array} directory - see {@link FileLoader}
  330. * @param {String} property - see {@link FileLoader}
  331. * @param {Object} opt - see {@link FileLoader}
  332. * @since 1.0.0
  333. */
  334. loadToApp(directory, property, opt) {
  335. const target = this.app[property] = {};
  336. opt = Object.assign({}, {
  337. directory,
  338. target,
  339. inject: this.app,
  340. }, opt);
  341. const timingKey = `Load "${String(property)}" to Application`;
  342. this.timing.start(timingKey);
  343. new FileLoader(opt).load();
  344. this.timing.end(timingKey);
  345. }
  346. /**
  347. * Load files using {@link ContextLoader}
  348. * @param {String|Array} directory - see {@link ContextLoader}
  349. * @param {String} property - see {@link ContextLoader}
  350. * @param {Object} opt - see {@link ContextLoader}
  351. * @since 1.0.0
  352. */
  353. loadToContext(directory, property, opt) {
  354. opt = Object.assign({}, {
  355. directory,
  356. property,
  357. inject: this.app,
  358. }, opt);
  359. const timingKey = `Load "${String(property)}" to Context`;
  360. this.timing.start(timingKey);
  361. new ContextLoader(opt).load();
  362. this.timing.end(timingKey);
  363. }
  364. /**
  365. * @member {FileLoader} EggLoader#FileLoader
  366. * @since 1.0.0
  367. */
  368. get FileLoader() {
  369. return FileLoader;
  370. }
  371. /**
  372. * @member {ContextLoader} EggLoader#ContextLoader
  373. * @since 1.0.0
  374. */
  375. get ContextLoader() {
  376. return ContextLoader;
  377. }
  378. getTypeFiles(filename) {
  379. const files = [ `${filename}.default` ];
  380. if (this.serverScope) files.push(`${filename}.${this.serverScope}`);
  381. if (this.serverEnv === 'default') return files;
  382. files.push(`${filename}.${this.serverEnv}`);
  383. if (this.serverScope) files.push(`${filename}.${this.serverScope}_${this.serverEnv}`);
  384. return files;
  385. }
  386. resolveModule(filepath) {
  387. let fullPath;
  388. try {
  389. fullPath = require.resolve(filepath);
  390. } catch (e) {
  391. return undefined;
  392. }
  393. if (process.env.EGG_TYPESCRIPT !== 'true' && fullPath.endsWith('.ts')) {
  394. return undefined;
  395. }
  396. return fullPath;
  397. }
  398. }
  399. /**
  400. * Mixin methods to EggLoader
  401. * // ES6 Multiple Inheritance
  402. * https://medium.com/@leocavalcante/es6-multiple-inheritance-73a3c66d2b6b
  403. */
  404. const loaders = [
  405. require('./mixin/plugin'),
  406. require('./mixin/config'),
  407. require('./mixin/extend'),
  408. require('./mixin/custom'),
  409. require('./mixin/service'),
  410. require('./mixin/middleware'),
  411. require('./mixin/controller'),
  412. require('./mixin/router'),
  413. require('./mixin/custom_loader'),
  414. ];
  415. for (const loader of loaders) {
  416. Object.assign(EggLoader.prototype, loader);
  417. }
  418. module.exports = EggLoader;