123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409 |
- 'use strict';
- const fs = require('fs');
- const path = require('path');
- const debug = require('debug')('egg-core:plugin');
- const sequencify = require('../../utils/sequencify');
- const loadFile = require('../../utils').loadFile;
- module.exports = {
- /**
- * Load config/plugin.js from {EggLoader#loadUnits}
- *
- * plugin.js is written below
- *
- * ```js
- * {
- * 'xxx-client': {
- * enable: true,
- * package: 'xxx-client',
- * dep: [],
- * env: [],
- * },
- * // short hand
- * 'rds': false,
- * 'depd': {
- * enable: true,
- * path: 'path/to/depd'
- * }
- * }
- * ```
- *
- * If the plugin has path, Loader will find the module from it.
- *
- * Otherwise Loader will lookup follow the order by packageName
- *
- * 1. $APP_BASE/node_modules/${package}
- * 2. $EGG_BASE/node_modules/${package}
- *
- * You can call `loader.plugins` that retrieve enabled plugins.
- *
- * ```js
- * loader.plugins['xxx-client'] = {
- * name: 'xxx-client', // the plugin name, it can be used in `dep`
- * package: 'xxx-client', // the package name of plugin
- * enable: true, // whether enabled
- * path: 'path/to/xxx-client', // the directory of the plugin package
- * dep: [], // the dependent plugins, you can use the plugin name
- * env: [ 'local', 'unittest' ], // specify the serverEnv that only enable the plugin in it
- * }
- * ```
- *
- * `loader.allPlugins` can be used when retrieve all plugins.
- * @function EggLoader#loadPlugin
- * @since 1.0.0
- */
- loadPlugin() {
- this.timing.start('Load Plugin');
- // loader plugins from application
- const appPlugins = this.readPluginConfigs(path.join(this.options.baseDir, 'config/plugin.default'));
- debug('Loaded app plugins: %j', Object.keys(appPlugins));
- // loader plugins from framework
- const eggPluginConfigPaths = this.eggPaths.map(eggPath => path.join(eggPath, 'config/plugin.default'));
- const eggPlugins = this.readPluginConfigs(eggPluginConfigPaths);
- debug('Loaded egg plugins: %j', Object.keys(eggPlugins));
- // loader plugins from process.env.EGG_PLUGINS
- let customPlugins;
- if (process.env.EGG_PLUGINS) {
- try {
- customPlugins = JSON.parse(process.env.EGG_PLUGINS);
- } catch (e) {
- debug('parse EGG_PLUGINS failed, %s', e);
- }
- }
- // loader plugins from options.plugins
- if (this.options.plugins) {
- customPlugins = Object.assign({}, customPlugins, this.options.plugins);
- }
- if (customPlugins) {
- for (const name in customPlugins) {
- this.normalizePluginConfig(customPlugins, name);
- }
- debug('Loaded custom plugins: %j', Object.keys(customPlugins));
- }
- this.allPlugins = {};
- this.appPlugins = appPlugins;
- this.customPlugins = customPlugins;
- this.eggPlugins = eggPlugins;
- this._extendPlugins(this.allPlugins, eggPlugins);
- this._extendPlugins(this.allPlugins, appPlugins);
- this._extendPlugins(this.allPlugins, customPlugins);
- const enabledPluginNames = []; // enabled plugins that configured explicitly
- const plugins = {};
- const env = this.serverEnv;
- for (const name in this.allPlugins) {
- const plugin = this.allPlugins[name];
- // resolve the real plugin.path based on plugin or package
- plugin.path = this.getPluginPath(plugin, this.options.baseDir);
- // read plugin information from ${plugin.path}/package.json
- this.mergePluginConfig(plugin);
- // disable the plugin that not match the serverEnv
- if (env && plugin.env.length && !plugin.env.includes(env)) {
- this.options.logger.info('Plugin %s is disabled by env unmatched, require env(%s) but got env is %s', name, plugin.env, env);
- plugin.enable = false;
- continue;
- }
- plugins[name] = plugin;
- if (plugin.enable) {
- enabledPluginNames.push(name);
- }
- }
- // retrieve the ordered plugins
- this.orderPlugins = this.getOrderPlugins(plugins, enabledPluginNames, appPlugins);
- const enablePlugins = {};
- for (const plugin of this.orderPlugins) {
- enablePlugins[plugin.name] = plugin;
- }
- debug('Loaded plugins: %j', Object.keys(enablePlugins));
- /**
- * Retrieve enabled plugins
- * @member {Object} EggLoader#plugins
- * @since 1.0.0
- */
- this.plugins = enablePlugins;
- this.timing.end('Load Plugin');
- },
- /*
- * Read plugin.js from multiple directory
- */
- readPluginConfigs(configPaths) {
- if (!Array.isArray(configPaths)) {
- configPaths = [ configPaths ];
- }
- // Get all plugin configurations
- // plugin.default.js
- // plugin.${scope}.js
- // plugin.${env}.js
- // plugin.${scope}_${env}.js
- const newConfigPaths = [];
- for (const filename of this.getTypeFiles('plugin')) {
- for (let configPath of configPaths) {
- configPath = path.join(path.dirname(configPath), filename);
- newConfigPaths.push(configPath);
- }
- }
- const plugins = {};
- for (const configPath of newConfigPaths) {
- let filepath = this.resolveModule(configPath);
- // let plugin.js compatible
- if (configPath.endsWith('plugin.default') && !filepath) {
- filepath = this.resolveModule(configPath.replace(/plugin\.default$/, 'plugin'));
- }
- if (!filepath) {
- continue;
- }
- const config = loadFile(filepath);
- for (const name in config) {
- this.normalizePluginConfig(config, name, filepath);
- }
- this._extendPlugins(plugins, config);
- }
- return plugins;
- },
- normalizePluginConfig(plugins, name, configPath) {
- const plugin = plugins[name];
- // plugin_name: false
- if (typeof plugin === 'boolean') {
- plugins[name] = {
- name,
- enable: plugin,
- dependencies: [],
- optionalDependencies: [],
- env: [],
- from: configPath,
- };
- return;
- }
- if (!('enable' in plugin)) {
- plugin.enable = true;
- }
- plugin.name = name;
- plugin.dependencies = plugin.dependencies || [];
- plugin.optionalDependencies = plugin.optionalDependencies || [];
- plugin.env = plugin.env || [];
- plugin.from = configPath;
- depCompatible(plugin);
- },
- // Read plugin information from package.json and merge
- // {
- // eggPlugin: {
- // "name": "", plugin name, must be same as name in config/plugin.js
- // "dep": [], dependent plugins
- // "env": "" env
- // "strict": true, whether check plugin name, default to true.
- // }
- // }
- mergePluginConfig(plugin) {
- let pkg;
- let config;
- const pluginPackage = path.join(plugin.path, 'package.json');
- if (fs.existsSync(pluginPackage)) {
- pkg = require(pluginPackage);
- config = pkg.eggPlugin;
- if (pkg.version) {
- plugin.version = pkg.version;
- }
- }
- const logger = this.options.logger;
- if (!config) {
- logger.warn(`[egg:loader] pkg.eggPlugin is missing in ${pluginPackage}`);
- return;
- }
- if (config.name && config.strict !== false && config.name !== plugin.name) {
- // pluginName is configured in config/plugin.js
- // pluginConfigName is pkg.eggPlugin.name
- logger.warn(`[egg:loader] pluginName(${plugin.name}) is different from pluginConfigName(${config.name})`);
- }
- // dep compatible
- depCompatible(config);
- for (const key of [ 'dependencies', 'optionalDependencies', 'env' ]) {
- if (!plugin[key].length && Array.isArray(config[key])) {
- plugin[key] = config[key];
- }
- }
- },
- getOrderPlugins(allPlugins, enabledPluginNames, appPlugins) {
- // no plugins enabled
- if (!enabledPluginNames.length) {
- return [];
- }
- const result = sequencify(allPlugins, enabledPluginNames);
- debug('Got plugins %j after sequencify', result);
- // catch error when result.sequence is empty
- if (!result.sequence.length) {
- const err = new Error(`sequencify plugins has problem, missing: [${result.missingTasks}], recursive: [${result.recursiveDependencies}]`);
- // find plugins which is required by the missing plugin
- for (const missName of result.missingTasks) {
- const requires = [];
- for (const name in allPlugins) {
- if (allPlugins[name].dependencies.includes(missName)) {
- requires.push(name);
- }
- }
- err.message += `\n\t>> Plugin [${missName}] is disabled or missed, but is required by [${requires}]`;
- }
- err.name = 'PluginSequencifyError';
- throw err;
- }
- // log the plugins that be enabled implicitly
- const implicitEnabledPlugins = [];
- const requireMap = {};
- result.sequence.forEach(name => {
- for (const depName of allPlugins[name].dependencies) {
- if (!requireMap[depName]) {
- requireMap[depName] = [];
- }
- requireMap[depName].push(name);
- }
- if (!allPlugins[name].enable) {
- implicitEnabledPlugins.push(name);
- allPlugins[name].enable = true;
- }
- });
- // Following plugins will be enabled implicitly.
- // - configclient required by [hsfclient]
- // - eagleeye required by [hsfclient]
- // - diamond required by [hsfclient]
- if (implicitEnabledPlugins.length) {
- let message = implicitEnabledPlugins
- .map(name => ` - ${name} required by [${requireMap[name]}]`)
- .join('\n');
- this.options.logger.info(`Following plugins will be enabled implicitly.\n${message}`);
- // should warn when the plugin is disabled by app
- const disabledPlugins = implicitEnabledPlugins.filter(name => appPlugins[name] && appPlugins[name].enable === false);
- if (disabledPlugins.length) {
- message = disabledPlugins
- .map(name => ` - ${name} required by [${requireMap[name]}]`)
- .join('\n');
- this.options.logger.warn(`Following plugins will be enabled implicitly that is disabled by application.\n${message}`);
- }
- }
- return result.sequence.map(name => allPlugins[name]);
- },
- // Get the real plugin path
- getPluginPath(plugin) {
- if (plugin.path) {
- return plugin.path;
- }
- const name = plugin.package || plugin.name;
- const lookupDirs = new Set();
- // try to locate the plugin in the following directories
- // -> {APP_PATH}/node_modules
- // -> {EGG_PATH}/node_modules
- // -> $CWD/node_modules
- lookupDirs.add(path.join(this.options.baseDir, 'node_modules'));
- // try to locate the plugin at framework from upper to lower
- for (let i = this.eggPaths.length - 1; i >= 0; i--) {
- const eggPath = this.eggPaths[i];
- lookupDirs.add(path.join(eggPath, 'node_modules'));
- }
- // should find the $cwd/node_modules when test the plugins under npm3
- lookupDirs.add(path.join(process.cwd(), 'node_modules'));
- // should find the siblings directory of framework when using pnpm
- for (let i = this.eggPaths.length - 1; i >= 0; i--) {
- const eggPath = this.eggPaths[i];
- // 'node_modules/.pnpm/yadan@2.0.0/node_modules/yadan/node_modules',
- // 'node_modules/.pnpm/yadan@2.0.0/node_modules', <- this is the sibling directory
- // 'node_modules/.pnpm/egg@2.33.1/node_modules/egg/node_modules',
- // 'node_modules/.pnpm/egg@2.33.1/node_modules', <- this is the sibling directory
- lookupDirs.add(path.dirname(eggPath));
- }
- for (let dir of lookupDirs) {
- dir = path.join(dir, name);
- if (fs.existsSync(dir)) {
- return fs.realpathSync(dir);
- }
- }
- throw new Error(`Can not find plugin ${name} in "${[ ...lookupDirs ].join(', ')}"`);
- },
- _extendPlugins(target, plugins) {
- if (!plugins) {
- return;
- }
- for (const name in plugins) {
- const plugin = plugins[name];
- let targetPlugin = target[name];
- if (!targetPlugin) {
- targetPlugin = target[name] = {};
- }
- if (targetPlugin.package && targetPlugin.package === plugin.package) {
- this.options.logger.warn('plugin %s has been defined that is %j, but you define again in %s',
- name, targetPlugin, plugin.from);
- }
- if (plugin.path || plugin.package) {
- delete targetPlugin.path;
- delete targetPlugin.package;
- }
- for (const prop in plugin) {
- if (plugin[prop] === undefined) {
- continue;
- }
- if (targetPlugin[prop] && Array.isArray(plugin[prop]) && !plugin[prop].length) {
- continue;
- }
- targetPlugin[prop] = plugin[prop];
- }
- }
- },
- };
- function depCompatible(plugin) {
- if (plugin.dep && !(Array.isArray(plugin.dependencies) && plugin.dependencies.length)) {
- plugin.dependencies = plugin.dep;
- delete plugin.dep;
- }
- }
|