From 74dcaab06cc1536fcc1f5aa41e67a3d57a39958c Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Sun, 12 Sep 2021 20:19:32 +0900 Subject: [PATCH 01/36] Migrate `PluginInstall` plugin logics into standalone command --- commands/plugin/install.js | 199 +++++++++++++++++++++++++++++++++ commands/plugin/lib/utils.js | 92 ++++++++++++++++ lib/plugins/index.js | 1 - lib/plugins/plugin/install.js | 201 ---------------------------------- scripts/serverless.js | 13 +++ 5 files changed, 304 insertions(+), 202 deletions(-) create mode 100644 commands/plugin/install.js create mode 100644 commands/plugin/lib/utils.js diff --git a/commands/plugin/install.js b/commands/plugin/install.js new file mode 100644 index 00000000000..c25a42dacc4 --- /dev/null +++ b/commands/plugin/install.js @@ -0,0 +1,199 @@ +'use strict'; + +const BbPromise = require('bluebird'); +const childProcess = BbPromise.promisifyAll(require('child_process')); +const fsp = require('fs').promises; +const fse = require('fs-extra'); +const path = require('path'); +const _ = require('lodash'); +const isPlainObject = require('type/plain-object/is'); +const yaml = require('js-yaml'); +const cloudformationSchema = require('@serverless/utils/cloudformation-schema'); +const log = require('@serverless/utils/log'); +const ServerlessError = require('../../lib/serverless-error'); +const yamlAstParser = require('../../lib/utils/yamlAstParser'); +const fileExists = require('../../lib/utils/fs/fileExists'); +const pluginUtils = require('./lib/utils'); +const npmCommandDeferred = require('../../lib/utils/npm-command-deferred'); +const CLI = require('../../lib/classes/CLI'); + +module.exports = async ({ configuration, serviceDir, configurationFilename, options }) => { + await new PluginInstall({ + configuration, + serviceDir, + configurationFilename, + options, + }).install(); +}; + +const requestManualUpdate = (serverlessFilePath) => + log(` + Can't automatically add plugin into "${path.basename(serverlessFilePath)}" file. + Please make it manually. +`); + +class PluginInstall { + constructor({ configuration, serviceDir, configurationFilename, options }) { + this.configuration = configuration; + this.serviceDir = serviceDir; + this.configurationFilename = configurationFilename; + this.options = options; + + this.cli = new CLI(undefined); + + Object.assign(this, pluginUtils); + } + + async install() { + const pluginInfo = pluginUtils.getPluginInfo(this.options.name); + this.options.pluginName = pluginInfo[0]; + this.options.pluginVersion = pluginInfo[1] || 'latest'; + + return BbPromise.bind(this) + .then(this.validate) + .then(this.getPlugins) + .then((plugins) => { + const plugin = plugins.find((item) => item.name === this.options.pluginName); + if (!plugin) { + this.cli.log('Plugin not found in serverless registry, continuing to install'); + } + return BbPromise.bind(this) + .then(this.pluginInstall) + .then(this.addPluginToServerlessFile) + .then(this.installPeerDependencies) + .then(() => { + const message = [ + 'Successfully installed', + ` "${this.options.pluginName}@${this.options.pluginVersion}"`, + ].join(''); + this.cli.log(message); + }); + }); + } + + async pluginInstall() { + const serviceDir = this.serviceDir; + const packageJsonFilePath = path.join(serviceDir, 'package.json'); + + return fileExists(packageJsonFilePath) + .then((exists) => { + // check if package.json is already present. Otherwise create one + if (!exists) { + this.cli.log('Creating an empty package.json file in your service directory'); + + const packageJsonFileContent = { + name: this.configuration.service, + description: '', + version: '0.1.0', + dependencies: {}, + devDependencies: {}, + }; + return fse.writeJson(packageJsonFilePath, packageJsonFileContent); + } + return BbPromise.resolve(); + }) + .then(() => { + // install the package through npm + const pluginFullName = `${this.options.pluginName}@${this.options.pluginVersion}`; + const message = [ + `Installing plugin "${pluginFullName}"`, + ' (this might take a few seconds...)', + ].join(''); + this.cli.log(message); + return this.npmInstall(pluginFullName); + }); + } + + async addPluginToServerlessFile() { + const serverlessFilePath = this.getServerlessFilePath(); + const fileExtension = path.extname(serverlessFilePath); + if (fileExtension === '.js' || fileExtension === '.ts') { + requestManualUpdate(serverlessFilePath); + return; + } + + const checkIsArrayPluginsObject = (pluginsObject) => + pluginsObject == null || Array.isArray(pluginsObject); + // pluginsObject type determined based on the value loaded during the serverless init. + if (_.last(serverlessFilePath.split('.')) === 'json') { + const serverlessFileObj = await fse.readJson(serverlessFilePath); + const newServerlessFileObj = serverlessFileObj; + const isArrayPluginsObject = checkIsArrayPluginsObject(newServerlessFileObj.plugins); + // null modules property is not supported + let plugins = isArrayPluginsObject + ? newServerlessFileObj.plugins || [] + : newServerlessFileObj.plugins.modules; + + if (plugins == null) { + throw new ServerlessError( + 'plugins modules property must be present', + 'PLUGINS_MODULES_MISSING' + ); + } + + plugins.push(this.options.pluginName); + plugins = _.sortedUniq(plugins); + + if (isArrayPluginsObject) { + newServerlessFileObj.plugins = plugins; + } else { + newServerlessFileObj.plugins.modules = plugins; + } + + await fse.writeJson(serverlessFilePath, newServerlessFileObj); + return; + } + + const serverlessFileObj = yaml.load(await fsp.readFile(serverlessFilePath, 'utf8'), { + filename: serverlessFilePath, + schema: cloudformationSchema, + }); + if (serverlessFileObj.plugins != null) { + // Plugins section can be behind veriables, opt-out in such case + if (isPlainObject(serverlessFileObj.plugins)) { + if ( + serverlessFileObj.plugins.modules != null && + !Array.isArray(serverlessFileObj.plugins.modules) + ) { + requestManualUpdate(serverlessFilePath); + return; + } + } else if (!Array.isArray(serverlessFileObj.plugins)) { + requestManualUpdate(serverlessFilePath); + return; + } + } + await yamlAstParser.addNewArrayItem( + serverlessFilePath, + checkIsArrayPluginsObject(serverlessFileObj.plugins) ? 'plugins' : 'plugins.modules', + this.options.pluginName + ); + } + + async installPeerDependencies() { + const pluginPackageJsonFilePath = path.join( + this.serviceDir, + 'node_modules', + this.options.pluginName, + 'package.json' + ); + return fse.readJson(pluginPackageJsonFilePath).then((pluginPackageJson) => { + if (pluginPackageJson.peerDependencies) { + const pluginsArray = []; + Object.entries(pluginPackageJson.peerDependencies).forEach(([k, v]) => { + pluginsArray.push(`${k}@"${v}"`); + }); + return BbPromise.map(pluginsArray, this.npmInstall); + } + return BbPromise.resolve(); + }); + } + + async npmInstall(name) { + return npmCommandDeferred.then((npmCommand) => + childProcess.execAsync(`${npmCommand} install --save-dev ${name}`, { + stdio: 'ignore', + }) + ); + } +} diff --git a/commands/plugin/lib/utils.js b/commands/plugin/lib/utils.js new file mode 100644 index 00000000000..6fd3c6cb4e5 --- /dev/null +++ b/commands/plugin/lib/utils.js @@ -0,0 +1,92 @@ +'use strict'; + +const path = require('path'); +const fetch = require('node-fetch'); +const BbPromise = require('bluebird'); +const HttpsProxyAgent = require('https-proxy-agent'); +const url = require('url'); +const chalk = require('chalk'); +const _ = require('lodash'); +const ServerlessError = require('../../../lib/serverless-error'); + +module.exports = { + async validate() { + if (!this.serviceDir) { + throw new ServerlessError( + 'This command can only be run inside a service directory', + 'MISSING_SERVICE_DIRECTORY' + ); + } + + return BbPromise.resolve(); + }, + + getServerlessFilePath() { + if (this.configurationFilename) { + return path.resolve(this.serviceDir, this.configurationFilename); + } + throw new ServerlessError( + 'Could not find any serverless service definition file.', + 'MISSING_SERVICE_CONFIGURATION_FILE' + ); + }, + + async getPlugins() { + const endpoint = 'https://raw.githubusercontent.com/serverless/plugins/master/plugins.json'; + + // Use HTTPS Proxy (Optional) + const proxy = + process.env.proxy || + process.env.HTTP_PROXY || + process.env.http_proxy || + process.env.HTTPS_PROXY || + process.env.https_proxy; + + const options = {}; + if (proxy) { + // not relying on recommended WHATWG URL + // due to missing support for it in https-proxy-agent + // https://github.com/TooTallNate/node-https-proxy-agent/issues/117 + options.agent = new HttpsProxyAgent(url.parse(proxy)); + } + + return fetch(endpoint, options) + .then((result) => result.json()) + .then((json) => json); + }, + + getPluginInfo(name) { + let pluginInfo; + if (name.startsWith('@')) { + pluginInfo = name.slice(1).split('@', 2); + pluginInfo[0] = `@${pluginInfo[0]}`; + } else { + pluginInfo = name.split('@', 2); + } + return pluginInfo; + }, + + async display(plugins) { + let message = ''; + if (plugins && plugins.length) { + // order plugins by name + const orderedPlugins = _.orderBy(plugins, ['name'], ['asc']); + orderedPlugins.forEach((plugin) => { + message += `${chalk.yellow.underline(plugin.name)} - ${plugin.description}\n`; + }); + // remove last two newlines for a prettier output + message = message.slice(0, -2); + this.cli.consoleLog(message); + this.cli.consoleLog(` +To install a plugin run 'serverless plugin install --name plugin-name-here' + +It will be automatically downloaded and added to your package.json and serverless.yml file + `); + } else { + message = 'There are no plugins available to display'; + this.cli.consoleLog(message); + } + + return BbPromise.resolve(message); + }, +}; diff --git a/lib/plugins/index.js b/lib/plugins/index.js index 1232a41e407..af1460c64a6 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -16,7 +16,6 @@ module.exports = [ require('./rollback.js'), require('./slstats.js'), require('./plugin/plugin.js'), - require('./plugin/install.js'), require('./plugin/uninstall.js'), require('./plugin/list.js'), require('./plugin/search.js'), diff --git a/lib/plugins/plugin/install.js b/lib/plugins/plugin/install.js index 3a5813c2333..e69de29bb2d 100644 --- a/lib/plugins/plugin/install.js +++ b/lib/plugins/plugin/install.js @@ -1,201 +0,0 @@ -'use strict'; - -const BbPromise = require('bluebird'); -const childProcess = BbPromise.promisifyAll(require('child_process')); -const fsp = require('fs').promises; -const fse = require('fs-extra'); -const path = require('path'); -const _ = require('lodash'); -const isPlainObject = require('type/plain-object/is'); -const yaml = require('js-yaml'); -const cloudformationSchema = require('@serverless/utils/cloudformation-schema'); -const log = require('@serverless/utils/log'); -const ServerlessError = require('../../serverless-error'); -const cliCommandsSchema = require('../../cli/commands-schema'); -const yamlAstParser = require('../../utils/yamlAstParser'); -const fileExists = require('../../utils/fs/fileExists'); -const pluginUtils = require('./lib/utils'); -const npmCommandDeferred = require('../../utils/npm-command-deferred'); - -const requestManualUpdate = (serverlessFilePath) => - log(` - Can't automatically add plugin into "${path.basename(serverlessFilePath)}" file. - Please make it manually. -`); - -class PluginInstall { - constructor(serverless, options) { - this.serverless = serverless; - this.options = options; - - Object.assign(this, pluginUtils); - - this.commands = { - plugin: { - commands: { - install: { - ...cliCommandsSchema.get('plugin install'), - }, - }, - }, - }; - this.hooks = { - 'plugin:install:install': async () => BbPromise.bind(this).then(this.install), - }; - } - - async install() { - const pluginInfo = pluginUtils.getPluginInfo(this.options.name); - this.options.pluginName = pluginInfo[0]; - this.options.pluginVersion = pluginInfo[1] || 'latest'; - - return BbPromise.bind(this) - .then(this.validate) - .then(this.getPlugins) - .then((plugins) => { - const plugin = plugins.find((item) => item.name === this.options.pluginName); - if (!plugin) { - this.serverless.cli.log('Plugin not found in serverless registry, continuing to install'); - } - return BbPromise.bind(this) - .then(this.pluginInstall) - .then(this.addPluginToServerlessFile) - .then(this.installPeerDependencies) - .then(() => { - const message = [ - 'Successfully installed', - ` "${this.options.pluginName}@${this.options.pluginVersion}"`, - ].join(''); - this.serverless.cli.log(message); - }); - }); - } - - async pluginInstall() { - const serviceDir = this.serverless.serviceDir; - const packageJsonFilePath = path.join(serviceDir, 'package.json'); - - return fileExists(packageJsonFilePath) - .then((exists) => { - // check if package.json is already present. Otherwise create one - if (!exists) { - this.serverless.cli.log('Creating an empty package.json file in your service directory'); - - const packageJsonFileContent = { - name: this.serverless.service.service, - description: '', - version: '0.1.0', - dependencies: {}, - devDependencies: {}, - }; - return fse.writeJson(packageJsonFilePath, packageJsonFileContent); - } - return BbPromise.resolve(); - }) - .then(() => { - // install the package through npm - const pluginFullName = `${this.options.pluginName}@${this.options.pluginVersion}`; - const message = [ - `Installing plugin "${pluginFullName}"`, - ' (this might take a few seconds...)', - ].join(''); - this.serverless.cli.log(message); - return this.npmInstall(pluginFullName); - }); - } - - async addPluginToServerlessFile() { - const serverlessFilePath = this.getServerlessFilePath(); - const fileExtension = path.extname(serverlessFilePath); - if (fileExtension === '.js' || fileExtension === '.ts') { - requestManualUpdate(serverlessFilePath); - return; - } - - const checkIsArrayPluginsObject = (pluginsObject) => - pluginsObject == null || Array.isArray(pluginsObject); - // pluginsObject type determined based on the value loaded during the serverless init. - if (_.last(serverlessFilePath.split('.')) === 'json') { - const serverlessFileObj = await fse.readJson(serverlessFilePath); - const newServerlessFileObj = serverlessFileObj; - const isArrayPluginsObject = checkIsArrayPluginsObject(newServerlessFileObj.plugins); - // null modules property is not supported - let plugins = isArrayPluginsObject - ? newServerlessFileObj.plugins || [] - : newServerlessFileObj.plugins.modules; - - if (plugins == null) { - throw new ServerlessError( - 'plugins modules property must be present', - 'PLUGINS_MODULES_MISSING' - ); - } - - plugins.push(this.options.pluginName); - plugins = _.sortedUniq(plugins); - - if (isArrayPluginsObject) { - newServerlessFileObj.plugins = plugins; - } else { - newServerlessFileObj.plugins.modules = plugins; - } - - await fse.writeJson(serverlessFilePath, newServerlessFileObj); - return; - } - - const serverlessFileObj = yaml.load(await fsp.readFile(serverlessFilePath, 'utf8'), { - filename: serverlessFilePath, - schema: cloudformationSchema, - }); - if (serverlessFileObj.plugins != null) { - // Plugins section can be behind veriables, opt-out in such case - if (isPlainObject(serverlessFileObj.plugins)) { - if ( - serverlessFileObj.plugins.modules != null && - !Array.isArray(serverlessFileObj.plugins.modules) - ) { - requestManualUpdate(serverlessFilePath); - return; - } - } else if (!Array.isArray(serverlessFileObj.plugins)) { - requestManualUpdate(serverlessFilePath); - return; - } - } - await yamlAstParser.addNewArrayItem( - serverlessFilePath, - checkIsArrayPluginsObject(serverlessFileObj.plugins) ? 'plugins' : 'plugins.modules', - this.options.pluginName - ); - } - - async installPeerDependencies() { - const pluginPackageJsonFilePath = path.join( - this.serverless.serviceDir, - 'node_modules', - this.options.pluginName, - 'package.json' - ); - return fse.readJson(pluginPackageJsonFilePath).then((pluginPackageJson) => { - if (pluginPackageJson.peerDependencies) { - const pluginsArray = []; - Object.entries(pluginPackageJson.peerDependencies).forEach(([k, v]) => { - pluginsArray.push(`${k}@"${v}"`); - }); - return BbPromise.map(pluginsArray, this.npmInstall); - } - return BbPromise.resolve(); - }); - } - - async npmInstall(name) { - return npmCommandDeferred.then((npmCommand) => - childProcess.execAsync(`${npmCommand} install --save-dev ${name}`, { - stdio: 'ignore', - }) - ); - } -} - -module.exports = PluginInstall; diff --git a/scripts/serverless.js b/scripts/serverless.js index a09c6cf59e4..c9eca13969f 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -41,6 +41,8 @@ let hasTelemetryBeenReported = false; // to propery handle e.g. `SIGINT` interrupt const keepAliveTimer = setTimeout(() => {}, 60 * 60 * 1000); +const externalCommands = ['plugin install']; + process.once('uncaughtException', (error) => { clearTimeout(keepAliveTimer); progress.clear(); @@ -495,6 +497,17 @@ const processSpanPromise = (async () => { const configurationFilename = configuration && configurationPath.slice(serviceDir.length + 1); + if (externalCommands.includes(command)) { + require('../lib/cli/ensure-supported-command')(configuration); + require(`../commands/${commands.join('/')}`)({ + configuration, + serviceDir, + configurationFilename, + options, + }); + return; + } + if (isInteractiveSetup) { require('../lib/cli/ensure-supported-command')(configuration); From 50515811293fd61bc594245a328eeb6749a10819 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Mon, 13 Sep 2021 22:16:14 +0900 Subject: [PATCH 02/36] Wait async-able standalone commands run --- scripts/serverless.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/serverless.js b/scripts/serverless.js index c9eca13969f..951aabb973a 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -499,7 +499,7 @@ const processSpanPromise = (async () => { if (externalCommands.includes(command)) { require('../lib/cli/ensure-supported-command')(configuration); - require(`../commands/${commands.join('/')}`)({ + await require(`../commands/${commands.join('/')}`)({ configuration, serviceDir, configurationFilename, From 411ce044b745be14ad0abe62e43f3f34cee91695 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Sun, 19 Sep 2021 18:33:08 +0900 Subject: [PATCH 03/36] Rely on native promises --- commands/plugin/install.js | 50 +++++++++++++++++------------------- commands/plugin/lib/utils.js | 5 ++-- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index c25a42dacc4..9df8bf6f98b 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -1,13 +1,13 @@ 'use strict'; -const BbPromise = require('bluebird'); -const childProcess = BbPromise.promisifyAll(require('child_process')); +const childProcess = require('child_process'); const fsp = require('fs').promises; const fse = require('fs-extra'); const path = require('path'); const _ = require('lodash'); const isPlainObject = require('type/plain-object/is'); const yaml = require('js-yaml'); +const { promisify } = require('util'); const cloudformationSchema = require('@serverless/utils/cloudformation-schema'); const log = require('@serverless/utils/log'); const ServerlessError = require('../../lib/serverless-error'); @@ -17,6 +17,8 @@ const pluginUtils = require('./lib/utils'); const npmCommandDeferred = require('../../lib/utils/npm-command-deferred'); const CLI = require('../../lib/classes/CLI'); +const execAsync = promisify(childProcess.exec); + module.exports = async ({ configuration, serviceDir, configurationFilename, options }) => { await new PluginInstall({ configuration, @@ -49,26 +51,22 @@ class PluginInstall { this.options.pluginName = pluginInfo[0]; this.options.pluginVersion = pluginInfo[1] || 'latest'; - return BbPromise.bind(this) - .then(this.validate) - .then(this.getPlugins) - .then((plugins) => { - const plugin = plugins.find((item) => item.name === this.options.pluginName); - if (!plugin) { - this.cli.log('Plugin not found in serverless registry, continuing to install'); - } - return BbPromise.bind(this) - .then(this.pluginInstall) - .then(this.addPluginToServerlessFile) - .then(this.installPeerDependencies) - .then(() => { - const message = [ - 'Successfully installed', - ` "${this.options.pluginName}@${this.options.pluginVersion}"`, - ].join(''); - this.cli.log(message); - }); - }); + await this.validate(); + const plugins = await this.getPlugins(); + const plugin = plugins.find((item) => item.name === this.options.pluginName); + if (!plugin) { + this.cli.log('Plugin not found in serverless registry, continuing to install'); + } + + await this.pluginInstall(); + await this.addPluginToServerlessFile(); + await this.installPeerDependencies(); + + const message = [ + 'Successfully installed', + ` "${this.options.pluginName}@${this.options.pluginVersion}"`, + ].join(''); + this.cli.log(message); } async pluginInstall() { @@ -90,7 +88,7 @@ class PluginInstall { }; return fse.writeJson(packageJsonFilePath, packageJsonFileContent); } - return BbPromise.resolve(); + return Promise.resolve(); }) .then(() => { // install the package through npm @@ -183,15 +181,15 @@ class PluginInstall { Object.entries(pluginPackageJson.peerDependencies).forEach(([k, v]) => { pluginsArray.push(`${k}@"${v}"`); }); - return BbPromise.map(pluginsArray, this.npmInstall); + return Promise.all(pluginsArray.map((plugin) => this.npmInstall(plugin))); } - return BbPromise.resolve(); + return Promise.resolve(); }); } async npmInstall(name) { return npmCommandDeferred.then((npmCommand) => - childProcess.execAsync(`${npmCommand} install --save-dev ${name}`, { + execAsync(`${npmCommand} install --save-dev ${name}`, { stdio: 'ignore', }) ); diff --git a/commands/plugin/lib/utils.js b/commands/plugin/lib/utils.js index 6fd3c6cb4e5..5ceb891184a 100644 --- a/commands/plugin/lib/utils.js +++ b/commands/plugin/lib/utils.js @@ -2,7 +2,6 @@ const path = require('path'); const fetch = require('node-fetch'); -const BbPromise = require('bluebird'); const HttpsProxyAgent = require('https-proxy-agent'); const url = require('url'); const chalk = require('chalk'); @@ -18,7 +17,7 @@ module.exports = { ); } - return BbPromise.resolve(); + return Promise.resolve(); }, getServerlessFilePath() { @@ -87,6 +86,6 @@ It will be automatically downloaded and added to your package.json and serverles this.cli.consoleLog(message); } - return BbPromise.resolve(message); + return Promise.resolve(message); }, }; From 5fbac5772dd5e4b80ac96aa553471bf1830f6f90 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Sun, 19 Sep 2021 18:38:50 +0900 Subject: [PATCH 04/36] Keep all private modules in `/lib` --- commands/plugin/install.js | 2 +- .../plugin/lib/utils.js => lib/commands/plugin-management.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename commands/plugin/lib/utils.js => lib/commands/plugin-management.js (97%) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index 9df8bf6f98b..aa3915685e8 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -13,7 +13,7 @@ const log = require('@serverless/utils/log'); const ServerlessError = require('../../lib/serverless-error'); const yamlAstParser = require('../../lib/utils/yamlAstParser'); const fileExists = require('../../lib/utils/fs/fileExists'); -const pluginUtils = require('./lib/utils'); +const pluginUtils = require('../../lib/commands/plugin-management'); const npmCommandDeferred = require('../../lib/utils/npm-command-deferred'); const CLI = require('../../lib/classes/CLI'); diff --git a/commands/plugin/lib/utils.js b/lib/commands/plugin-management.js similarity index 97% rename from commands/plugin/lib/utils.js rename to lib/commands/plugin-management.js index 5ceb891184a..a89ecb056a4 100644 --- a/commands/plugin/lib/utils.js +++ b/lib/commands/plugin-management.js @@ -6,7 +6,7 @@ const HttpsProxyAgent = require('https-proxy-agent'); const url = require('url'); const chalk = require('chalk'); const _ = require('lodash'); -const ServerlessError = require('../../../lib/serverless-error'); +const ServerlessError = require('../../lib/serverless-error'); module.exports = { async validate() { From 8179d009e207c6726a954cd92236f971a98f1ce5 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Sun, 19 Sep 2021 18:42:35 +0900 Subject: [PATCH 05/36] Choose better variable naming --- scripts/serverless.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/serverless.js b/scripts/serverless.js index 951aabb973a..a42cd3ae606 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -41,7 +41,7 @@ let hasTelemetryBeenReported = false; // to propery handle e.g. `SIGINT` interrupt const keepAliveTimer = setTimeout(() => {}, 60 * 60 * 1000); -const externalCommands = ['plugin install']; +const standaloneCommands = ['plugin install']; process.once('uncaughtException', (error) => { clearTimeout(keepAliveTimer); @@ -497,7 +497,7 @@ const processSpanPromise = (async () => { const configurationFilename = configuration && configurationPath.slice(serviceDir.length + 1); - if (externalCommands.includes(command)) { + if (standaloneCommands.includes(command)) { require('../lib/cli/ensure-supported-command')(configuration); await require(`../commands/${commands.join('/')}`)({ configuration, From 2a62902cd1382a58f5d69e16c3a6e6ba081449ac Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 12:25:36 +0900 Subject: [PATCH 06/36] Add unit test for "plugin install" command --- test/unit/commands/plugin/install.test.js | 83 +++ test/unit/lib/plugins/plugin/install.test.js | 561 ------------------- 2 files changed, 83 insertions(+), 561 deletions(-) create mode 100644 test/unit/commands/plugin/install.test.js delete mode 100644 test/unit/lib/plugins/plugin/install.test.js diff --git a/test/unit/commands/plugin/install.test.js b/test/unit/commands/plugin/install.test.js new file mode 100644 index 00000000000..b738acc5573 --- /dev/null +++ b/test/unit/commands/plugin/install.test.js @@ -0,0 +1,83 @@ +'use strict'; + +const chai = require('chai'); +const sinon = require('sinon'); +const yaml = require('js-yaml'); +const path = require('path'); +const fse = require('fs-extra'); +const proxyquire = require('proxyquire'); +const fixturesEngine = require('../../../fixtures/programmatic'); +const resolveConfigurationPath = require('../../../../lib/cli/resolve-configuration-path'); +const { expect } = require('chai'); + +chai.use(require('chai-as-promised')); + +const npmCommand = 'npm'; + +describe.only('test/unit/commands/plugin/install.test.js', async () => { + let execFake; + let serviceDir; + let configurationFilePath; + + const pluginName = 'serverless-plugin-1'; + + before(async () => { + execFake = sinon.fake(async (command, ...args) => { + if (command.startsWith(`${npmCommand} install --save-dev`)) { + const _pluginName = command.split(' ')[3]; + const pluginNameWithoutVersion = _pluginName.split('@')[0]; + + if (pluginNameWithoutVersion) { + const pluginPackageJsonFilePath = path.join( + serviceDir, + 'node_modules', + pluginName, + 'package.json' + ); + const packageJsonFileContent = {}; + await fse.ensureFile(pluginPackageJsonFilePath); + await fse.writeJson(pluginPackageJsonFilePath, packageJsonFileContent); + } + } + const callback = args[args.length - 1]; + callback(); + }); + const installPlugin = proxyquire('../../../../commands/plugin/install', { + child_process: { + exec: execFake, + }, + }); + + const fixture = await fixturesEngine.setup('function'); + + const configuration = fixture.serviceConfig; + serviceDir = fixture.servicePath; + configurationFilePath = await resolveConfigurationPath({ + cwd: serviceDir, + }); + const configurationFilename = configurationFilePath.slice(serviceDir.length + 1); + const options = { + name: pluginName, + }; + + await installPlugin({ + configuration, + serviceDir, + configurationFilename, + options, + }); + }); + + it('should install plugin', () => { + const command = execFake.firstArg; + const expectedCommand = `${npmCommand} install --save-dev ${pluginName}`; + expect(command).to.have.string(expectedCommand); + }); + + it('should add plugin to serverless file', async () => { + const serverlessFileObj = yaml.load(await fse.readFile(configurationFilePath, 'utf8'), { + filename: configurationFilePath, + }); + expect(serverlessFileObj.plugins).to.include(pluginName); + }); +}); diff --git a/test/unit/lib/plugins/plugin/install.test.js b/test/unit/lib/plugins/plugin/install.test.js deleted file mode 100644 index c8124ecf641..00000000000 --- a/test/unit/lib/plugins/plugin/install.test.js +++ /dev/null @@ -1,561 +0,0 @@ -'use strict'; - -const chai = require('chai'); -const sinon = require('sinon'); -const BbPromise = require('bluebird'); -const yaml = require('js-yaml'); -const path = require('path'); -const childProcess = BbPromise.promisifyAll(require('child_process')); -const fs = require('fs'); -const fse = require('fs-extra'); -const PluginInstall = require('../../../../../lib/plugins/plugin/install'); -const Serverless = require('../../../../../lib/Serverless'); -const CLI = require('../../../../../lib/classes/CLI'); -const { expect } = require('chai'); -const { getTmpDirPath } = require('../../../../utils/fs'); - -chai.use(require('chai-as-promised')); - -describe('PluginInstall', () => { - let pluginInstall; - let serverless; - let serverlessErrorStub; - const plugins = [ - { - name: '@scope/serverless-plugin-1', - description: 'Scoped Serverless Plugin 1', - githubUrl: 'https://github.com/serverless/serverless-plugin-1', - }, - { - name: 'serverless-plugin-1', - description: 'Serverless Plugin 1', - githubUrl: 'https://github.com/serverless/serverless-plugin-1', - }, - { - name: 'serverless-plugin-2', - description: 'Serverless Plugin 2', - githubUrl: 'https://github.com/serverless/serverless-plugin-2', - }, - { - name: 'serverless-existing-plugin', - description: 'Serverless Existing plugin', - githubUrl: 'https://github.com/serverless/serverless-existing-plugin', - }, - ]; - - beforeEach(() => { - serverless = new Serverless(); - serverless.cli = new CLI(serverless); - const options = {}; - pluginInstall = new PluginInstall(serverless, options); - serverlessErrorStub = sinon.stub(serverless.classes, 'Error').throws(); - }); - - afterEach(() => { - serverless.classes.Error.restore(); - }); - - describe('#constructor()', () => { - let installStub; - - beforeEach(() => { - installStub = sinon.stub(pluginInstall, 'install').returns(BbPromise.resolve()); - }); - - afterEach(() => { - pluginInstall.install.restore(); - }); - - it('should have the sub-command "install"', () => { - expect(pluginInstall.commands.plugin.commands.install).to.not.equal(undefined); - }); - - it('should have the lifecycle event "install" for the "install" sub-command', () => { - expect(pluginInstall.commands.plugin.commands.install.lifecycleEvents).to.deep.equal([ - 'install', - ]); - }); - - it('should have a required option "name" for the "install" sub-command', () => { - // eslint-disable-next-line no-unused-expressions - expect(pluginInstall.commands.plugin.commands.install.options.name.required).to.be.true; - }); - - it('should have a "plugin:install:install" hook', () => { - expect(pluginInstall.hooks['plugin:install:install']).to.not.equal(undefined); - }); - - it('should run promise chain in order for "plugin:install:install" hook', () => - pluginInstall.hooks['plugin:install:install']().then(() => { - expect(installStub.calledOnce).to.equal(true); - })); - }); - - describe('#install()', () => { - let serviceDir; - let serverlessYmlFilePath; - let pluginInstallStub; - let validateStub; - let getPluginsStub; - let savedCwd; - let addPluginToServerlessFileStub; - let installPeerDependenciesStub; - - beforeEach(() => { - serviceDir = getTmpDirPath(); - pluginInstall.serverless.serviceDir = serviceDir; - fse.ensureDirSync(serviceDir); - serverlessYmlFilePath = path.join(serviceDir, 'serverless.yml'); - validateStub = sinon.stub(pluginInstall, 'validate').returns(BbPromise.resolve()); - pluginInstallStub = sinon.stub(pluginInstall, 'pluginInstall').returns(BbPromise.resolve()); - addPluginToServerlessFileStub = sinon - .stub(pluginInstall, 'addPluginToServerlessFile') - .returns(BbPromise.resolve()); - installPeerDependenciesStub = sinon - .stub(pluginInstall, 'installPeerDependencies') - .returns(BbPromise.resolve()); - getPluginsStub = sinon.stub(pluginInstall, 'getPlugins').returns(BbPromise.resolve(plugins)); - // save the cwd so that we can restore it later - savedCwd = process.cwd(); - process.chdir(serviceDir); - }); - - afterEach(() => { - pluginInstall.validate.restore(); - pluginInstall.getPlugins.restore(); - pluginInstall.pluginInstall.restore(); - pluginInstall.addPluginToServerlessFile.restore(); - pluginInstall.installPeerDependencies.restore(); - process.chdir(savedCwd); - }); - - it('should install the plugin if it can be found in the registry', () => { - // serverless.yml - const serverlessYml = { - service: 'plugin-service', - provider: 'aws', - }; - serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); - - pluginInstall.options.name = 'serverless-plugin-1'; - - return expect(pluginInstall.install()).to.be.fulfilled.then(() => { - expect(validateStub.calledOnce).to.equal(true); - expect(getPluginsStub.calledOnce).to.equal(true); - expect(pluginInstallStub.calledOnce).to.equal(true); - expect(serverlessErrorStub.calledOnce).to.equal(false); - expect(addPluginToServerlessFileStub.calledOnce).to.equal(true); - expect(installPeerDependenciesStub.calledOnce).to.equal(true); - }); - }); - - it('should install a scoped plugin if it can be found in the registry', () => { - // serverless.yml - const serverlessYml = { - service: 'plugin-service', - provider: 'aws', - }; - serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); - - pluginInstall.options.name = '@scope/serverless-plugin-1'; - - return expect(pluginInstall.install()).to.be.fulfilled.then(() => { - expect(validateStub.calledOnce).to.equal(true); - expect(getPluginsStub.calledOnce).to.equal(true); - expect(pluginInstallStub.calledOnce).to.equal(true); - expect(serverlessErrorStub.calledOnce).to.equal(false); - expect(addPluginToServerlessFileStub.calledOnce).to.equal(true); - expect(installPeerDependenciesStub.calledOnce).to.equal(true); - }); - }); - - it('should install the plugin even if it can not be found in the registry', () => { - // serverless.yml - const serverlessYml = { - service: 'plugin-service', - provider: 'aws', - }; - serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); - - pluginInstall.options.name = 'serverless-not-in-registry-plugin'; - return expect(pluginInstall.install()).to.be.fulfilled.then(() => { - expect(validateStub.calledOnce).to.equal(true); - expect(getPluginsStub.calledOnce).to.equal(true); - expect(pluginInstallStub.calledOnce).to.equal(true); - expect(serverlessErrorStub.calledOnce).to.equal(false); - expect(addPluginToServerlessFileStub.calledOnce).to.equal(true); - expect(installPeerDependenciesStub.calledOnce).to.equal(true); - }); - }); - - it('should apply the latest version if you can not get the version from name option', () => { - const serverlessYml = { - service: 'plugin-service', - provider: 'aws', - }; - serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); - pluginInstall.options.name = 'serverless-plugin-1'; - return expect(pluginInstall.install()).to.be.fulfilled.then(() => { - expect(pluginInstall.options.pluginName).to.be.equal('serverless-plugin-1'); - expect(pluginInstall.options.pluginVersion).to.be.equal('latest'); - }); - }); - - it( - 'should apply the latest version if you can not get the ' + - 'version from name option even if scoped', - () => { - const serverlessYml = { - service: 'plugin-service', - provider: 'aws', - }; - serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); - pluginInstall.options.name = '@scope/serverless-plugin-1'; - return expect(pluginInstall.install()).to.be.fulfilled.then(() => { - expect(pluginInstall.options.pluginName).to.be.equal('@scope/serverless-plugin-1'); - expect(pluginInstall.options.pluginVersion).to.be.equal('latest'); - }); - } - ); - - it('should apply the specified version if you can get the version from name option', () => { - const serverlessYml = { - service: 'plugin-service', - provider: 'aws', - }; - serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); - pluginInstall.options.name = 'serverless-plugin-1@1.0.0'; - return expect(pluginInstall.install()).to.be.fulfilled.then(() => { - expect(pluginInstall.options.pluginName).to.be.equal('serverless-plugin-1'); - expect(pluginInstall.options.pluginVersion).to.be.equal('1.0.0'); - }); - }); - }); - - describe('#pluginInstall()', () => { - let serviceDir; - let packageJsonFilePath; - let npmInstallStub; - let savedCwd; - - beforeEach(() => { - pluginInstall.options.pluginName = 'serverless-plugin-1'; - pluginInstall.options.pluginVersion = 'latest'; - serviceDir = getTmpDirPath(); - pluginInstall.serverless.serviceDir = serviceDir; - fse.ensureDirSync(serviceDir); - packageJsonFilePath = path.join(serviceDir, 'package.json'); - npmInstallStub = sinon.stub(childProcess, 'execAsync').callsFake(() => { - const packageJson = serverless.utils.readFileSync(packageJsonFilePath, 'utf8'); - packageJson.devDependencies = { - 'serverless-plugin-1': 'latest', - }; - serverless.utils.writeFileSync(packageJsonFilePath, packageJson); - return BbPromise.resolve(); - }); - - // save the cwd so that we can restore it later - savedCwd = process.cwd(); - process.chdir(serviceDir); - }); - - afterEach(() => { - childProcess.execAsync.restore(); - process.chdir(savedCwd); - }); - - it('should install the plugin if it has not been installed yet', () => { - const packageJson = { - name: 'test-service', - description: '', - version: '0.1.0', - dependencies: {}, - devDependencies: {}, - }; - - serverless.utils.writeFileSync(packageJsonFilePath, packageJson); - - return expect(pluginInstall.pluginInstall()).to.be.fulfilled.then(() => - Promise.all([ - expect( - npmInstallStub.calledWithExactly('npm install --save-dev serverless-plugin-1@latest', { - stdio: 'ignore', - }) - ).to.equal(true), - expect(serverlessErrorStub.calledOnce).to.equal(false), - ]) - ); - }); - - it('should generate a package.json file in the service directory if not present', () => - expect(pluginInstall.pluginInstall()).to.be.fulfilled.then(() => { - expect( - npmInstallStub.calledWithExactly('npm install --save-dev serverless-plugin-1@latest', { - stdio: 'ignore', - }) - ).to.equal(true); - expect(fs.existsSync(packageJsonFilePath)).to.equal(true); - })); - }); - - describe('#addPluginToServerlessFile()', () => { - let serviceDir; - let serverlessYmlFilePath; - - beforeEach(() => { - serviceDir = getTmpDirPath(); - pluginInstall.serverless.serviceDir = pluginInstall.serverless.serviceDir = serviceDir; - pluginInstall.serverless.configurationFilename = 'serverless.yml'; - serverlessYmlFilePath = path.join(serviceDir, 'serverless.yml'); - }); - - it('should add the plugin to the service file if plugins array is not present', () => { - // serverless.yml - const serverlessYml = { - service: 'plugin-service', - provider: 'aws', - // no plugins array here - }; - serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); - - pluginInstall.options.pluginName = 'serverless-plugin-1'; - - return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')).to.deep.equal( - Object.assign({}, serverlessYml, { - plugins: ['serverless-plugin-1'], - }) - ); - }); - }); - - it('should push the plugin to the service files plugin array if present', () => { - // serverless.yml - const serverlessYml = { - service: 'plugin-service', - provider: 'aws', - plugins: ['serverless-existing-plugin'], // one plugin was already added - }; - serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); - - pluginInstall.options.pluginName = 'serverless-plugin-1'; - - return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')).to.deep.equal( - Object.assign({}, serverlessYml, { - plugins: ['serverless-existing-plugin', 'serverless-plugin-1'], - }) - ); - }); - }); - - it('should add the plugin to serverless file path for a .yaml file', () => { - const serverlessYamlFilePath = path.join(serviceDir, 'serverless.yaml'); - pluginInstall.serverless.configurationFilename = 'serverless.yaml'; - const serverlessYml = { - service: 'plugin-service', - provider: 'aws', - }; - serverless.utils.writeFileSync(serverlessYamlFilePath, yaml.dump(serverlessYml)); - pluginInstall.options.pluginName = 'serverless-plugin-1'; - return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessYamlFilePath, 'utf8')).to.deep.equal( - Object.assign({}, serverlessYml, { plugins: ['serverless-plugin-1'] }) - ); - }); - }); - - it('should add the plugin to serverless file path for a .json file', () => { - const serverlessJsonFilePath = path.join(serviceDir, 'serverless.json'); - pluginInstall.serverless.configurationFilename = 'serverless.json'; - const serverlessJson = { - service: 'plugin-service', - provider: 'aws', - }; - serverless.utils.writeFileSync(serverlessJsonFilePath, serverlessJson); - pluginInstall.options.pluginName = 'serverless-plugin-1'; - return expect(pluginInstall.addPluginToServerlessFile()) - .to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal( - Object.assign({}, serverlessJson, { plugins: ['serverless-plugin-1'] }) - ); - }) - .then(() => { - pluginInstall.options.pluginName = 'serverless-plugin-2'; - return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal( - Object.assign({}, serverlessJson, { - plugins: ['serverless-plugin-1', 'serverless-plugin-2'], - }) - ); - }); - }); - }); - - it('should not modify serverless .js file', async () => { - const serverlessJsFilePath = path.join(serviceDir, 'serverless.js'); - pluginInstall.serverless.configurationFilename = 'serverless.js'; - const serverlessJson = { - service: 'plugin-service', - provider: 'aws', - plugins: [], - }; - serverless.utils.writeFileSync( - serverlessJsFilePath, - `module.exports = ${JSON.stringify(serverlessJson)};` - ); - pluginInstall.options.pluginName = 'serverless-plugin-1'; - await pluginInstall.addPluginToServerlessFile(); - // use require to load serverless.js - // eslint-disable-next-line global-require - expect(require(serverlessJsFilePath).plugins).to.be.deep.equal([]); - }); - - it('should not modify serverless .ts file', () => { - const serverlessTsFilePath = path.join(serviceDir, 'serverless.ts'); - pluginInstall.serverless.configurationFilename = 'serverless.ts'; - const serverlessJson = { - service: 'plugin-service', - provider: 'aws', - plugins: [], - }; - serverless.utils.writeFileSync( - serverlessTsFilePath, - `module.exports = ${JSON.stringify(serverlessJson)};` - ); - pluginInstall.options.pluginName = 'serverless-plugin-1'; - return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - // use require to load serverless.js - // eslint-disable-next-line global-require - expect(require(serverlessTsFilePath).plugins).to.be.deep.equal([]); - }); - }); - - describe('if plugins object is not array', () => { - it('should add the plugin to the service file', () => { - // serverless.yml - const serverlessYml = { - service: 'plugin-service', - provider: 'aws', - plugins: { - localPath: 'test', - modules: [], - }, - }; - serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); - - pluginInstall.options.pluginName = 'serverless-plugin-1'; - - return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')).to.deep.equal( - Object.assign({}, serverlessYml, { - plugins: { - localPath: 'test', - modules: [pluginInstall.options.pluginName], - }, - }) - ); - }); - }); - - it('should add the plugin to serverless file path for a .json file', () => { - const serverlessJsonFilePath = path.join(serviceDir, 'serverless.json'); - pluginInstall.serverless.configurationFilename = 'serverless.json'; - const serverlessJson = { - service: 'plugin-service', - provider: 'aws', - plugins: { - localPath: 'test', - modules: [], - }, - }; - serverless.utils.writeFileSync(serverlessJsonFilePath, serverlessJson); - pluginInstall.options.pluginName = 'serverless-plugin-1'; - return expect(pluginInstall.addPluginToServerlessFile()) - .to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal( - Object.assign({}, serverlessJson, { - plugins: { - localPath: 'test', - modules: [pluginInstall.options.pluginName], - }, - }) - ); - }) - .then(() => { - pluginInstall.options.pluginName = 'serverless-plugin-2'; - return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { - expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal( - Object.assign({}, serverlessJson, { - plugins: { - localPath: 'test', - modules: ['serverless-plugin-1', 'serverless-plugin-2'], - }, - }) - ); - }); - }); - }); - }); - }); - - describe('#installPeerDependencies()', () => { - let serviceDir; - let servicePackageJsonFilePath; - let pluginPath; - let pluginPackageJsonFilePath; - let pluginName; - let npmInstallStub; - let savedCwd; - - beforeEach(() => { - pluginName = 'some-plugin'; - pluginInstall.options.pluginName = pluginName; - serviceDir = getTmpDirPath(); - fse.ensureDirSync(serviceDir); - pluginInstall.serverless.serviceDir = serviceDir; - servicePackageJsonFilePath = path.join(serviceDir, 'package.json'); - fse.writeJsonSync(servicePackageJsonFilePath, { - devDependencies: {}, - }); - pluginPath = path.join(serviceDir, 'node_modules', pluginName); - fse.ensureDirSync(pluginPath); - pluginPackageJsonFilePath = path.join(pluginPath, 'package.json'); - npmInstallStub = sinon.stub(childProcess, 'execAsync').returns(BbPromise.resolve()); - savedCwd = process.cwd(); - process.chdir(serviceDir); - }); - - afterEach(() => { - childProcess.execAsync.restore(); - process.chdir(savedCwd); - }); - - it('should install peerDependencies if an installed plugin has ones', () => { - fse.writeJsonSync(pluginPackageJsonFilePath, { - peerDependencies: { - 'some-package': '*', - }, - }); - return expect(pluginInstall.installPeerDependencies()).to.be.fulfilled.then(() => { - expect( - npmInstallStub.calledWithExactly('npm install --save-dev some-package@"*"', { - stdio: 'ignore', - }) - ).to.equal(true); - }); - }); - - it('should not install peerDependencies if an installed plugin does not have ones', () => { - fse.writeJsonSync(pluginPackageJsonFilePath, {}); - return expect(pluginInstall.installPeerDependencies()).to.be.fulfilled.then(() => { - expect(fse.readJsonSync(servicePackageJsonFilePath)).to.be.deep.equal({ - devDependencies: {}, - }); - expect(npmInstallStub.calledWithExactly('npm install', { stdio: 'ignore' })).to.equal( - false - ); - }); - }); - }); -}); From 8864e882e232f7f65e5b221750b66a55f5007b7c Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 12:27:14 +0900 Subject: [PATCH 07/36] Unset `only` on mocha test --- test/unit/commands/plugin/install.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/commands/plugin/install.test.js b/test/unit/commands/plugin/install.test.js index b738acc5573..1ba0e0d02ef 100644 --- a/test/unit/commands/plugin/install.test.js +++ b/test/unit/commands/plugin/install.test.js @@ -14,7 +14,7 @@ chai.use(require('chai-as-promised')); const npmCommand = 'npm'; -describe.only('test/unit/commands/plugin/install.test.js', async () => { +describe('test/unit/commands/plugin/install.test.js', async () => { let execFake; let serviceDir; let configurationFilePath; From 3b26b4aa79fb6fa1fa598569e759b01328aba3f9 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 12:46:19 +0900 Subject: [PATCH 08/36] Update module pathes in document --- test/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/README.md b/test/README.md index 0d124be37ca..ef0ee90fb77 100644 --- a/test/README.md +++ b/test/README.md @@ -61,7 +61,7 @@ Check existing set of AWS integration tests at [test/integration](./integration) Pass test file to Mocha directly as follows ``` -AWS_ACCESS_KEY_ID=XXX AWS_SECRET_ACCESS_KEY=xxx npx mocha tests/integration/{chosen}.test.js +AWS_ACCESS_KEY_ID=XXX AWS_SECRET_ACCESS_KEY=xxx npx mocha test/integration/{chosen}.test.js ``` ### Tests that depend on shared infrastructure stack @@ -90,13 +90,13 @@ To run all integration tests run: To run only a specific integration test run: ``` -tests/templates/integration-test-template TEMPLATE_NAME BUILD_COMMAND +test/templates/integration-test-template TEMPLATE_NAME BUILD_COMMAND ``` so for example: ``` -tests/templates/integration-test-template aws-java-maven mvn package +test/templates/integration-test-template aws-java-maven mvn package ``` If you add a new template make sure to add it to the `test-all-templates` file and configure the `docker-compose.yml` file for your template. From b2017bdba3fe866d089dcdbdd89259791e7710a3 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 12:49:24 +0900 Subject: [PATCH 09/36] Rely on async/await syntax --- commands/plugin/install.js | 50 ++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index aa3915685e8..9ec1f73db9a 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -73,33 +73,29 @@ class PluginInstall { const serviceDir = this.serviceDir; const packageJsonFilePath = path.join(serviceDir, 'package.json'); - return fileExists(packageJsonFilePath) - .then((exists) => { - // check if package.json is already present. Otherwise create one - if (!exists) { - this.cli.log('Creating an empty package.json file in your service directory'); - - const packageJsonFileContent = { - name: this.configuration.service, - description: '', - version: '0.1.0', - dependencies: {}, - devDependencies: {}, - }; - return fse.writeJson(packageJsonFilePath, packageJsonFileContent); - } - return Promise.resolve(); - }) - .then(() => { - // install the package through npm - const pluginFullName = `${this.options.pluginName}@${this.options.pluginVersion}`; - const message = [ - `Installing plugin "${pluginFullName}"`, - ' (this might take a few seconds...)', - ].join(''); - this.cli.log(message); - return this.npmInstall(pluginFullName); - }); + // check if package.json is already present. Otherwise create one + const exists = await fileExists(packageJsonFilePath); + if (!exists) { + this.cli.log('Creating an empty package.json file in your service directory'); + + const packageJsonFileContent = { + name: this.configuration.service, + description: '', + version: '0.1.0', + dependencies: {}, + devDependencies: {}, + }; + await fse.writeJson(packageJsonFilePath, packageJsonFileContent); + } + + // install the package through npm + const pluginFullName = `${this.options.pluginName}@${this.options.pluginVersion}`; + const message = [ + `Installing plugin "${pluginFullName}"`, + ' (this might take a few seconds...)', + ].join(''); + this.cli.log(message); + await this.npmInstall(pluginFullName); } async addPluginToServerlessFile() { From 65dd658822c3cbf2c189054b23b89d2294b84260 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 12:55:15 +0900 Subject: [PATCH 10/36] Rely on async/await syntax --- commands/plugin/install.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index 9ec1f73db9a..04ee2e37b1d 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -184,10 +184,9 @@ class PluginInstall { } async npmInstall(name) { - return npmCommandDeferred.then((npmCommand) => - execAsync(`${npmCommand} install --save-dev ${name}`, { - stdio: 'ignore', - }) - ); + const npmCommand = await npmCommandDeferred; + execAsync(`${npmCommand} install --save-dev ${name}`, { + stdio: 'ignore', + }); } } From c1b1c9a8b2955a655cfe612cf1fea84fd1c37c6c Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 12:56:51 +0900 Subject: [PATCH 11/36] Return promise to be waited --- commands/plugin/install.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index 04ee2e37b1d..3ead9124712 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -185,7 +185,7 @@ class PluginInstall { async npmInstall(name) { const npmCommand = await npmCommandDeferred; - execAsync(`${npmCommand} install --save-dev ${name}`, { + return execAsync(`${npmCommand} install --save-dev ${name}`, { stdio: 'ignore', }); } From e2281c688a7dacdb912ad72318849b0fb29c4cbd Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 13:07:43 +0900 Subject: [PATCH 12/36] Run npm command in service directory --- commands/plugin/install.js | 1 + 1 file changed, 1 insertion(+) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index 3ead9124712..64a232f64b8 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -186,6 +186,7 @@ class PluginInstall { async npmInstall(name) { const npmCommand = await npmCommandDeferred; return execAsync(`${npmCommand} install --save-dev ${name}`, { + cwd: this.serviceDir, stdio: 'ignore', }); } From b6b6bfdd066936889c9aaba583eb10c5d1f88835 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 13:47:16 +0900 Subject: [PATCH 13/36] Rely on `child-process-ext/spawn` --- commands/plugin/install.js | 7 ++----- test/unit/commands/plugin/install.test.js | 17 +++++++---------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index 64a232f64b8..a83cdab5786 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -1,13 +1,12 @@ 'use strict'; -const childProcess = require('child_process'); +const spawn = require('child-process-ext/spawn'); const fsp = require('fs').promises; const fse = require('fs-extra'); const path = require('path'); const _ = require('lodash'); const isPlainObject = require('type/plain-object/is'); const yaml = require('js-yaml'); -const { promisify } = require('util'); const cloudformationSchema = require('@serverless/utils/cloudformation-schema'); const log = require('@serverless/utils/log'); const ServerlessError = require('../../lib/serverless-error'); @@ -17,8 +16,6 @@ const pluginUtils = require('../../lib/commands/plugin-management'); const npmCommandDeferred = require('../../lib/utils/npm-command-deferred'); const CLI = require('../../lib/classes/CLI'); -const execAsync = promisify(childProcess.exec); - module.exports = async ({ configuration, serviceDir, configurationFilename, options }) => { await new PluginInstall({ configuration, @@ -185,7 +182,7 @@ class PluginInstall { async npmInstall(name) { const npmCommand = await npmCommandDeferred; - return execAsync(`${npmCommand} install --save-dev ${name}`, { + await spawn(npmCommand, ['install', '--save-dev', name], { cwd: this.serviceDir, stdio: 'ignore', }); diff --git a/test/unit/commands/plugin/install.test.js b/test/unit/commands/plugin/install.test.js index 1ba0e0d02ef..1b10f79f70b 100644 --- a/test/unit/commands/plugin/install.test.js +++ b/test/unit/commands/plugin/install.test.js @@ -15,16 +15,16 @@ chai.use(require('chai-as-promised')); const npmCommand = 'npm'; describe('test/unit/commands/plugin/install.test.js', async () => { - let execFake; + let spawnFake; let serviceDir; let configurationFilePath; const pluginName = 'serverless-plugin-1'; before(async () => { - execFake = sinon.fake(async (command, ...args) => { - if (command.startsWith(`${npmCommand} install --save-dev`)) { - const _pluginName = command.split(' ')[3]; + spawnFake = sinon.fake(async (command, args) => { + if (command === npmCommand && args[0] === 'install' && args[1] === '--save-dev') { + const _pluginName = args[2]; const pluginNameWithoutVersion = _pluginName.split('@')[0]; if (pluginNameWithoutVersion) { @@ -39,13 +39,9 @@ describe('test/unit/commands/plugin/install.test.js', async () => { await fse.writeJson(pluginPackageJsonFilePath, packageJsonFileContent); } } - const callback = args[args.length - 1]; - callback(); }); const installPlugin = proxyquire('../../../../commands/plugin/install', { - child_process: { - exec: execFake, - }, + 'child-process-ext/spawn': spawnFake, }); const fixture = await fixturesEngine.setup('function'); @@ -69,7 +65,8 @@ describe('test/unit/commands/plugin/install.test.js', async () => { }); it('should install plugin', () => { - const command = execFake.firstArg; + const firstCall = spawnFake.firstCall; + const command = [firstCall.args[0], ...firstCall.args[1]].join(' '); const expectedCommand = `${npmCommand} install --save-dev ${pluginName}`; expect(command).to.have.string(expectedCommand); }); From 19eb1d32ff02f26f135adb0fb240b71be4f671e1 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 13:55:13 +0900 Subject: [PATCH 14/36] Rely on async/await syntax --- commands/plugin/install.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index a83cdab5786..40fb5ce8156 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -168,16 +168,14 @@ class PluginInstall { this.options.pluginName, 'package.json' ); - return fse.readJson(pluginPackageJsonFilePath).then((pluginPackageJson) => { - if (pluginPackageJson.peerDependencies) { - const pluginsArray = []; - Object.entries(pluginPackageJson.peerDependencies).forEach(([k, v]) => { - pluginsArray.push(`${k}@"${v}"`); - }); - return Promise.all(pluginsArray.map((plugin) => this.npmInstall(plugin))); - } - return Promise.resolve(); - }); + const pluginPackageJson = await fse.readJson(pluginPackageJsonFilePath); + if (pluginPackageJson.peerDependencies) { + const pluginsArray = []; + Object.entries(pluginPackageJson.peerDependencies).forEach(([k, v]) => { + pluginsArray.push(`${k}@"${v}"`); + }); + await Promise.all(pluginsArray.map((plugin) => this.npmInstall(plugin))); + } } async npmInstall(name) { From 5f9c0654790d8e860c42c5e45aa5309f06522c45 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 13:59:17 +0900 Subject: [PATCH 15/36] Rely on async/await syntax --- lib/commands/plugin-management.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/commands/plugin-management.js b/lib/commands/plugin-management.js index a89ecb056a4..591c156617b 100644 --- a/lib/commands/plugin-management.js +++ b/lib/commands/plugin-management.js @@ -49,9 +49,8 @@ module.exports = { options.agent = new HttpsProxyAgent(url.parse(proxy)); } - return fetch(endpoint, options) - .then((result) => result.json()) - .then((json) => json); + const result = await fetch(endpoint, options); + return result.json(); }, getPluginInfo(name) { From 7058e0fc2607cbf2b6819f752657018396a550b0 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 18:42:25 +0900 Subject: [PATCH 16/36] Destructure class into functions --- commands/plugin/install.js | 280 ++++++++++++++---------------- lib/commands/plugin-management.js | 21 +-- 2 files changed, 143 insertions(+), 158 deletions(-) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index 40fb5ce8156..9a31a6e6e67 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -12,177 +12,161 @@ const log = require('@serverless/utils/log'); const ServerlessError = require('../../lib/serverless-error'); const yamlAstParser = require('../../lib/utils/yamlAstParser'); const fileExists = require('../../lib/utils/fs/fileExists'); -const pluginUtils = require('../../lib/commands/plugin-management'); const npmCommandDeferred = require('../../lib/utils/npm-command-deferred'); const CLI = require('../../lib/classes/CLI'); +const { + getPlugins, + getPluginInfo, + getServerlessFilePath, + validate, +} = require('../../lib/commands/plugin-management'); -module.exports = async ({ configuration, serviceDir, configurationFilename, options }) => { - await new PluginInstall({ - configuration, - serviceDir, - configurationFilename, - options, - }).install(); -}; - -const requestManualUpdate = (serverlessFilePath) => - log(` - Can't automatically add plugin into "${path.basename(serverlessFilePath)}" file. - Please make it manually. -`); - -class PluginInstall { - constructor({ configuration, serviceDir, configurationFilename, options }) { - this.configuration = configuration; - this.serviceDir = serviceDir; - this.configurationFilename = configurationFilename; - this.options = options; - - this.cli = new CLI(undefined); +const cli = new CLI(undefined); - Object.assign(this, pluginUtils); +module.exports = async ({ configuration, serviceDir, configurationFilename, options }) => { + const pluginInfo = getPluginInfo(options.name); + options.pluginName = pluginInfo[0]; + options.pluginVersion = pluginInfo[1] || 'latest'; + + validate({ serviceDir }); + const plugins = await getPlugins(); + const plugin = plugins.find((item) => item.name === options.pluginName); + if (!plugin) { + cli.log('Plugin not found in serverless registry, continuing to install'); } - async install() { - const pluginInfo = pluginUtils.getPluginInfo(this.options.name); - this.options.pluginName = pluginInfo[0]; - this.options.pluginVersion = pluginInfo[1] || 'latest'; - - await this.validate(); - const plugins = await this.getPlugins(); - const plugin = plugins.find((item) => item.name === this.options.pluginName); - if (!plugin) { - this.cli.log('Plugin not found in serverless registry, continuing to install'); - } + await pluginInstall({ configuration, serviceDir, options }); + await addPluginToServerlessFile({ serviceDir, configurationFilename, options }); + await installPeerDependencies({ serviceDir, options }); - await this.pluginInstall(); - await this.addPluginToServerlessFile(); - await this.installPeerDependencies(); + const message = [ + 'Successfully installed', + ` "${options.pluginName}@${options.pluginVersion}"`, + ].join(''); + cli.log(message); +}; - const message = [ - 'Successfully installed', - ` "${this.options.pluginName}@${this.options.pluginVersion}"`, - ].join(''); - this.cli.log(message); +const pluginInstall = async ({ configuration, serviceDir, options }) => { + const packageJsonFilePath = path.join(serviceDir, 'package.json'); + + // check if package.json is already present. Otherwise create one + const exists = await fileExists(packageJsonFilePath); + if (!exists) { + cli.log('Creating an empty package.json file in your service directory'); + + const packageJsonFileContent = { + name: configuration.service, + description: '', + version: '0.1.0', + dependencies: {}, + devDependencies: {}, + }; + await fse.writeJson(packageJsonFilePath, packageJsonFileContent); } - async pluginInstall() { - const serviceDir = this.serviceDir; - const packageJsonFilePath = path.join(serviceDir, 'package.json'); - - // check if package.json is already present. Otherwise create one - const exists = await fileExists(packageJsonFilePath); - if (!exists) { - this.cli.log('Creating an empty package.json file in your service directory'); - - const packageJsonFileContent = { - name: this.configuration.service, - description: '', - version: '0.1.0', - dependencies: {}, - devDependencies: {}, - }; - await fse.writeJson(packageJsonFilePath, packageJsonFileContent); - } + // install the package through npm + const pluginFullName = `${options.pluginName}@${options.pluginVersion}`; + const message = [ + `Installing plugin "${pluginFullName}"`, + ' (this might take a few seconds...)', + ].join(''); + cli.log(message); + await npmInstall(pluginFullName, { serviceDir }); +}; - // install the package through npm - const pluginFullName = `${this.options.pluginName}@${this.options.pluginVersion}`; - const message = [ - `Installing plugin "${pluginFullName}"`, - ' (this might take a few seconds...)', - ].join(''); - this.cli.log(message); - await this.npmInstall(pluginFullName); +const addPluginToServerlessFile = async ({ serviceDir, configurationFilename, options }) => { + const serverlessFilePath = getServerlessFilePath({ serviceDir, configurationFilename }); + const fileExtension = path.extname(serverlessFilePath); + if (fileExtension === '.js' || fileExtension === '.ts') { + requestManualUpdate(serverlessFilePath); + return; } - async addPluginToServerlessFile() { - const serverlessFilePath = this.getServerlessFilePath(); - const fileExtension = path.extname(serverlessFilePath); - if (fileExtension === '.js' || fileExtension === '.ts') { - requestManualUpdate(serverlessFilePath); - return; + const checkIsArrayPluginsObject = (pluginsObject) => + pluginsObject == null || Array.isArray(pluginsObject); + // pluginsObject type determined based on the value loaded during the serverless init. + if (_.last(serverlessFilePath.split('.')) === 'json') { + const serverlessFileObj = await fse.readJson(serverlessFilePath); + const newServerlessFileObj = serverlessFileObj; + const isArrayPluginsObject = checkIsArrayPluginsObject(newServerlessFileObj.plugins); + // null modules property is not supported + let plugins = isArrayPluginsObject + ? newServerlessFileObj.plugins || [] + : newServerlessFileObj.plugins.modules; + + if (plugins == null) { + throw new ServerlessError( + 'plugins modules property must be present', + 'PLUGINS_MODULES_MISSING' + ); } - const checkIsArrayPluginsObject = (pluginsObject) => - pluginsObject == null || Array.isArray(pluginsObject); - // pluginsObject type determined based on the value loaded during the serverless init. - if (_.last(serverlessFilePath.split('.')) === 'json') { - const serverlessFileObj = await fse.readJson(serverlessFilePath); - const newServerlessFileObj = serverlessFileObj; - const isArrayPluginsObject = checkIsArrayPluginsObject(newServerlessFileObj.plugins); - // null modules property is not supported - let plugins = isArrayPluginsObject - ? newServerlessFileObj.plugins || [] - : newServerlessFileObj.plugins.modules; - - if (plugins == null) { - throw new ServerlessError( - 'plugins modules property must be present', - 'PLUGINS_MODULES_MISSING' - ); - } + plugins.push(options.pluginName); + plugins = _.sortedUniq(plugins); - plugins.push(this.options.pluginName); - plugins = _.sortedUniq(plugins); - - if (isArrayPluginsObject) { - newServerlessFileObj.plugins = plugins; - } else { - newServerlessFileObj.plugins.modules = plugins; - } - - await fse.writeJson(serverlessFilePath, newServerlessFileObj); - return; + if (isArrayPluginsObject) { + newServerlessFileObj.plugins = plugins; + } else { + newServerlessFileObj.plugins.modules = plugins; } - const serverlessFileObj = yaml.load(await fsp.readFile(serverlessFilePath, 'utf8'), { - filename: serverlessFilePath, - schema: cloudformationSchema, - }); - if (serverlessFileObj.plugins != null) { - // Plugins section can be behind veriables, opt-out in such case - if (isPlainObject(serverlessFileObj.plugins)) { - if ( - serverlessFileObj.plugins.modules != null && - !Array.isArray(serverlessFileObj.plugins.modules) - ) { - requestManualUpdate(serverlessFilePath); - return; - } - } else if (!Array.isArray(serverlessFileObj.plugins)) { + await fse.writeJson(serverlessFilePath, newServerlessFileObj); + return; + } + + const serverlessFileObj = yaml.load(await fsp.readFile(serverlessFilePath, 'utf8'), { + filename: serverlessFilePath, + schema: cloudformationSchema, + }); + if (serverlessFileObj.plugins != null) { + // Plugins section can be behind veriables, opt-out in such case + if (isPlainObject(serverlessFileObj.plugins)) { + if ( + serverlessFileObj.plugins.modules != null && + !Array.isArray(serverlessFileObj.plugins.modules) + ) { requestManualUpdate(serverlessFilePath); return; } - } - await yamlAstParser.addNewArrayItem( - serverlessFilePath, - checkIsArrayPluginsObject(serverlessFileObj.plugins) ? 'plugins' : 'plugins.modules', - this.options.pluginName - ); - } - - async installPeerDependencies() { - const pluginPackageJsonFilePath = path.join( - this.serviceDir, - 'node_modules', - this.options.pluginName, - 'package.json' - ); - const pluginPackageJson = await fse.readJson(pluginPackageJsonFilePath); - if (pluginPackageJson.peerDependencies) { - const pluginsArray = []; - Object.entries(pluginPackageJson.peerDependencies).forEach(([k, v]) => { - pluginsArray.push(`${k}@"${v}"`); - }); - await Promise.all(pluginsArray.map((plugin) => this.npmInstall(plugin))); + } else if (!Array.isArray(serverlessFileObj.plugins)) { + requestManualUpdate(serverlessFilePath); + return; } } + await yamlAstParser.addNewArrayItem( + serverlessFilePath, + checkIsArrayPluginsObject(serverlessFileObj.plugins) ? 'plugins' : 'plugins.modules', + options.pluginName + ); +}; - async npmInstall(name) { - const npmCommand = await npmCommandDeferred; - await spawn(npmCommand, ['install', '--save-dev', name], { - cwd: this.serviceDir, - stdio: 'ignore', +const installPeerDependencies = async ({ serviceDir, options }) => { + const pluginPackageJsonFilePath = path.join( + serviceDir, + 'node_modules', + options.pluginName, + 'package.json' + ); + const pluginPackageJson = await fse.readJson(pluginPackageJsonFilePath); + if (pluginPackageJson.peerDependencies) { + const pluginsArray = []; + Object.entries(pluginPackageJson.peerDependencies).forEach(([k, v]) => { + pluginsArray.push(`${k}@"${v}"`); }); + await Promise.all(pluginsArray.map((plugin) => npmInstall(plugin, { serviceDir }))); } -} +}; + +const npmInstall = async (name, { serviceDir }) => { + const npmCommand = await npmCommandDeferred; + await spawn(npmCommand, ['install', '--save-dev', name], { + cwd: serviceDir, + stdio: 'ignore', + }); +}; + +const requestManualUpdate = (serverlessFilePath) => + log(` + Can't automatically add plugin into "${path.basename(serverlessFilePath)}" file. + Please make it manually. +`); diff --git a/lib/commands/plugin-management.js b/lib/commands/plugin-management.js index 591c156617b..31c6b29f25e 100644 --- a/lib/commands/plugin-management.js +++ b/lib/commands/plugin-management.js @@ -6,23 +6,24 @@ const HttpsProxyAgent = require('https-proxy-agent'); const url = require('url'); const chalk = require('chalk'); const _ = require('lodash'); +const CLI = require('../../lib/classes/CLI'); const ServerlessError = require('../../lib/serverless-error'); +const cli = new CLI(undefined); + module.exports = { - async validate() { - if (!this.serviceDir) { + validate({ serviceDir }) { + if (!serviceDir) { throw new ServerlessError( 'This command can only be run inside a service directory', 'MISSING_SERVICE_DIRECTORY' ); } - - return Promise.resolve(); }, - getServerlessFilePath() { - if (this.configurationFilename) { - return path.resolve(this.serviceDir, this.configurationFilename); + getServerlessFilePath({ serviceDir, configurationFilename }) { + if (configurationFilename) { + return path.resolve(serviceDir, configurationFilename); } throw new ServerlessError( 'Could not find any serverless service definition file.', @@ -74,15 +75,15 @@ module.exports = { }); // remove last two newlines for a prettier output message = message.slice(0, -2); - this.cli.consoleLog(message); - this.cli.consoleLog(` + cli.consoleLog(message); + cli.consoleLog(` To install a plugin run 'serverless plugin install --name plugin-name-here' It will be automatically downloaded and added to your package.json and serverless.yml file `); } else { message = 'There are no plugins available to display'; - this.cli.consoleLog(message); + cli.consoleLog(message); } return Promise.resolve(message); From ae67be8059fd8b4949dbf1834f4c008a7d1a2ed1 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 18:59:16 +0900 Subject: [PATCH 17/36] Manage edge case on installing plugin --- commands/plugin/install.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index 9a31a6e6e67..fbec5ae1bc3 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -162,6 +162,9 @@ const npmInstall = async (name, { serviceDir }) => { await spawn(npmCommand, ['install', '--save-dev', name], { cwd: serviceDir, stdio: 'ignore', + // To parse quotes used in module versions. E.g. 'serverless@"^1.60.0 || 2"' + // https://stackoverflow.com/a/48015470 + shell: true, }); }; From 320e3ca88deefde2d4e190a838a0b772facf01d7 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 19:02:42 +0900 Subject: [PATCH 18/36] Alter binding for plugin management related utils --- test/unit/lib/plugins/plugin/lib/utils.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/lib/plugins/plugin/lib/utils.test.js b/test/unit/lib/plugins/plugin/lib/utils.test.js index 4aad4710f9f..ac7bb8d988d 100644 --- a/test/unit/lib/plugins/plugin/lib/utils.test.js +++ b/test/unit/lib/plugins/plugin/lib/utils.test.js @@ -5,7 +5,7 @@ const sinon = require('sinon'); const BbPromise = require('bluebird'); const proxyquire = require('proxyquire'); const chalk = require('chalk'); -const PluginInstall = require('../../../../../../lib/plugins/plugin/install'); +const PluginUninstall = require('../../../../../../lib/plugins/plugin/uninstall'); const Serverless = require('../../../../../../lib/Serverless'); const CLI = require('../../../../../../lib/classes/CLI'); const { expect } = require('chai'); @@ -38,7 +38,7 @@ describe('PluginUtils', () => { serverless = new Serverless(); serverless.cli = new CLI(serverless); const options = {}; - pluginUtils = new PluginInstall(serverless, options); + pluginUtils = new PluginUninstall(serverless, options); consoleLogStub = sinon.stub(serverless.cli, 'consoleLog').returns(); }); From ca471c6cd39165da589eb118e03ce1c2bacbb70a Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 22:16:38 +0900 Subject: [PATCH 19/36] Improve interface of `getPluginInfo` function --- commands/plugin/install.js | 6 +++--- lib/commands/plugin-management.js | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index fbec5ae1bc3..2341e44fb46 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -24,9 +24,9 @@ const { const cli = new CLI(undefined); module.exports = async ({ configuration, serviceDir, configurationFilename, options }) => { - const pluginInfo = getPluginInfo(options.name); - options.pluginName = pluginInfo[0]; - options.pluginVersion = pluginInfo[1] || 'latest'; + const { name, version } = getPluginInfo(options.name); + options.pluginName = name; + options.pluginVersion = version || 'latest'; validate({ serviceDir }); const plugins = await getPlugins(); diff --git a/lib/commands/plugin-management.js b/lib/commands/plugin-management.js index 31c6b29f25e..e6e4442976b 100644 --- a/lib/commands/plugin-management.js +++ b/lib/commands/plugin-management.js @@ -54,15 +54,16 @@ module.exports = { return result.json(); }, - getPluginInfo(name) { - let pluginInfo; - if (name.startsWith('@')) { - pluginInfo = name.slice(1).split('@', 2); - pluginInfo[0] = `@${pluginInfo[0]}`; + getPluginInfo(name_) { + let name; + let version; + if (name_.startsWith('@')) { + [, name, version] = name_.split('@', 3); + name = `@${name}`; } else { - pluginInfo = name.split('@', 2); + [name, version] = name_.split('@', 2); } - return pluginInfo; + return { name, version }; }, async display(plugins) { From f00c9dbc8f4e369a979732e36f4596f89ae321f6 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 22:27:10 +0900 Subject: [PATCH 20/36] Don't change input arguments --- commands/plugin/install.js | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index 2341e44fb46..dc975fffb44 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -24,29 +24,27 @@ const { const cli = new CLI(undefined); module.exports = async ({ configuration, serviceDir, configurationFilename, options }) => { - const { name, version } = getPluginInfo(options.name); - options.pluginName = name; - options.pluginVersion = version || 'latest'; + const pluginInfo = getPluginInfo(options.name); + const pluginName = pluginInfo.name; + const pluginVersion = pluginInfo.version || 'latest'; validate({ serviceDir }); const plugins = await getPlugins(); - const plugin = plugins.find((item) => item.name === options.pluginName); + const plugin = plugins.find((item) => item.name === pluginName); if (!plugin) { cli.log('Plugin not found in serverless registry, continuing to install'); } - await pluginInstall({ configuration, serviceDir, options }); - await addPluginToServerlessFile({ serviceDir, configurationFilename, options }); - await installPeerDependencies({ serviceDir, options }); + const context = { configuration, serviceDir, configurationFilename, pluginName, pluginVersion }; + await pluginInstall(context); + await addPluginToServerlessFile(context); + await installPeerDependencies(context); - const message = [ - 'Successfully installed', - ` "${options.pluginName}@${options.pluginVersion}"`, - ].join(''); + const message = ['Successfully installed', ` "${pluginName}@${pluginVersion}"`].join(''); cli.log(message); }; -const pluginInstall = async ({ configuration, serviceDir, options }) => { +const pluginInstall = async ({ configuration, serviceDir, pluginName, pluginVersion }) => { const packageJsonFilePath = path.join(serviceDir, 'package.json'); // check if package.json is already present. Otherwise create one @@ -65,7 +63,7 @@ const pluginInstall = async ({ configuration, serviceDir, options }) => { } // install the package through npm - const pluginFullName = `${options.pluginName}@${options.pluginVersion}`; + const pluginFullName = `${pluginName}@${pluginVersion}`; const message = [ `Installing plugin "${pluginFullName}"`, ' (this might take a few seconds...)', @@ -74,7 +72,7 @@ const pluginInstall = async ({ configuration, serviceDir, options }) => { await npmInstall(pluginFullName, { serviceDir }); }; -const addPluginToServerlessFile = async ({ serviceDir, configurationFilename, options }) => { +const addPluginToServerlessFile = async ({ serviceDir, configurationFilename, pluginName }) => { const serverlessFilePath = getServerlessFilePath({ serviceDir, configurationFilename }); const fileExtension = path.extname(serverlessFilePath); if (fileExtension === '.js' || fileExtension === '.ts') { @@ -101,7 +99,7 @@ const addPluginToServerlessFile = async ({ serviceDir, configurationFilename, op ); } - plugins.push(options.pluginName); + plugins.push(pluginName); plugins = _.sortedUniq(plugins); if (isArrayPluginsObject) { @@ -136,15 +134,15 @@ const addPluginToServerlessFile = async ({ serviceDir, configurationFilename, op await yamlAstParser.addNewArrayItem( serverlessFilePath, checkIsArrayPluginsObject(serverlessFileObj.plugins) ? 'plugins' : 'plugins.modules', - options.pluginName + pluginName ); }; -const installPeerDependencies = async ({ serviceDir, options }) => { +const installPeerDependencies = async ({ serviceDir, pluginName }) => { const pluginPackageJsonFilePath = path.join( serviceDir, 'node_modules', - options.pluginName, + pluginName, 'package.json' ); const pluginPackageJson = await fse.readJson(pluginPackageJsonFilePath); From 46b6db95521434329f1e3936b8c5b7c71088d3ba Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 22:34:30 +0900 Subject: [PATCH 21/36] Reduce unnecessary log --- commands/plugin/install.js | 10 ++-------- lib/commands/plugin-management.js | 26 -------------------------- 2 files changed, 2 insertions(+), 34 deletions(-) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index dc975fffb44..9e28fb84a58 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -15,7 +15,6 @@ const fileExists = require('../../lib/utils/fs/fileExists'); const npmCommandDeferred = require('../../lib/utils/npm-command-deferred'); const CLI = require('../../lib/classes/CLI'); const { - getPlugins, getPluginInfo, getServerlessFilePath, validate, @@ -24,17 +23,12 @@ const { const cli = new CLI(undefined); module.exports = async ({ configuration, serviceDir, configurationFilename, options }) => { + validate({ serviceDir }); + const pluginInfo = getPluginInfo(options.name); const pluginName = pluginInfo.name; const pluginVersion = pluginInfo.version || 'latest'; - validate({ serviceDir }); - const plugins = await getPlugins(); - const plugin = plugins.find((item) => item.name === pluginName); - if (!plugin) { - cli.log('Plugin not found in serverless registry, continuing to install'); - } - const context = { configuration, serviceDir, configurationFilename, pluginName, pluginVersion }; await pluginInstall(context); await addPluginToServerlessFile(context); diff --git a/lib/commands/plugin-management.js b/lib/commands/plugin-management.js index e6e4442976b..659d3ecb0ae 100644 --- a/lib/commands/plugin-management.js +++ b/lib/commands/plugin-management.js @@ -1,9 +1,6 @@ 'use strict'; const path = require('path'); -const fetch = require('node-fetch'); -const HttpsProxyAgent = require('https-proxy-agent'); -const url = require('url'); const chalk = require('chalk'); const _ = require('lodash'); const CLI = require('../../lib/classes/CLI'); @@ -31,29 +28,6 @@ module.exports = { ); }, - async getPlugins() { - const endpoint = 'https://raw.githubusercontent.com/serverless/plugins/master/plugins.json'; - - // Use HTTPS Proxy (Optional) - const proxy = - process.env.proxy || - process.env.HTTP_PROXY || - process.env.http_proxy || - process.env.HTTPS_PROXY || - process.env.https_proxy; - - const options = {}; - if (proxy) { - // not relying on recommended WHATWG URL - // due to missing support for it in https-proxy-agent - // https://github.com/TooTallNate/node-https-proxy-agent/issues/117 - options.agent = new HttpsProxyAgent(url.parse(proxy)); - } - - const result = await fetch(endpoint, options); - return result.json(); - }, - getPluginInfo(name_) { let name; let version; From 54f54f2c5b61adc7e2895a17027946db4c0fe37f Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 22:48:26 +0900 Subject: [PATCH 22/36] Stop doing things beyond the responsibilities --- commands/plugin/install.js | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/commands/plugin/install.js b/commands/plugin/install.js index 9e28fb84a58..a0661f4788d 100644 --- a/commands/plugin/install.js +++ b/commands/plugin/install.js @@ -11,7 +11,6 @@ const cloudformationSchema = require('@serverless/utils/cloudformation-schema'); const log = require('@serverless/utils/log'); const ServerlessError = require('../../lib/serverless-error'); const yamlAstParser = require('../../lib/utils/yamlAstParser'); -const fileExists = require('../../lib/utils/fs/fileExists'); const npmCommandDeferred = require('../../lib/utils/npm-command-deferred'); const CLI = require('../../lib/classes/CLI'); const { @@ -38,25 +37,7 @@ module.exports = async ({ configuration, serviceDir, configurationFilename, opti cli.log(message); }; -const pluginInstall = async ({ configuration, serviceDir, pluginName, pluginVersion }) => { - const packageJsonFilePath = path.join(serviceDir, 'package.json'); - - // check if package.json is already present. Otherwise create one - const exists = await fileExists(packageJsonFilePath); - if (!exists) { - cli.log('Creating an empty package.json file in your service directory'); - - const packageJsonFileContent = { - name: configuration.service, - description: '', - version: '0.1.0', - dependencies: {}, - devDependencies: {}, - }; - await fse.writeJson(packageJsonFilePath, packageJsonFileContent); - } - - // install the package through npm +const pluginInstall = async ({ serviceDir, pluginName, pluginVersion }) => { const pluginFullName = `${pluginName}@${pluginVersion}`; const message = [ `Installing plugin "${pluginFullName}"`, From d04f05f27117978b105b08cd4215622fc96c4188 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Wed, 22 Sep 2021 22:56:46 +0900 Subject: [PATCH 23/36] Remove unused util --- lib/commands/plugin-management.js | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/lib/commands/plugin-management.js b/lib/commands/plugin-management.js index 659d3ecb0ae..6efa1cc0f32 100644 --- a/lib/commands/plugin-management.js +++ b/lib/commands/plugin-management.js @@ -1,13 +1,8 @@ 'use strict'; const path = require('path'); -const chalk = require('chalk'); -const _ = require('lodash'); -const CLI = require('../../lib/classes/CLI'); const ServerlessError = require('../../lib/serverless-error'); -const cli = new CLI(undefined); - module.exports = { validate({ serviceDir }) { if (!serviceDir) { @@ -39,28 +34,4 @@ module.exports = { } return { name, version }; }, - - async display(plugins) { - let message = ''; - if (plugins && plugins.length) { - // order plugins by name - const orderedPlugins = _.orderBy(plugins, ['name'], ['asc']); - orderedPlugins.forEach((plugin) => { - message += `${chalk.yellow.underline(plugin.name)} - ${plugin.description}\n`; - }); - // remove last two newlines for a prettier output - message = message.slice(0, -2); - cli.consoleLog(message); - cli.consoleLog(` -To install a plugin run 'serverless plugin install --name plugin-name-here' - -It will be automatically downloaded and added to your package.json and serverless.yml file - `); - } else { - message = 'There are no plugins available to display'; - cli.consoleLog(message); - } - - return Promise.resolve(message); - }, }; From c760f5270e234f27ee1548e4299014eb670f3eed Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Sun, 26 Sep 2021 12:25:36 +0900 Subject: [PATCH 24/36] Align to sub-level commands naming policy --- commands/{plugin/install.js => plugin-install.js} | 10 +++++----- scripts/serverless.js | 2 +- .../{plugin/install.test.js => plugin-install.test.js} | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) rename commands/{plugin/install.js => plugin-install.js} (94%) rename test/unit/commands/{plugin/install.test.js => plugin-install.test.js} (88%) diff --git a/commands/plugin/install.js b/commands/plugin-install.js similarity index 94% rename from commands/plugin/install.js rename to commands/plugin-install.js index a0661f4788d..c187037385e 100644 --- a/commands/plugin/install.js +++ b/commands/plugin-install.js @@ -9,15 +9,15 @@ const isPlainObject = require('type/plain-object/is'); const yaml = require('js-yaml'); const cloudformationSchema = require('@serverless/utils/cloudformation-schema'); const log = require('@serverless/utils/log'); -const ServerlessError = require('../../lib/serverless-error'); -const yamlAstParser = require('../../lib/utils/yamlAstParser'); -const npmCommandDeferred = require('../../lib/utils/npm-command-deferred'); -const CLI = require('../../lib/classes/CLI'); +const ServerlessError = require('../lib/serverless-error'); +const yamlAstParser = require('../lib/utils/yamlAstParser'); +const npmCommandDeferred = require('../lib/utils/npm-command-deferred'); +const CLI = require('../lib/classes/CLI'); const { getPluginInfo, getServerlessFilePath, validate, -} = require('../../lib/commands/plugin-management'); +} = require('../lib/commands/plugin-management'); const cli = new CLI(undefined); diff --git a/scripts/serverless.js b/scripts/serverless.js index a42cd3ae606..b0a68afa083 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -499,7 +499,7 @@ const processSpanPromise = (async () => { if (standaloneCommands.includes(command)) { require('../lib/cli/ensure-supported-command')(configuration); - await require(`../commands/${commands.join('/')}`)({ + await require(`../commands/${commands.join('-')}`)({ configuration, serviceDir, configurationFilename, diff --git a/test/unit/commands/plugin/install.test.js b/test/unit/commands/plugin-install.test.js similarity index 88% rename from test/unit/commands/plugin/install.test.js rename to test/unit/commands/plugin-install.test.js index 1b10f79f70b..2f7ec13c67f 100644 --- a/test/unit/commands/plugin/install.test.js +++ b/test/unit/commands/plugin-install.test.js @@ -6,15 +6,15 @@ const yaml = require('js-yaml'); const path = require('path'); const fse = require('fs-extra'); const proxyquire = require('proxyquire'); -const fixturesEngine = require('../../../fixtures/programmatic'); -const resolveConfigurationPath = require('../../../../lib/cli/resolve-configuration-path'); +const fixturesEngine = require('../../fixtures/programmatic'); +const resolveConfigurationPath = require('../../../lib/cli/resolve-configuration-path'); const { expect } = require('chai'); chai.use(require('chai-as-promised')); const npmCommand = 'npm'; -describe('test/unit/commands/plugin/install.test.js', async () => { +describe.only('test/unit/commands/plugin-install.test.js', async () => { let spawnFake; let serviceDir; let configurationFilePath; @@ -40,7 +40,7 @@ describe('test/unit/commands/plugin/install.test.js', async () => { } } }); - const installPlugin = proxyquire('../../../../commands/plugin/install', { + const installPlugin = proxyquire('../../../commands/plugin-install', { 'child-process-ext/spawn': spawnFake, }); From 4aefbb48269196094d98342ff4aaa26856b8af2a Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Sun, 26 Sep 2021 12:50:27 +0900 Subject: [PATCH 25/36] Tear down correctly after run standalone commands --- scripts/serverless.js | 47 +++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/scripts/serverless.js b/scripts/serverless.js index b0a68afa083..d221293ed59 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -497,35 +497,38 @@ const processSpanPromise = (async () => { const configurationFilename = configuration && configurationPath.slice(serviceDir.length + 1); - if (standaloneCommands.includes(command)) { - require('../lib/cli/ensure-supported-command')(configuration); - await require(`../commands/${commands.join('-')}`)({ - configuration, - serviceDir, - configurationFilename, - options, - }); - return; - } + const isStandaloneCommand = standaloneCommands.includes(command); - if (isInteractiveSetup) { - require('../lib/cli/ensure-supported-command')(configuration); + if (isInteractiveSetup || isStandaloneCommand) { + let configurationFromInteractive; - if (!process.stdin.isTTY && !process.env.SLS_INTERACTIVE_SETUP_ENABLE) { - throw new ServerlessError( - 'Attempted to run an interactive setup in non TTY environment.\n' + - "If that's intentended enforce with SLS_INTERACTIVE_SETUP_ENABLE=1 environment variable", - 'INTERACTIVE_SETUP_IN_NON_TTY' - ); - } - const { configuration: configurationFromInteractive } = - await require('../lib/cli/interactive-setup')({ + if (isInteractiveSetup) { + require('../lib/cli/ensure-supported-command')(configuration); + + if (!process.stdin.isTTY && !process.env.SLS_INTERACTIVE_SETUP_ENABLE) { + throw new ServerlessError( + 'Attempted to run an interactive setup in non TTY environment.\n' + + "If that's intentended enforce with SLS_INTERACTIVE_SETUP_ENABLE=1 environment variable", + 'INTERACTIVE_SETUP_IN_NON_TTY' + ); + } + const result = await require('../lib/cli/interactive-setup')({ configuration, serviceDir, configurationFilename, options, commandUsage, }); + configurationFromInteractive = result.configuration; + } else if (isStandaloneCommand) { + require('../lib/cli/ensure-supported-command')(configuration); + await require(`../commands/${commands.join('-')}`)({ + configuration, + serviceDir, + configurationFilename, + options, + }); + } progress.clear(); @@ -540,7 +543,7 @@ const processSpanPromise = (async () => { options, commandSchema, serviceDir, - configuration: configurationFromInteractive, + configuration: isInteractiveSetup ? configurationFromInteractive : configuration, commandUsage, variableSources: variableSourcesInConfig, }), From 0f144f9cc35210a9efb01f7d514d5202fa2efb2a Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Sun, 26 Sep 2021 13:02:39 +0900 Subject: [PATCH 26/36] Stop taking care of peer dependencies when installing plugin --- commands/plugin-install.js | 18 ------------------ test/unit/commands/plugin-install.test.js | 20 +------------------- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/commands/plugin-install.js b/commands/plugin-install.js index c187037385e..aaaed99648f 100644 --- a/commands/plugin-install.js +++ b/commands/plugin-install.js @@ -31,7 +31,6 @@ module.exports = async ({ configuration, serviceDir, configurationFilename, opti const context = { configuration, serviceDir, configurationFilename, pluginName, pluginVersion }; await pluginInstall(context); await addPluginToServerlessFile(context); - await installPeerDependencies(context); const message = ['Successfully installed', ` "${pluginName}@${pluginVersion}"`].join(''); cli.log(message); @@ -113,23 +112,6 @@ const addPluginToServerlessFile = async ({ serviceDir, configurationFilename, pl ); }; -const installPeerDependencies = async ({ serviceDir, pluginName }) => { - const pluginPackageJsonFilePath = path.join( - serviceDir, - 'node_modules', - pluginName, - 'package.json' - ); - const pluginPackageJson = await fse.readJson(pluginPackageJsonFilePath); - if (pluginPackageJson.peerDependencies) { - const pluginsArray = []; - Object.entries(pluginPackageJson.peerDependencies).forEach(([k, v]) => { - pluginsArray.push(`${k}@"${v}"`); - }); - await Promise.all(pluginsArray.map((plugin) => npmInstall(plugin, { serviceDir }))); - } -}; - const npmInstall = async (name, { serviceDir }) => { const npmCommand = await npmCommandDeferred; await spawn(npmCommand, ['install', '--save-dev', name], { diff --git a/test/unit/commands/plugin-install.test.js b/test/unit/commands/plugin-install.test.js index 2f7ec13c67f..4102d1fc939 100644 --- a/test/unit/commands/plugin-install.test.js +++ b/test/unit/commands/plugin-install.test.js @@ -3,7 +3,6 @@ const chai = require('chai'); const sinon = require('sinon'); const yaml = require('js-yaml'); -const path = require('path'); const fse = require('fs-extra'); const proxyquire = require('proxyquire'); const fixturesEngine = require('../../fixtures/programmatic'); @@ -22,24 +21,7 @@ describe.only('test/unit/commands/plugin-install.test.js', async () => { const pluginName = 'serverless-plugin-1'; before(async () => { - spawnFake = sinon.fake(async (command, args) => { - if (command === npmCommand && args[0] === 'install' && args[1] === '--save-dev') { - const _pluginName = args[2]; - const pluginNameWithoutVersion = _pluginName.split('@')[0]; - - if (pluginNameWithoutVersion) { - const pluginPackageJsonFilePath = path.join( - serviceDir, - 'node_modules', - pluginName, - 'package.json' - ); - const packageJsonFileContent = {}; - await fse.ensureFile(pluginPackageJsonFilePath); - await fse.writeJson(pluginPackageJsonFilePath, packageJsonFileContent); - } - } - }); + spawnFake = sinon.fake(); const installPlugin = proxyquire('../../../commands/plugin-install', { 'child-process-ext/spawn': spawnFake, }); From 96aab3bce5eea6dc2ed9a1b469375328ae4720a4 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Sun, 26 Sep 2021 14:35:50 +0900 Subject: [PATCH 27/36] Confirm whether plugin is not already installed --- commands/plugin-install.js | 49 ++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/commands/plugin-install.js b/commands/plugin-install.js index aaaed99648f..53ab4f469f0 100644 --- a/commands/plugin-install.js +++ b/commands/plugin-install.js @@ -5,6 +5,7 @@ const fsp = require('fs').promises; const fse = require('fs-extra'); const path = require('path'); const _ = require('lodash'); +const cjsResolve = require('ncjsm/resolve/sync'); const isPlainObject = require('type/plain-object/is'); const yaml = require('js-yaml'); const cloudformationSchema = require('@serverless/utils/cloudformation-schema'); @@ -27,8 +28,13 @@ module.exports = async ({ configuration, serviceDir, configurationFilename, opti const pluginInfo = getPluginInfo(options.name); const pluginName = pluginInfo.name; const pluginVersion = pluginInfo.version || 'latest'; + const configurationFilePath = getServerlessFilePath({ serviceDir, configurationFilename }); - const context = { configuration, serviceDir, configurationFilename, pluginName, pluginVersion }; + const context = { configuration, serviceDir, configurationFilePath, pluginName, pluginVersion }; + if (await isInstalled(context)) { + cli.log(`"${options.name}" has already been installed`); + return; + } await pluginInstall(context); await addPluginToServerlessFile(context); @@ -36,6 +42,20 @@ module.exports = async ({ configuration, serviceDir, configurationFilename, opti cli.log(message); }; +const isInstalled = async ({ serviceDir, configurationFilePath, pluginName }) => { + try { + cjsResolve(serviceDir, pluginName); + } catch { + return false; + } + + const serverlessFileObj = yaml.load(await fse.readFile(configurationFilePath, 'utf8'), { + filename: configurationFilePath, + }); + const installedPlugins = serverlessFileObj.plugins || []; + return installedPlugins.includes(pluginName); +}; + const pluginInstall = async ({ serviceDir, pluginName, pluginVersion }) => { const pluginFullName = `${pluginName}@${pluginVersion}`; const message = [ @@ -46,19 +66,18 @@ const pluginInstall = async ({ serviceDir, pluginName, pluginVersion }) => { await npmInstall(pluginFullName, { serviceDir }); }; -const addPluginToServerlessFile = async ({ serviceDir, configurationFilename, pluginName }) => { - const serverlessFilePath = getServerlessFilePath({ serviceDir, configurationFilename }); - const fileExtension = path.extname(serverlessFilePath); +const addPluginToServerlessFile = async ({ configurationFilePath, pluginName }) => { + const fileExtension = path.extname(configurationFilePath); if (fileExtension === '.js' || fileExtension === '.ts') { - requestManualUpdate(serverlessFilePath); + requestManualUpdate(configurationFilePath); return; } const checkIsArrayPluginsObject = (pluginsObject) => pluginsObject == null || Array.isArray(pluginsObject); // pluginsObject type determined based on the value loaded during the serverless init. - if (_.last(serverlessFilePath.split('.')) === 'json') { - const serverlessFileObj = await fse.readJson(serverlessFilePath); + if (_.last(configurationFilePath.split('.')) === 'json') { + const serverlessFileObj = await fse.readJson(configurationFilePath); const newServerlessFileObj = serverlessFileObj; const isArrayPluginsObject = checkIsArrayPluginsObject(newServerlessFileObj.plugins); // null modules property is not supported @@ -82,12 +101,12 @@ const addPluginToServerlessFile = async ({ serviceDir, configurationFilename, pl newServerlessFileObj.plugins.modules = plugins; } - await fse.writeJson(serverlessFilePath, newServerlessFileObj); + await fse.writeJson(configurationFilePath, newServerlessFileObj); return; } - const serverlessFileObj = yaml.load(await fsp.readFile(serverlessFilePath, 'utf8'), { - filename: serverlessFilePath, + const serverlessFileObj = yaml.load(await fsp.readFile(configurationFilePath, 'utf8'), { + filename: configurationFilePath, schema: cloudformationSchema, }); if (serverlessFileObj.plugins != null) { @@ -97,16 +116,16 @@ const addPluginToServerlessFile = async ({ serviceDir, configurationFilename, pl serverlessFileObj.plugins.modules != null && !Array.isArray(serverlessFileObj.plugins.modules) ) { - requestManualUpdate(serverlessFilePath); + requestManualUpdate(configurationFilePath); return; } } else if (!Array.isArray(serverlessFileObj.plugins)) { - requestManualUpdate(serverlessFilePath); + requestManualUpdate(configurationFilePath); return; } } await yamlAstParser.addNewArrayItem( - serverlessFilePath, + configurationFilePath, checkIsArrayPluginsObject(serverlessFileObj.plugins) ? 'plugins' : 'plugins.modules', pluginName ); @@ -123,8 +142,8 @@ const npmInstall = async (name, { serviceDir }) => { }); }; -const requestManualUpdate = (serverlessFilePath) => +const requestManualUpdate = (configurationFilePath) => log(` - Can't automatically add plugin into "${path.basename(serverlessFilePath)}" file. + Can't automatically add plugin into "${path.basename(configurationFilePath)}" file. Please make it manually. `); From afcdd19b40c1bc76d21518d9bbf80998da3cfd44 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Sun, 26 Sep 2021 14:41:01 +0900 Subject: [PATCH 28/36] Reduce duplicate calculations --- commands/plugin-install.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/commands/plugin-install.js b/commands/plugin-install.js index 53ab4f469f0..0a622c8109e 100644 --- a/commands/plugin-install.js +++ b/commands/plugin-install.js @@ -42,17 +42,14 @@ module.exports = async ({ configuration, serviceDir, configurationFilename, opti cli.log(message); }; -const isInstalled = async ({ serviceDir, configurationFilePath, pluginName }) => { +const isInstalled = async ({ configuration, serviceDir, pluginName }) => { try { cjsResolve(serviceDir, pluginName); } catch { return false; } - const serverlessFileObj = yaml.load(await fse.readFile(configurationFilePath, 'utf8'), { - filename: configurationFilePath, - }); - const installedPlugins = serverlessFileObj.plugins || []; + const installedPlugins = configuration.plugins || []; return installedPlugins.includes(pluginName); }; From 645344b4de23fe7100083d1458aa254ec33a10be Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Sun, 26 Sep 2021 14:56:55 +0900 Subject: [PATCH 29/36] Rely on `@serverless/utils/log` --- commands/plugin-install.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/commands/plugin-install.js b/commands/plugin-install.js index 0a622c8109e..50803a1b5f1 100644 --- a/commands/plugin-install.js +++ b/commands/plugin-install.js @@ -13,15 +13,12 @@ const log = require('@serverless/utils/log'); const ServerlessError = require('../lib/serverless-error'); const yamlAstParser = require('../lib/utils/yamlAstParser'); const npmCommandDeferred = require('../lib/utils/npm-command-deferred'); -const CLI = require('../lib/classes/CLI'); const { getPluginInfo, getServerlessFilePath, validate, } = require('../lib/commands/plugin-management'); -const cli = new CLI(undefined); - module.exports = async ({ configuration, serviceDir, configurationFilename, options }) => { validate({ serviceDir }); @@ -32,14 +29,14 @@ module.exports = async ({ configuration, serviceDir, configurationFilename, opti const context = { configuration, serviceDir, configurationFilePath, pluginName, pluginVersion }; if (await isInstalled(context)) { - cli.log(`"${options.name}" has already been installed`); + log(`"${options.name}" has already been installed`); return; } await pluginInstall(context); await addPluginToServerlessFile(context); const message = ['Successfully installed', ` "${pluginName}@${pluginVersion}"`].join(''); - cli.log(message); + log(message); }; const isInstalled = async ({ configuration, serviceDir, pluginName }) => { @@ -59,7 +56,7 @@ const pluginInstall = async ({ serviceDir, pluginName, pluginVersion }) => { `Installing plugin "${pluginFullName}"`, ' (this might take a few seconds...)', ].join(''); - cli.log(message); + log(message); await npmInstall(pluginFullName, { serviceDir }); }; From ce0ff935c90c32b091c374e53341c9394d726a18 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Sun, 26 Sep 2021 14:58:25 +0900 Subject: [PATCH 30/36] Unset `only` on mocha test --- test/unit/commands/plugin-install.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/commands/plugin-install.test.js b/test/unit/commands/plugin-install.test.js index 4102d1fc939..d84adb871b2 100644 --- a/test/unit/commands/plugin-install.test.js +++ b/test/unit/commands/plugin-install.test.js @@ -13,7 +13,7 @@ chai.use(require('chai-as-promised')); const npmCommand = 'npm'; -describe.only('test/unit/commands/plugin-install.test.js', async () => { +describe('test/unit/commands/plugin-install.test.js', async () => { let spawnFake; let serviceDir; let configurationFilePath; From f32b6c346ef34bff7f64f7154dcd3c5ca8ccf729 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Mon, 27 Sep 2021 21:42:43 +0900 Subject: [PATCH 31/36] Prefer verb first function name --- commands/plugin-install.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/plugin-install.js b/commands/plugin-install.js index 50803a1b5f1..f7147bc6aea 100644 --- a/commands/plugin-install.js +++ b/commands/plugin-install.js @@ -32,7 +32,7 @@ module.exports = async ({ configuration, serviceDir, configurationFilename, opti log(`"${options.name}" has already been installed`); return; } - await pluginInstall(context); + await installPlugin(context); await addPluginToServerlessFile(context); const message = ['Successfully installed', ` "${pluginName}@${pluginVersion}"`].join(''); @@ -50,7 +50,7 @@ const isInstalled = async ({ configuration, serviceDir, pluginName }) => { return installedPlugins.includes(pluginName); }; -const pluginInstall = async ({ serviceDir, pluginName, pluginVersion }) => { +const installPlugin = async ({ serviceDir, pluginName, pluginVersion }) => { const pluginFullName = `${pluginName}@${pluginVersion}`; const message = [ `Installing plugin "${pluginFullName}"`, From 25b5099abb41729435876261b6274722ef92897a Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Mon, 27 Sep 2021 21:54:05 +0900 Subject: [PATCH 32/36] Enable to upgrade plugin --- commands/plugin-install.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/commands/plugin-install.js b/commands/plugin-install.js index f7147bc6aea..8ac92515790 100644 --- a/commands/plugin-install.js +++ b/commands/plugin-install.js @@ -5,7 +5,6 @@ const fsp = require('fs').promises; const fse = require('fs-extra'); const path = require('path'); const _ = require('lodash'); -const cjsResolve = require('ncjsm/resolve/sync'); const isPlainObject = require('type/plain-object/is'); const yaml = require('js-yaml'); const cloudformationSchema = require('@serverless/utils/cloudformation-schema'); @@ -28,10 +27,6 @@ module.exports = async ({ configuration, serviceDir, configurationFilename, opti const configurationFilePath = getServerlessFilePath({ serviceDir, configurationFilename }); const context = { configuration, serviceDir, configurationFilePath, pluginName, pluginVersion }; - if (await isInstalled(context)) { - log(`"${options.name}" has already been installed`); - return; - } await installPlugin(context); await addPluginToServerlessFile(context); @@ -39,17 +34,6 @@ module.exports = async ({ configuration, serviceDir, configurationFilename, opti log(message); }; -const isInstalled = async ({ configuration, serviceDir, pluginName }) => { - try { - cjsResolve(serviceDir, pluginName); - } catch { - return false; - } - - const installedPlugins = configuration.plugins || []; - return installedPlugins.includes(pluginName); -}; - const installPlugin = async ({ serviceDir, pluginName, pluginVersion }) => { const pluginFullName = `${pluginName}@${pluginVersion}`; const message = [ From 7308c9fb30f97c81cb5e8f1075eebc886c4cc470 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Mon, 27 Sep 2021 22:03:53 +0900 Subject: [PATCH 33/36] Leave minimum conditions --- scripts/serverless.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/serverless.js b/scripts/serverless.js index d221293ed59..1a2a2fd6417 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -520,7 +520,7 @@ const processSpanPromise = (async () => { commandUsage, }); configurationFromInteractive = result.configuration; - } else if (isStandaloneCommand) { + } else { require('../lib/cli/ensure-supported-command')(configuration); await require(`../commands/${commands.join('-')}`)({ configuration, From f1a85421508245bc3e476748a4e4a520845cc9f1 Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Mon, 27 Sep 2021 22:10:12 +0900 Subject: [PATCH 34/36] Reduce redundant variable --- scripts/serverless.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/serverless.js b/scripts/serverless.js index 1a2a2fd6417..82e517b5a01 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -500,8 +500,6 @@ const processSpanPromise = (async () => { const isStandaloneCommand = standaloneCommands.includes(command); if (isInteractiveSetup || isStandaloneCommand) { - let configurationFromInteractive; - if (isInteractiveSetup) { require('../lib/cli/ensure-supported-command')(configuration); @@ -519,7 +517,9 @@ const processSpanPromise = (async () => { options, commandUsage, }); - configurationFromInteractive = result.configuration; + if (result.configuration) { + configuration = result.configuration; + } } else { require('../lib/cli/ensure-supported-command')(configuration); await require(`../commands/${commands.join('-')}`)({ @@ -543,7 +543,7 @@ const processSpanPromise = (async () => { options, commandSchema, serviceDir, - configuration: isInteractiveSetup ? configurationFromInteractive : configuration, + configuration, commandUsage, variableSources: variableSourcesInConfig, }), From 29b4a7ec7270d43bbd1243ca4eed834b83714b3b Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Mon, 27 Sep 2021 23:12:09 +0900 Subject: [PATCH 35/36] Ensure backward compatibility --- lib/plugins/plugin/install.js | 203 ++++++++++++++++++ .../unit/lib/plugins/plugin/lib/utils.test.js | 4 +- 2 files changed, 205 insertions(+), 2 deletions(-) diff --git a/lib/plugins/plugin/install.js b/lib/plugins/plugin/install.js index e69de29bb2d..65af0000944 100644 --- a/lib/plugins/plugin/install.js +++ b/lib/plugins/plugin/install.js @@ -0,0 +1,203 @@ +// TODO: Remove in v3 + +'use strict'; + +const BbPromise = require('bluebird'); +const childProcess = BbPromise.promisifyAll(require('child_process')); +const fsp = require('fs').promises; +const fse = require('fs-extra'); +const path = require('path'); +const _ = require('lodash'); +const isPlainObject = require('type/plain-object/is'); +const yaml = require('js-yaml'); +const cloudformationSchema = require('@serverless/utils/cloudformation-schema'); +const log = require('@serverless/utils/log'); +const ServerlessError = require('../../serverless-error'); +const cliCommandsSchema = require('../../cli/commands-schema'); +const yamlAstParser = require('../../utils/yamlAstParser'); +const fileExists = require('../../utils/fs/fileExists'); +const pluginUtils = require('./lib/utils'); +const npmCommandDeferred = require('../../utils/npm-command-deferred'); + +const requestManualUpdate = (serverlessFilePath) => + log(` + Can't automatically add plugin into "${path.basename(serverlessFilePath)}" file. + Please make it manually. +`); + +class PluginInstall { + constructor(serverless, options) { + this.serverless = serverless; + this.options = options; + + Object.assign(this, pluginUtils); + + this.commands = { + plugin: { + commands: { + install: { + ...cliCommandsSchema.get('plugin install'), + }, + }, + }, + }; + this.hooks = { + 'plugin:install:install': async () => BbPromise.bind(this).then(this.install), + }; + } + + async install() { + const pluginInfo = pluginUtils.getPluginInfo(this.options.name); + this.options.pluginName = pluginInfo[0]; + this.options.pluginVersion = pluginInfo[1] || 'latest'; + + return BbPromise.bind(this) + .then(this.validate) + .then(this.getPlugins) + .then((plugins) => { + const plugin = plugins.find((item) => item.name === this.options.pluginName); + if (!plugin) { + this.serverless.cli.log('Plugin not found in serverless registry, continuing to install'); + } + return BbPromise.bind(this) + .then(this.pluginInstall) + .then(this.addPluginToServerlessFile) + .then(this.installPeerDependencies) + .then(() => { + const message = [ + 'Successfully installed', + ` "${this.options.pluginName}@${this.options.pluginVersion}"`, + ].join(''); + this.serverless.cli.log(message); + }); + }); + } + + async pluginInstall() { + const serviceDir = this.serverless.serviceDir; + const packageJsonFilePath = path.join(serviceDir, 'package.json'); + + return fileExists(packageJsonFilePath) + .then((exists) => { + // check if package.json is already present. Otherwise create one + if (!exists) { + this.serverless.cli.log('Creating an empty package.json file in your service directory'); + + const packageJsonFileContent = { + name: this.serverless.service.service, + description: '', + version: '0.1.0', + dependencies: {}, + devDependencies: {}, + }; + return fse.writeJson(packageJsonFilePath, packageJsonFileContent); + } + return BbPromise.resolve(); + }) + .then(() => { + // install the package through npm + const pluginFullName = `${this.options.pluginName}@${this.options.pluginVersion}`; + const message = [ + `Installing plugin "${pluginFullName}"`, + ' (this might take a few seconds...)', + ].join(''); + this.serverless.cli.log(message); + return this.npmInstall(pluginFullName); + }); + } + + async addPluginToServerlessFile() { + const serverlessFilePath = this.getServerlessFilePath(); + const fileExtension = path.extname(serverlessFilePath); + if (fileExtension === '.js' || fileExtension === '.ts') { + requestManualUpdate(serverlessFilePath); + return; + } + + const checkIsArrayPluginsObject = (pluginsObject) => + pluginsObject == null || Array.isArray(pluginsObject); + // pluginsObject type determined based on the value loaded during the serverless init. + if (_.last(serverlessFilePath.split('.')) === 'json') { + const serverlessFileObj = await fse.readJson(serverlessFilePath); + const newServerlessFileObj = serverlessFileObj; + const isArrayPluginsObject = checkIsArrayPluginsObject(newServerlessFileObj.plugins); + // null modules property is not supported + let plugins = isArrayPluginsObject + ? newServerlessFileObj.plugins || [] + : newServerlessFileObj.plugins.modules; + + if (plugins == null) { + throw new ServerlessError( + 'plugins modules property must be present', + 'PLUGINS_MODULES_MISSING' + ); + } + + plugins.push(this.options.pluginName); + plugins = _.sortedUniq(plugins); + + if (isArrayPluginsObject) { + newServerlessFileObj.plugins = plugins; + } else { + newServerlessFileObj.plugins.modules = plugins; + } + + await fse.writeJson(serverlessFilePath, newServerlessFileObj); + return; + } + + const serverlessFileObj = yaml.load(await fsp.readFile(serverlessFilePath, 'utf8'), { + filename: serverlessFilePath, + schema: cloudformationSchema, + }); + if (serverlessFileObj.plugins != null) { + // Plugins section can be behind veriables, opt-out in such case + if (isPlainObject(serverlessFileObj.plugins)) { + if ( + serverlessFileObj.plugins.modules != null && + !Array.isArray(serverlessFileObj.plugins.modules) + ) { + requestManualUpdate(serverlessFilePath); + return; + } + } else if (!Array.isArray(serverlessFileObj.plugins)) { + requestManualUpdate(serverlessFilePath); + return; + } + } + await yamlAstParser.addNewArrayItem( + serverlessFilePath, + checkIsArrayPluginsObject(serverlessFileObj.plugins) ? 'plugins' : 'plugins.modules', + this.options.pluginName + ); + } + + async installPeerDependencies() { + const pluginPackageJsonFilePath = path.join( + this.serverless.serviceDir, + 'node_modules', + this.options.pluginName, + 'package.json' + ); + return fse.readJson(pluginPackageJsonFilePath).then((pluginPackageJson) => { + if (pluginPackageJson.peerDependencies) { + const pluginsArray = []; + Object.entries(pluginPackageJson.peerDependencies).forEach(([k, v]) => { + pluginsArray.push(`${k}@"${v}"`); + }); + return BbPromise.map(pluginsArray, this.npmInstall); + } + return BbPromise.resolve(); + }); + } + + async npmInstall(name) { + return npmCommandDeferred.then((npmCommand) => + childProcess.execAsync(`${npmCommand} install --save-dev ${name}`, { + stdio: 'ignore', + }) + ); + } +} + +module.exports = PluginInstall; diff --git a/test/unit/lib/plugins/plugin/lib/utils.test.js b/test/unit/lib/plugins/plugin/lib/utils.test.js index ac7bb8d988d..4aad4710f9f 100644 --- a/test/unit/lib/plugins/plugin/lib/utils.test.js +++ b/test/unit/lib/plugins/plugin/lib/utils.test.js @@ -5,7 +5,7 @@ const sinon = require('sinon'); const BbPromise = require('bluebird'); const proxyquire = require('proxyquire'); const chalk = require('chalk'); -const PluginUninstall = require('../../../../../../lib/plugins/plugin/uninstall'); +const PluginInstall = require('../../../../../../lib/plugins/plugin/install'); const Serverless = require('../../../../../../lib/Serverless'); const CLI = require('../../../../../../lib/classes/CLI'); const { expect } = require('chai'); @@ -38,7 +38,7 @@ describe('PluginUtils', () => { serverless = new Serverless(); serverless.cli = new CLI(serverless); const options = {}; - pluginUtils = new PluginUninstall(serverless, options); + pluginUtils = new PluginInstall(serverless, options); consoleLogStub = sinon.stub(serverless.cli, 'consoleLog').returns(); }); From 846859aeebfb684e8a3495ce2597a711bccd29db Mon Sep 17 00:00:00 2001 From: Seungchan Ahn Date: Mon, 27 Sep 2021 23:25:06 +0900 Subject: [PATCH 36/36] Ensure backward compatibility --- lib/plugins/index.js | 1 + test/unit/lib/plugins/plugin/install.test.js | 561 +++++++++++++++++++ 2 files changed, 562 insertions(+) create mode 100644 test/unit/lib/plugins/plugin/install.test.js diff --git a/lib/plugins/index.js b/lib/plugins/index.js index af1460c64a6..1232a41e407 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -16,6 +16,7 @@ module.exports = [ require('./rollback.js'), require('./slstats.js'), require('./plugin/plugin.js'), + require('./plugin/install.js'), require('./plugin/uninstall.js'), require('./plugin/list.js'), require('./plugin/search.js'), diff --git a/test/unit/lib/plugins/plugin/install.test.js b/test/unit/lib/plugins/plugin/install.test.js new file mode 100644 index 00000000000..c8124ecf641 --- /dev/null +++ b/test/unit/lib/plugins/plugin/install.test.js @@ -0,0 +1,561 @@ +'use strict'; + +const chai = require('chai'); +const sinon = require('sinon'); +const BbPromise = require('bluebird'); +const yaml = require('js-yaml'); +const path = require('path'); +const childProcess = BbPromise.promisifyAll(require('child_process')); +const fs = require('fs'); +const fse = require('fs-extra'); +const PluginInstall = require('../../../../../lib/plugins/plugin/install'); +const Serverless = require('../../../../../lib/Serverless'); +const CLI = require('../../../../../lib/classes/CLI'); +const { expect } = require('chai'); +const { getTmpDirPath } = require('../../../../utils/fs'); + +chai.use(require('chai-as-promised')); + +describe('PluginInstall', () => { + let pluginInstall; + let serverless; + let serverlessErrorStub; + const plugins = [ + { + name: '@scope/serverless-plugin-1', + description: 'Scoped Serverless Plugin 1', + githubUrl: 'https://github.com/serverless/serverless-plugin-1', + }, + { + name: 'serverless-plugin-1', + description: 'Serverless Plugin 1', + githubUrl: 'https://github.com/serverless/serverless-plugin-1', + }, + { + name: 'serverless-plugin-2', + description: 'Serverless Plugin 2', + githubUrl: 'https://github.com/serverless/serverless-plugin-2', + }, + { + name: 'serverless-existing-plugin', + description: 'Serverless Existing plugin', + githubUrl: 'https://github.com/serverless/serverless-existing-plugin', + }, + ]; + + beforeEach(() => { + serverless = new Serverless(); + serverless.cli = new CLI(serverless); + const options = {}; + pluginInstall = new PluginInstall(serverless, options); + serverlessErrorStub = sinon.stub(serverless.classes, 'Error').throws(); + }); + + afterEach(() => { + serverless.classes.Error.restore(); + }); + + describe('#constructor()', () => { + let installStub; + + beforeEach(() => { + installStub = sinon.stub(pluginInstall, 'install').returns(BbPromise.resolve()); + }); + + afterEach(() => { + pluginInstall.install.restore(); + }); + + it('should have the sub-command "install"', () => { + expect(pluginInstall.commands.plugin.commands.install).to.not.equal(undefined); + }); + + it('should have the lifecycle event "install" for the "install" sub-command', () => { + expect(pluginInstall.commands.plugin.commands.install.lifecycleEvents).to.deep.equal([ + 'install', + ]); + }); + + it('should have a required option "name" for the "install" sub-command', () => { + // eslint-disable-next-line no-unused-expressions + expect(pluginInstall.commands.plugin.commands.install.options.name.required).to.be.true; + }); + + it('should have a "plugin:install:install" hook', () => { + expect(pluginInstall.hooks['plugin:install:install']).to.not.equal(undefined); + }); + + it('should run promise chain in order for "plugin:install:install" hook', () => + pluginInstall.hooks['plugin:install:install']().then(() => { + expect(installStub.calledOnce).to.equal(true); + })); + }); + + describe('#install()', () => { + let serviceDir; + let serverlessYmlFilePath; + let pluginInstallStub; + let validateStub; + let getPluginsStub; + let savedCwd; + let addPluginToServerlessFileStub; + let installPeerDependenciesStub; + + beforeEach(() => { + serviceDir = getTmpDirPath(); + pluginInstall.serverless.serviceDir = serviceDir; + fse.ensureDirSync(serviceDir); + serverlessYmlFilePath = path.join(serviceDir, 'serverless.yml'); + validateStub = sinon.stub(pluginInstall, 'validate').returns(BbPromise.resolve()); + pluginInstallStub = sinon.stub(pluginInstall, 'pluginInstall').returns(BbPromise.resolve()); + addPluginToServerlessFileStub = sinon + .stub(pluginInstall, 'addPluginToServerlessFile') + .returns(BbPromise.resolve()); + installPeerDependenciesStub = sinon + .stub(pluginInstall, 'installPeerDependencies') + .returns(BbPromise.resolve()); + getPluginsStub = sinon.stub(pluginInstall, 'getPlugins').returns(BbPromise.resolve(plugins)); + // save the cwd so that we can restore it later + savedCwd = process.cwd(); + process.chdir(serviceDir); + }); + + afterEach(() => { + pluginInstall.validate.restore(); + pluginInstall.getPlugins.restore(); + pluginInstall.pluginInstall.restore(); + pluginInstall.addPluginToServerlessFile.restore(); + pluginInstall.installPeerDependencies.restore(); + process.chdir(savedCwd); + }); + + it('should install the plugin if it can be found in the registry', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + }; + serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); + + pluginInstall.options.name = 'serverless-plugin-1'; + + return expect(pluginInstall.install()).to.be.fulfilled.then(() => { + expect(validateStub.calledOnce).to.equal(true); + expect(getPluginsStub.calledOnce).to.equal(true); + expect(pluginInstallStub.calledOnce).to.equal(true); + expect(serverlessErrorStub.calledOnce).to.equal(false); + expect(addPluginToServerlessFileStub.calledOnce).to.equal(true); + expect(installPeerDependenciesStub.calledOnce).to.equal(true); + }); + }); + + it('should install a scoped plugin if it can be found in the registry', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + }; + serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); + + pluginInstall.options.name = '@scope/serverless-plugin-1'; + + return expect(pluginInstall.install()).to.be.fulfilled.then(() => { + expect(validateStub.calledOnce).to.equal(true); + expect(getPluginsStub.calledOnce).to.equal(true); + expect(pluginInstallStub.calledOnce).to.equal(true); + expect(serverlessErrorStub.calledOnce).to.equal(false); + expect(addPluginToServerlessFileStub.calledOnce).to.equal(true); + expect(installPeerDependenciesStub.calledOnce).to.equal(true); + }); + }); + + it('should install the plugin even if it can not be found in the registry', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + }; + serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); + + pluginInstall.options.name = 'serverless-not-in-registry-plugin'; + return expect(pluginInstall.install()).to.be.fulfilled.then(() => { + expect(validateStub.calledOnce).to.equal(true); + expect(getPluginsStub.calledOnce).to.equal(true); + expect(pluginInstallStub.calledOnce).to.equal(true); + expect(serverlessErrorStub.calledOnce).to.equal(false); + expect(addPluginToServerlessFileStub.calledOnce).to.equal(true); + expect(installPeerDependenciesStub.calledOnce).to.equal(true); + }); + }); + + it('should apply the latest version if you can not get the version from name option', () => { + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + }; + serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); + pluginInstall.options.name = 'serverless-plugin-1'; + return expect(pluginInstall.install()).to.be.fulfilled.then(() => { + expect(pluginInstall.options.pluginName).to.be.equal('serverless-plugin-1'); + expect(pluginInstall.options.pluginVersion).to.be.equal('latest'); + }); + }); + + it( + 'should apply the latest version if you can not get the ' + + 'version from name option even if scoped', + () => { + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + }; + serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); + pluginInstall.options.name = '@scope/serverless-plugin-1'; + return expect(pluginInstall.install()).to.be.fulfilled.then(() => { + expect(pluginInstall.options.pluginName).to.be.equal('@scope/serverless-plugin-1'); + expect(pluginInstall.options.pluginVersion).to.be.equal('latest'); + }); + } + ); + + it('should apply the specified version if you can get the version from name option', () => { + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + }; + serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); + pluginInstall.options.name = 'serverless-plugin-1@1.0.0'; + return expect(pluginInstall.install()).to.be.fulfilled.then(() => { + expect(pluginInstall.options.pluginName).to.be.equal('serverless-plugin-1'); + expect(pluginInstall.options.pluginVersion).to.be.equal('1.0.0'); + }); + }); + }); + + describe('#pluginInstall()', () => { + let serviceDir; + let packageJsonFilePath; + let npmInstallStub; + let savedCwd; + + beforeEach(() => { + pluginInstall.options.pluginName = 'serverless-plugin-1'; + pluginInstall.options.pluginVersion = 'latest'; + serviceDir = getTmpDirPath(); + pluginInstall.serverless.serviceDir = serviceDir; + fse.ensureDirSync(serviceDir); + packageJsonFilePath = path.join(serviceDir, 'package.json'); + npmInstallStub = sinon.stub(childProcess, 'execAsync').callsFake(() => { + const packageJson = serverless.utils.readFileSync(packageJsonFilePath, 'utf8'); + packageJson.devDependencies = { + 'serverless-plugin-1': 'latest', + }; + serverless.utils.writeFileSync(packageJsonFilePath, packageJson); + return BbPromise.resolve(); + }); + + // save the cwd so that we can restore it later + savedCwd = process.cwd(); + process.chdir(serviceDir); + }); + + afterEach(() => { + childProcess.execAsync.restore(); + process.chdir(savedCwd); + }); + + it('should install the plugin if it has not been installed yet', () => { + const packageJson = { + name: 'test-service', + description: '', + version: '0.1.0', + dependencies: {}, + devDependencies: {}, + }; + + serverless.utils.writeFileSync(packageJsonFilePath, packageJson); + + return expect(pluginInstall.pluginInstall()).to.be.fulfilled.then(() => + Promise.all([ + expect( + npmInstallStub.calledWithExactly('npm install --save-dev serverless-plugin-1@latest', { + stdio: 'ignore', + }) + ).to.equal(true), + expect(serverlessErrorStub.calledOnce).to.equal(false), + ]) + ); + }); + + it('should generate a package.json file in the service directory if not present', () => + expect(pluginInstall.pluginInstall()).to.be.fulfilled.then(() => { + expect( + npmInstallStub.calledWithExactly('npm install --save-dev serverless-plugin-1@latest', { + stdio: 'ignore', + }) + ).to.equal(true); + expect(fs.existsSync(packageJsonFilePath)).to.equal(true); + })); + }); + + describe('#addPluginToServerlessFile()', () => { + let serviceDir; + let serverlessYmlFilePath; + + beforeEach(() => { + serviceDir = getTmpDirPath(); + pluginInstall.serverless.serviceDir = pluginInstall.serverless.serviceDir = serviceDir; + pluginInstall.serverless.configurationFilename = 'serverless.yml'; + serverlessYmlFilePath = path.join(serviceDir, 'serverless.yml'); + }); + + it('should add the plugin to the service file if plugins array is not present', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + // no plugins array here + }; + serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); + + pluginInstall.options.pluginName = 'serverless-plugin-1'; + + return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { + expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')).to.deep.equal( + Object.assign({}, serverlessYml, { + plugins: ['serverless-plugin-1'], + }) + ); + }); + }); + + it('should push the plugin to the service files plugin array if present', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + plugins: ['serverless-existing-plugin'], // one plugin was already added + }; + serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); + + pluginInstall.options.pluginName = 'serverless-plugin-1'; + + return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { + expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')).to.deep.equal( + Object.assign({}, serverlessYml, { + plugins: ['serverless-existing-plugin', 'serverless-plugin-1'], + }) + ); + }); + }); + + it('should add the plugin to serverless file path for a .yaml file', () => { + const serverlessYamlFilePath = path.join(serviceDir, 'serverless.yaml'); + pluginInstall.serverless.configurationFilename = 'serverless.yaml'; + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + }; + serverless.utils.writeFileSync(serverlessYamlFilePath, yaml.dump(serverlessYml)); + pluginInstall.options.pluginName = 'serverless-plugin-1'; + return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { + expect(serverless.utils.readFileSync(serverlessYamlFilePath, 'utf8')).to.deep.equal( + Object.assign({}, serverlessYml, { plugins: ['serverless-plugin-1'] }) + ); + }); + }); + + it('should add the plugin to serverless file path for a .json file', () => { + const serverlessJsonFilePath = path.join(serviceDir, 'serverless.json'); + pluginInstall.serverless.configurationFilename = 'serverless.json'; + const serverlessJson = { + service: 'plugin-service', + provider: 'aws', + }; + serverless.utils.writeFileSync(serverlessJsonFilePath, serverlessJson); + pluginInstall.options.pluginName = 'serverless-plugin-1'; + return expect(pluginInstall.addPluginToServerlessFile()) + .to.be.fulfilled.then(() => { + expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal( + Object.assign({}, serverlessJson, { plugins: ['serverless-plugin-1'] }) + ); + }) + .then(() => { + pluginInstall.options.pluginName = 'serverless-plugin-2'; + return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { + expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal( + Object.assign({}, serverlessJson, { + plugins: ['serverless-plugin-1', 'serverless-plugin-2'], + }) + ); + }); + }); + }); + + it('should not modify serverless .js file', async () => { + const serverlessJsFilePath = path.join(serviceDir, 'serverless.js'); + pluginInstall.serverless.configurationFilename = 'serverless.js'; + const serverlessJson = { + service: 'plugin-service', + provider: 'aws', + plugins: [], + }; + serverless.utils.writeFileSync( + serverlessJsFilePath, + `module.exports = ${JSON.stringify(serverlessJson)};` + ); + pluginInstall.options.pluginName = 'serverless-plugin-1'; + await pluginInstall.addPluginToServerlessFile(); + // use require to load serverless.js + // eslint-disable-next-line global-require + expect(require(serverlessJsFilePath).plugins).to.be.deep.equal([]); + }); + + it('should not modify serverless .ts file', () => { + const serverlessTsFilePath = path.join(serviceDir, 'serverless.ts'); + pluginInstall.serverless.configurationFilename = 'serverless.ts'; + const serverlessJson = { + service: 'plugin-service', + provider: 'aws', + plugins: [], + }; + serverless.utils.writeFileSync( + serverlessTsFilePath, + `module.exports = ${JSON.stringify(serverlessJson)};` + ); + pluginInstall.options.pluginName = 'serverless-plugin-1'; + return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { + // use require to load serverless.js + // eslint-disable-next-line global-require + expect(require(serverlessTsFilePath).plugins).to.be.deep.equal([]); + }); + }); + + describe('if plugins object is not array', () => { + it('should add the plugin to the service file', () => { + // serverless.yml + const serverlessYml = { + service: 'plugin-service', + provider: 'aws', + plugins: { + localPath: 'test', + modules: [], + }, + }; + serverless.utils.writeFileSync(serverlessYmlFilePath, yaml.dump(serverlessYml)); + + pluginInstall.options.pluginName = 'serverless-plugin-1'; + + return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { + expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')).to.deep.equal( + Object.assign({}, serverlessYml, { + plugins: { + localPath: 'test', + modules: [pluginInstall.options.pluginName], + }, + }) + ); + }); + }); + + it('should add the plugin to serverless file path for a .json file', () => { + const serverlessJsonFilePath = path.join(serviceDir, 'serverless.json'); + pluginInstall.serverless.configurationFilename = 'serverless.json'; + const serverlessJson = { + service: 'plugin-service', + provider: 'aws', + plugins: { + localPath: 'test', + modules: [], + }, + }; + serverless.utils.writeFileSync(serverlessJsonFilePath, serverlessJson); + pluginInstall.options.pluginName = 'serverless-plugin-1'; + return expect(pluginInstall.addPluginToServerlessFile()) + .to.be.fulfilled.then(() => { + expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal( + Object.assign({}, serverlessJson, { + plugins: { + localPath: 'test', + modules: [pluginInstall.options.pluginName], + }, + }) + ); + }) + .then(() => { + pluginInstall.options.pluginName = 'serverless-plugin-2'; + return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => { + expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal( + Object.assign({}, serverlessJson, { + plugins: { + localPath: 'test', + modules: ['serverless-plugin-1', 'serverless-plugin-2'], + }, + }) + ); + }); + }); + }); + }); + }); + + describe('#installPeerDependencies()', () => { + let serviceDir; + let servicePackageJsonFilePath; + let pluginPath; + let pluginPackageJsonFilePath; + let pluginName; + let npmInstallStub; + let savedCwd; + + beforeEach(() => { + pluginName = 'some-plugin'; + pluginInstall.options.pluginName = pluginName; + serviceDir = getTmpDirPath(); + fse.ensureDirSync(serviceDir); + pluginInstall.serverless.serviceDir = serviceDir; + servicePackageJsonFilePath = path.join(serviceDir, 'package.json'); + fse.writeJsonSync(servicePackageJsonFilePath, { + devDependencies: {}, + }); + pluginPath = path.join(serviceDir, 'node_modules', pluginName); + fse.ensureDirSync(pluginPath); + pluginPackageJsonFilePath = path.join(pluginPath, 'package.json'); + npmInstallStub = sinon.stub(childProcess, 'execAsync').returns(BbPromise.resolve()); + savedCwd = process.cwd(); + process.chdir(serviceDir); + }); + + afterEach(() => { + childProcess.execAsync.restore(); + process.chdir(savedCwd); + }); + + it('should install peerDependencies if an installed plugin has ones', () => { + fse.writeJsonSync(pluginPackageJsonFilePath, { + peerDependencies: { + 'some-package': '*', + }, + }); + return expect(pluginInstall.installPeerDependencies()).to.be.fulfilled.then(() => { + expect( + npmInstallStub.calledWithExactly('npm install --save-dev some-package@"*"', { + stdio: 'ignore', + }) + ).to.equal(true); + }); + }); + + it('should not install peerDependencies if an installed plugin does not have ones', () => { + fse.writeJsonSync(pluginPackageJsonFilePath, {}); + return expect(pluginInstall.installPeerDependencies()).to.be.fulfilled.then(() => { + expect(fse.readJsonSync(servicePackageJsonFilePath)).to.be.deep.equal({ + devDependencies: {}, + }); + expect(npmInstallStub.calledWithExactly('npm install', { stdio: 'ignore' })).to.equal( + false + ); + }); + }); + }); +});