plugin.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. 'use strict';
  2. const fs = require('fs');
  3. const path = require('path');
  4. const debug = require('debug')('egg-core:plugin');
  5. const sequencify = require('../../utils/sequencify');
  6. const loadFile = require('../../utils').loadFile;
  7. module.exports = {
  8. /**
  9. * Load config/plugin.js from {EggLoader#loadUnits}
  10. *
  11. * plugin.js is written below
  12. *
  13. * ```js
  14. * {
  15. * 'xxx-client': {
  16. * enable: true,
  17. * package: 'xxx-client',
  18. * dep: [],
  19. * env: [],
  20. * },
  21. * // short hand
  22. * 'rds': false,
  23. * 'depd': {
  24. * enable: true,
  25. * path: 'path/to/depd'
  26. * }
  27. * }
  28. * ```
  29. *
  30. * If the plugin has path, Loader will find the module from it.
  31. *
  32. * Otherwise Loader will lookup follow the order by packageName
  33. *
  34. * 1. $APP_BASE/node_modules/${package}
  35. * 2. $EGG_BASE/node_modules/${package}
  36. *
  37. * You can call `loader.plugins` that retrieve enabled plugins.
  38. *
  39. * ```js
  40. * loader.plugins['xxx-client'] = {
  41. * name: 'xxx-client', // the plugin name, it can be used in `dep`
  42. * package: 'xxx-client', // the package name of plugin
  43. * enable: true, // whether enabled
  44. * path: 'path/to/xxx-client', // the directory of the plugin package
  45. * dep: [], // the dependent plugins, you can use the plugin name
  46. * env: [ 'local', 'unittest' ], // specify the serverEnv that only enable the plugin in it
  47. * }
  48. * ```
  49. *
  50. * `loader.allPlugins` can be used when retrieve all plugins.
  51. * @function EggLoader#loadPlugin
  52. * @since 1.0.0
  53. */
  54. loadPlugin() {
  55. this.timing.start('Load Plugin');
  56. // loader plugins from application
  57. const appPlugins = this.readPluginConfigs(path.join(this.options.baseDir, 'config/plugin.default'));
  58. debug('Loaded app plugins: %j', Object.keys(appPlugins));
  59. // loader plugins from framework
  60. const eggPluginConfigPaths = this.eggPaths.map(eggPath => path.join(eggPath, 'config/plugin.default'));
  61. const eggPlugins = this.readPluginConfigs(eggPluginConfigPaths);
  62. debug('Loaded egg plugins: %j', Object.keys(eggPlugins));
  63. // loader plugins from process.env.EGG_PLUGINS
  64. let customPlugins;
  65. if (process.env.EGG_PLUGINS) {
  66. try {
  67. customPlugins = JSON.parse(process.env.EGG_PLUGINS);
  68. } catch (e) {
  69. debug('parse EGG_PLUGINS failed, %s', e);
  70. }
  71. }
  72. // loader plugins from options.plugins
  73. if (this.options.plugins) {
  74. customPlugins = Object.assign({}, customPlugins, this.options.plugins);
  75. }
  76. if (customPlugins) {
  77. for (const name in customPlugins) {
  78. this.normalizePluginConfig(customPlugins, name);
  79. }
  80. debug('Loaded custom plugins: %j', Object.keys(customPlugins));
  81. }
  82. this.allPlugins = {};
  83. this.appPlugins = appPlugins;
  84. this.customPlugins = customPlugins;
  85. this.eggPlugins = eggPlugins;
  86. this._extendPlugins(this.allPlugins, eggPlugins);
  87. this._extendPlugins(this.allPlugins, appPlugins);
  88. this._extendPlugins(this.allPlugins, customPlugins);
  89. const enabledPluginNames = []; // enabled plugins that configured explicitly
  90. const plugins = {};
  91. const env = this.serverEnv;
  92. for (const name in this.allPlugins) {
  93. const plugin = this.allPlugins[name];
  94. // resolve the real plugin.path based on plugin or package
  95. plugin.path = this.getPluginPath(plugin, this.options.baseDir);
  96. // read plugin information from ${plugin.path}/package.json
  97. this.mergePluginConfig(plugin);
  98. // disable the plugin that not match the serverEnv
  99. if (env && plugin.env.length && !plugin.env.includes(env)) {
  100. this.options.logger.info('Plugin %s is disabled by env unmatched, require env(%s) but got env is %s', name, plugin.env, env);
  101. plugin.enable = false;
  102. continue;
  103. }
  104. plugins[name] = plugin;
  105. if (plugin.enable) {
  106. enabledPluginNames.push(name);
  107. }
  108. }
  109. // retrieve the ordered plugins
  110. this.orderPlugins = this.getOrderPlugins(plugins, enabledPluginNames, appPlugins);
  111. const enablePlugins = {};
  112. for (const plugin of this.orderPlugins) {
  113. enablePlugins[plugin.name] = plugin;
  114. }
  115. debug('Loaded plugins: %j', Object.keys(enablePlugins));
  116. /**
  117. * Retrieve enabled plugins
  118. * @member {Object} EggLoader#plugins
  119. * @since 1.0.0
  120. */
  121. this.plugins = enablePlugins;
  122. this.timing.end('Load Plugin');
  123. },
  124. /*
  125. * Read plugin.js from multiple directory
  126. */
  127. readPluginConfigs(configPaths) {
  128. if (!Array.isArray(configPaths)) {
  129. configPaths = [ configPaths ];
  130. }
  131. // Get all plugin configurations
  132. // plugin.default.js
  133. // plugin.${scope}.js
  134. // plugin.${env}.js
  135. // plugin.${scope}_${env}.js
  136. const newConfigPaths = [];
  137. for (const filename of this.getTypeFiles('plugin')) {
  138. for (let configPath of configPaths) {
  139. configPath = path.join(path.dirname(configPath), filename);
  140. newConfigPaths.push(configPath);
  141. }
  142. }
  143. const plugins = {};
  144. for (const configPath of newConfigPaths) {
  145. let filepath = this.resolveModule(configPath);
  146. // let plugin.js compatible
  147. if (configPath.endsWith('plugin.default') && !filepath) {
  148. filepath = this.resolveModule(configPath.replace(/plugin\.default$/, 'plugin'));
  149. }
  150. if (!filepath) {
  151. continue;
  152. }
  153. const config = loadFile(filepath);
  154. for (const name in config) {
  155. this.normalizePluginConfig(config, name, filepath);
  156. }
  157. this._extendPlugins(plugins, config);
  158. }
  159. return plugins;
  160. },
  161. normalizePluginConfig(plugins, name, configPath) {
  162. const plugin = plugins[name];
  163. // plugin_name: false
  164. if (typeof plugin === 'boolean') {
  165. plugins[name] = {
  166. name,
  167. enable: plugin,
  168. dependencies: [],
  169. optionalDependencies: [],
  170. env: [],
  171. from: configPath,
  172. };
  173. return;
  174. }
  175. if (!('enable' in plugin)) {
  176. plugin.enable = true;
  177. }
  178. plugin.name = name;
  179. plugin.dependencies = plugin.dependencies || [];
  180. plugin.optionalDependencies = plugin.optionalDependencies || [];
  181. plugin.env = plugin.env || [];
  182. plugin.from = configPath;
  183. depCompatible(plugin);
  184. },
  185. // Read plugin information from package.json and merge
  186. // {
  187. // eggPlugin: {
  188. // "name": "", plugin name, must be same as name in config/plugin.js
  189. // "dep": [], dependent plugins
  190. // "env": "" env
  191. // "strict": true, whether check plugin name, default to true.
  192. // }
  193. // }
  194. mergePluginConfig(plugin) {
  195. let pkg;
  196. let config;
  197. const pluginPackage = path.join(plugin.path, 'package.json');
  198. if (fs.existsSync(pluginPackage)) {
  199. pkg = require(pluginPackage);
  200. config = pkg.eggPlugin;
  201. if (pkg.version) {
  202. plugin.version = pkg.version;
  203. }
  204. }
  205. const logger = this.options.logger;
  206. if (!config) {
  207. logger.warn(`[egg:loader] pkg.eggPlugin is missing in ${pluginPackage}`);
  208. return;
  209. }
  210. if (config.name && config.strict !== false && config.name !== plugin.name) {
  211. // pluginName is configured in config/plugin.js
  212. // pluginConfigName is pkg.eggPlugin.name
  213. logger.warn(`[egg:loader] pluginName(${plugin.name}) is different from pluginConfigName(${config.name})`);
  214. }
  215. // dep compatible
  216. depCompatible(config);
  217. for (const key of [ 'dependencies', 'optionalDependencies', 'env' ]) {
  218. if (!plugin[key].length && Array.isArray(config[key])) {
  219. plugin[key] = config[key];
  220. }
  221. }
  222. },
  223. getOrderPlugins(allPlugins, enabledPluginNames, appPlugins) {
  224. // no plugins enabled
  225. if (!enabledPluginNames.length) {
  226. return [];
  227. }
  228. const result = sequencify(allPlugins, enabledPluginNames);
  229. debug('Got plugins %j after sequencify', result);
  230. // catch error when result.sequence is empty
  231. if (!result.sequence.length) {
  232. const err = new Error(`sequencify plugins has problem, missing: [${result.missingTasks}], recursive: [${result.recursiveDependencies}]`);
  233. // find plugins which is required by the missing plugin
  234. for (const missName of result.missingTasks) {
  235. const requires = [];
  236. for (const name in allPlugins) {
  237. if (allPlugins[name].dependencies.includes(missName)) {
  238. requires.push(name);
  239. }
  240. }
  241. err.message += `\n\t>> Plugin [${missName}] is disabled or missed, but is required by [${requires}]`;
  242. }
  243. err.name = 'PluginSequencifyError';
  244. throw err;
  245. }
  246. // log the plugins that be enabled implicitly
  247. const implicitEnabledPlugins = [];
  248. const requireMap = {};
  249. result.sequence.forEach(name => {
  250. for (const depName of allPlugins[name].dependencies) {
  251. if (!requireMap[depName]) {
  252. requireMap[depName] = [];
  253. }
  254. requireMap[depName].push(name);
  255. }
  256. if (!allPlugins[name].enable) {
  257. implicitEnabledPlugins.push(name);
  258. allPlugins[name].enable = true;
  259. }
  260. });
  261. // Following plugins will be enabled implicitly.
  262. // - configclient required by [hsfclient]
  263. // - eagleeye required by [hsfclient]
  264. // - diamond required by [hsfclient]
  265. if (implicitEnabledPlugins.length) {
  266. let message = implicitEnabledPlugins
  267. .map(name => ` - ${name} required by [${requireMap[name]}]`)
  268. .join('\n');
  269. this.options.logger.info(`Following plugins will be enabled implicitly.\n${message}`);
  270. // should warn when the plugin is disabled by app
  271. const disabledPlugins = implicitEnabledPlugins.filter(name => appPlugins[name] && appPlugins[name].enable === false);
  272. if (disabledPlugins.length) {
  273. message = disabledPlugins
  274. .map(name => ` - ${name} required by [${requireMap[name]}]`)
  275. .join('\n');
  276. this.options.logger.warn(`Following plugins will be enabled implicitly that is disabled by application.\n${message}`);
  277. }
  278. }
  279. return result.sequence.map(name => allPlugins[name]);
  280. },
  281. // Get the real plugin path
  282. getPluginPath(plugin) {
  283. if (plugin.path) {
  284. return plugin.path;
  285. }
  286. const name = plugin.package || plugin.name;
  287. const lookupDirs = new Set();
  288. // try to locate the plugin in the following directories
  289. // -> {APP_PATH}/node_modules
  290. // -> {EGG_PATH}/node_modules
  291. // -> $CWD/node_modules
  292. lookupDirs.add(path.join(this.options.baseDir, 'node_modules'));
  293. // try to locate the plugin at framework from upper to lower
  294. for (let i = this.eggPaths.length - 1; i >= 0; i--) {
  295. const eggPath = this.eggPaths[i];
  296. lookupDirs.add(path.join(eggPath, 'node_modules'));
  297. }
  298. // should find the $cwd/node_modules when test the plugins under npm3
  299. lookupDirs.add(path.join(process.cwd(), 'node_modules'));
  300. // should find the siblings directory of framework when using pnpm
  301. for (let i = this.eggPaths.length - 1; i >= 0; i--) {
  302. const eggPath = this.eggPaths[i];
  303. // 'node_modules/.pnpm/yadan@2.0.0/node_modules/yadan/node_modules',
  304. // 'node_modules/.pnpm/yadan@2.0.0/node_modules', <- this is the sibling directory
  305. // 'node_modules/.pnpm/egg@2.33.1/node_modules/egg/node_modules',
  306. // 'node_modules/.pnpm/egg@2.33.1/node_modules', <- this is the sibling directory
  307. lookupDirs.add(path.dirname(eggPath));
  308. }
  309. for (let dir of lookupDirs) {
  310. dir = path.join(dir, name);
  311. if (fs.existsSync(dir)) {
  312. return fs.realpathSync(dir);
  313. }
  314. }
  315. throw new Error(`Can not find plugin ${name} in "${[ ...lookupDirs ].join(', ')}"`);
  316. },
  317. _extendPlugins(target, plugins) {
  318. if (!plugins) {
  319. return;
  320. }
  321. for (const name in plugins) {
  322. const plugin = plugins[name];
  323. let targetPlugin = target[name];
  324. if (!targetPlugin) {
  325. targetPlugin = target[name] = {};
  326. }
  327. if (targetPlugin.package && targetPlugin.package === plugin.package) {
  328. this.options.logger.warn('plugin %s has been defined that is %j, but you define again in %s',
  329. name, targetPlugin, plugin.from);
  330. }
  331. if (plugin.path || plugin.package) {
  332. delete targetPlugin.path;
  333. delete targetPlugin.package;
  334. }
  335. for (const prop in plugin) {
  336. if (plugin[prop] === undefined) {
  337. continue;
  338. }
  339. if (targetPlugin[prop] && Array.isArray(plugin[prop]) && !plugin[prop].length) {
  340. continue;
  341. }
  342. targetPlugin[prop] = plugin[prop];
  343. }
  344. }
  345. },
  346. };
  347. function depCompatible(plugin) {
  348. if (plugin.dep && !(Array.isArray(plugin.dependencies) && plugin.dependencies.length)) {
  349. plugin.dependencies = plugin.dep;
  350. delete plugin.dep;
  351. }
  352. }