mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
415 lines
14 KiB
JavaScript
415 lines
14 KiB
JavaScript
|
|
'use strict';
|
|
|
|
const BbPromise = require('bluebird');
|
|
const childProcess = require('child_process');
|
|
const fetch = require('node-fetch');
|
|
const chalk = require('chalk');
|
|
const fs = require('fs');
|
|
const fse = require('fs-extra');
|
|
const path = require('path');
|
|
const _ = require('lodash');
|
|
const userStats = require('../../utils/userStats');
|
|
const yamlAstParser = require('../../utils/yamlAstParser');
|
|
|
|
class Plugin {
|
|
constructor(serverless, options) {
|
|
this.serverless = serverless;
|
|
this.options = options;
|
|
|
|
this.commands = {
|
|
plugin: {
|
|
usage: 'Plugin management for Serverless',
|
|
commands: {
|
|
install: {
|
|
usage: 'Install and add a plugin to your service',
|
|
lifecycleEvents: [
|
|
'install',
|
|
],
|
|
options: {
|
|
name: {
|
|
usage: 'The plugin name',
|
|
required: true,
|
|
shortcut: 'n',
|
|
},
|
|
version: {
|
|
usage: 'The plugin version',
|
|
shortcut: 'v',
|
|
},
|
|
},
|
|
},
|
|
uninstall: {
|
|
usage: 'Uninstall and remove a plugin from your service',
|
|
lifecycleEvents: [
|
|
'uninstall',
|
|
],
|
|
options: {
|
|
name: {
|
|
usage: 'The plugin name',
|
|
required: true,
|
|
shortcut: 'n',
|
|
},
|
|
},
|
|
},
|
|
list: {
|
|
usage: 'Lists all available plugins',
|
|
lifecycleEvents: [
|
|
'list',
|
|
],
|
|
},
|
|
search: {
|
|
usage: 'Search for plugins',
|
|
lifecycleEvents: [
|
|
'search',
|
|
],
|
|
options: {
|
|
query: {
|
|
usage: 'Search query',
|
|
required: true,
|
|
shortcut: 'q',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
this.hooks = {
|
|
'plugin:install:install': () => BbPromise.bind(this)
|
|
.then(this.install)
|
|
.then(this.trackPluginInstall),
|
|
'plugin:uninstall:uninstall': () => BbPromise.bind(this)
|
|
.then(this.uninstall)
|
|
.then(this.trackPluginUninstall),
|
|
'plugin:list:list': () => BbPromise.bind(this)
|
|
.then(this.list)
|
|
.then(this.trackPluginList),
|
|
'plugin:search:search': () => BbPromise.bind(this)
|
|
.then(this.search)
|
|
.then(this.trackPluginSearch),
|
|
};
|
|
}
|
|
|
|
install() {
|
|
return BbPromise.bind(this)
|
|
.then(this.validate)
|
|
.then(this.getPlugins)
|
|
.then((plugins) => {
|
|
const pluginName = this.options.name;
|
|
const plugin = plugins.find((item) => item.name === pluginName);
|
|
const pluginVersion = this.options.version || 'latest';
|
|
|
|
if (!plugin) {
|
|
const message = `Plugin "${pluginName}" not found. Did you spell it correct?`;
|
|
this.serverless.cli.log(message);
|
|
} else if (this.checkPluginExists()) {
|
|
const message = `Plugin "${pluginName}" has been already installed.`;
|
|
this.serverless.cli.log(message);
|
|
} else {
|
|
return this.pluginInstall().then(pluginInstalled => {
|
|
if (pluginInstalled) {
|
|
return BbPromise.bind(this)
|
|
.then(this.addPluginToServerlessFile)
|
|
.then(this.installPeerDependencies)
|
|
.then(() => {
|
|
this.serverless.cli.log(`Successfully installed "${pluginName}@${pluginVersion}"`);
|
|
});
|
|
}
|
|
const message = 'An error occurred while installing your plugin. Please try again...';
|
|
this.serverless.cli.log(message);
|
|
return BbPromise.resolve();
|
|
});
|
|
}
|
|
return BbPromise.resolve();
|
|
});
|
|
}
|
|
|
|
uninstall() {
|
|
return BbPromise.bind(this)
|
|
.then(this.validate)
|
|
.then(this.getPlugins)
|
|
.then(plugins => {
|
|
const pluginName = this.options.name;
|
|
const plugin = plugins.find((item) => item.name === pluginName);
|
|
if (!plugin) {
|
|
const message = `Plugin "${pluginName}" not found. Did you spell it correct?`;
|
|
this.serverless.cli.log(message);
|
|
} else if (!this.checkPluginExists()) {
|
|
const message = `Plugin "${pluginName}" has been already uninstalled.`;
|
|
this.serverless.cli.log(message);
|
|
} else {
|
|
// uninstall the package through npm
|
|
this.serverless.cli
|
|
.log(`Uninstalling plugin "${pluginName}" (this might take a few seconds...)`);
|
|
|
|
return BbPromise.bind(this)
|
|
.then(this.uninstallPeerDependencies)
|
|
.then(this.pluginUninstall)
|
|
.then(pluginStillAvailable => {
|
|
if (!pluginStillAvailable) {
|
|
return this.removePluginFromServerlessFile()
|
|
.then(() => {
|
|
this.serverless.cli.log(`Successfully uninstalled "${pluginName}"`);
|
|
});
|
|
}
|
|
const message = 'An error occurred while uninstalling your plugin. Please try again...';
|
|
this.serverless.cli.log(message);
|
|
return BbPromise.resolve();
|
|
});
|
|
}
|
|
return BbPromise.resolve();
|
|
});
|
|
}
|
|
|
|
list() {
|
|
return BbPromise.bind(this)
|
|
.then(this.getPlugins)
|
|
.then((plugins) => this.display(plugins));
|
|
}
|
|
|
|
search() {
|
|
return BbPromise.bind(this)
|
|
.then(this.getPlugins)
|
|
.then((plugins) => {
|
|
// filter out plugins which match the query
|
|
const regex = new RegExp(this.options.query);
|
|
|
|
const filteredPlugins = plugins.filter((plugin) =>
|
|
(plugin.name.match(regex) || plugin.description.match(regex))
|
|
);
|
|
|
|
// print a message with the search result
|
|
const pluginCount = filteredPlugins.length;
|
|
const query = this.options.query;
|
|
const message = `${pluginCount} plugin(s) found for your search query "${query}"\n`;
|
|
this.serverless.cli.consoleLog(chalk.yellow(message));
|
|
|
|
return filteredPlugins;
|
|
})
|
|
.then((plugins) => {
|
|
this.display(plugins);
|
|
});
|
|
}
|
|
|
|
// helper methods
|
|
validate() {
|
|
if (!this.serverless.config.servicePath) {
|
|
throw new this.serverless.classes
|
|
.Error('This command can only be run inside a service directory');
|
|
}
|
|
|
|
return BbPromise.resolve();
|
|
}
|
|
|
|
getServerlessFilePath() {
|
|
const servicePath = this.serverless.config.servicePath;
|
|
const serverlessYmlFilePath = path.join(servicePath, 'serverless.yml');
|
|
const serverlessYamlFilePath = path.join(servicePath, 'serverless.yaml');
|
|
const serverlessJsonFilePath = path.join(servicePath, 'serverless.json');
|
|
|
|
let serverlessFilePath;
|
|
if (fs.existsSync(serverlessYmlFilePath)) {
|
|
serverlessFilePath = serverlessYmlFilePath;
|
|
} else if (fs.existsSync(serverlessYamlFilePath)) {
|
|
serverlessFilePath = serverlessYamlFilePath;
|
|
} else {
|
|
serverlessFilePath = serverlessJsonFilePath;
|
|
}
|
|
|
|
return serverlessFilePath;
|
|
}
|
|
|
|
getPlugins() {
|
|
const endpoint = 'https://raw.githubusercontent.com/serverless/plugins/master/plugins.json';
|
|
|
|
return fetch(endpoint).then((result) => result.json()).then((json) => json);
|
|
}
|
|
|
|
pluginInstall() {
|
|
const servicePath = this.serverless.config.servicePath;
|
|
const packageJsonFilePath = path.join(servicePath, 'package.json');
|
|
const pluginName = this.options.name;
|
|
const pluginVersion = this.options.version || 'latest';
|
|
|
|
// check if package.json is already present. Otherwise create one
|
|
if (!fs.existsSync(packageJsonFilePath)) {
|
|
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: {},
|
|
};
|
|
|
|
fse.writeJsonSync(packageJsonFilePath, packageJsonFileContent);
|
|
}
|
|
|
|
// install the package through npm
|
|
this.serverless.cli.log(
|
|
`Installing plugin "${pluginName}@${pluginVersion}" (this might take a few seconds...)`
|
|
);
|
|
|
|
childProcess
|
|
.execSync(`npm install --save-dev ${pluginName}@${pluginVersion}`, {
|
|
stdio: 'ignore',
|
|
});
|
|
const pluginInstalled = !!JSON.parse(
|
|
fs.readFileSync(packageJsonFilePath).toString()
|
|
).devDependencies[pluginName];
|
|
return BbPromise.resolve(pluginInstalled);
|
|
}
|
|
|
|
pluginUninstall() {
|
|
const pluginName = this.options.name;
|
|
const servicePath = this.serverless.config.servicePath;
|
|
const packageJsonFilePath = path.join(servicePath, 'package.json');
|
|
childProcess
|
|
.execSync(`npm uninstall --save-dev ${pluginName}`, {
|
|
stdio: 'ignore',
|
|
});
|
|
|
|
// check if plugin was uninstalled correctly
|
|
const pluginStillAvailable = !!JSON.parse(
|
|
fs.readFileSync(packageJsonFilePath).toString()
|
|
).devDependencies[pluginName];
|
|
return BbPromise.resolve(pluginStillAvailable);
|
|
}
|
|
|
|
addPluginToServerlessFile() {
|
|
const pluginName = this.options.name;
|
|
const serverlessFilePath = this.getServerlessFilePath();
|
|
if (_.last(_.split(serverlessFilePath, '.')) === 'json') {
|
|
const serverlessFileObj = fse.readJsonSync(serverlessFilePath);
|
|
if (serverlessFileObj.plugins) {
|
|
serverlessFileObj.plugins.push(pluginName);
|
|
} else {
|
|
serverlessFileObj.plugins = [pluginName];
|
|
}
|
|
serverlessFileObj.plugins = _.sortedUniq(serverlessFileObj.plugins);
|
|
fse.writeJsonSync(serverlessFilePath, serverlessFileObj);
|
|
} else {
|
|
yamlAstParser.addNewArrayItem(serverlessFilePath, 'plugins', pluginName);
|
|
}
|
|
return BbPromise.resolve();
|
|
}
|
|
|
|
removePluginFromServerlessFile() {
|
|
const pluginName = this.options.name;
|
|
const serverlessFilePath = this.getServerlessFilePath();
|
|
if (_.last(_.split(serverlessFilePath, '.')) === 'json') {
|
|
const serverlessFileObj = fse.readJsonSync(serverlessFilePath);
|
|
if (serverlessFileObj.plugins) {
|
|
_.pull(serverlessFileObj.plugins, pluginName);
|
|
if (_.isEmpty(serverlessFileObj.plugins)) {
|
|
_.unset(serverlessFileObj, 'plugins');
|
|
}
|
|
}
|
|
fse.writeJsonSync(serverlessFilePath, serverlessFileObj);
|
|
} else {
|
|
yamlAstParser.removeExistingArrayItem(serverlessFilePath, 'plugins', pluginName);
|
|
}
|
|
return BbPromise.resolve();
|
|
}
|
|
|
|
checkPluginExists() {
|
|
const pluginName = this.options.name;
|
|
const serverlessFilePath = this.getServerlessFilePath();
|
|
if (_.last(_.split(serverlessFilePath, '.')) === 'json') {
|
|
const serverlessFileObj = fse.readJsonSync(serverlessFilePath);
|
|
if (serverlessFileObj.plugins && _.includes(serverlessFileObj.plugins, pluginName)) {
|
|
return true;
|
|
}
|
|
} else if (yamlAstParser.checkArrayItemExists(serverlessFilePath, 'plugins', pluginName)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
installPeerDependencies() {
|
|
const pluginName = this.options.name;
|
|
const pluginPackageJsonFilePath = path.join(this.serverless.config.servicePath,
|
|
'node_modules', pluginName, 'package.json');
|
|
const servicePackageJsonFilePath = path.join(this.serverless.config.servicePath,
|
|
'package.json');
|
|
if (fs.existsSync(pluginPackageJsonFilePath) && fs.existsSync(servicePackageJsonFilePath)) {
|
|
const pluginPackageJson = fse.readJsonSync(pluginPackageJsonFilePath);
|
|
if (pluginPackageJson.peerDependencies) {
|
|
const servicePackageJson = fse.readJsonSync(servicePackageJsonFilePath);
|
|
servicePackageJson.devDependencies =
|
|
_.merge(servicePackageJson.devDependencies, pluginPackageJson.peerDependencies);
|
|
fse.writeJsonSync(servicePackageJsonFilePath, servicePackageJson);
|
|
childProcess
|
|
.execSync('npm install', {
|
|
stdio: 'ignore',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
uninstallPeerDependencies() {
|
|
const pluginName = this.options.name;
|
|
const pluginPackageJsonFilePath = path.join(this.serverless.config.servicePath,
|
|
'node_modules', pluginName, 'package.json');
|
|
if (fs.existsSync(pluginPackageJsonFilePath)) {
|
|
const pluginPackageJson = fse.readJsonSync(pluginPackageJsonFilePath);
|
|
if (pluginPackageJson.peerDependencies) {
|
|
_.forEach(pluginPackageJson.peerDependencies, (v, k) => {
|
|
childProcess
|
|
.execSync(`npm uninstall --save-dev ${k}`, {
|
|
stdio: 'ignore',
|
|
});
|
|
});
|
|
}
|
|
}
|
|
return BbPromise.resolve();
|
|
}
|
|
|
|
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.serverless.cli.consoleLog(message);
|
|
this.serverless.cli.consoleLog(`
|
|
To install a plugin run 'sls plugin install --name plugin-name-here'
|
|
|
|
It will be automatically downloaded, added to package.json & added to serverless.yml
|
|
`);
|
|
} else {
|
|
message = 'There are no plugins available to display';
|
|
this.serverless.cli.consoleLog(message);
|
|
}
|
|
return BbPromise.resolve(message);
|
|
}
|
|
|
|
trackPluginInstall() {
|
|
userStats.track('service_pluginInstalled');
|
|
return BbPromise.resolve();
|
|
}
|
|
|
|
trackPluginUninstall() {
|
|
userStats.track('service_pluginUninstalled');
|
|
return BbPromise.resolve();
|
|
}
|
|
|
|
trackPluginList() {
|
|
userStats.track('service_pluginListed');
|
|
return BbPromise.resolve();
|
|
}
|
|
|
|
trackPluginSearch() {
|
|
userStats.track('service_pluginSearched');
|
|
return BbPromise.resolve();
|
|
}
|
|
}
|
|
|
|
module.exports = Plugin;
|