Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate PluginInstall plugin logics into standalone command #9942

Merged
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
74dcaab
Migrate `PluginInstall` plugin logics into standalone command
issea1015 Sep 12, 2021
5051581
Wait async-able standalone commands run
issea1015 Sep 13, 2021
411ce04
Rely on native promises
issea1015 Sep 19, 2021
5fbac57
Keep all private modules in `/lib`
issea1015 Sep 19, 2021
8179d00
Choose better variable naming
issea1015 Sep 19, 2021
4cc6bb3
Merge remote-tracking branch 'origin/master' into 9740-seclude-plugin…
issea1015 Sep 21, 2021
2a62902
Add unit test for "plugin install" command
issea1015 Sep 22, 2021
8864e88
Unset `only` on mocha test
issea1015 Sep 22, 2021
3b26b4a
Update module pathes in document
issea1015 Sep 22, 2021
b2017bd
Rely on async/await syntax
issea1015 Sep 22, 2021
65dd658
Rely on async/await syntax
issea1015 Sep 22, 2021
c1b1c9a
Return promise to be waited
issea1015 Sep 22, 2021
e2281c6
Run npm command in service directory
issea1015 Sep 22, 2021
b6b6bfd
Rely on `child-process-ext/spawn`
issea1015 Sep 22, 2021
19eb1d3
Rely on async/await syntax
issea1015 Sep 22, 2021
5f9c065
Rely on async/await syntax
issea1015 Sep 22, 2021
7058e0f
Destructure class into functions
issea1015 Sep 22, 2021
ae67be8
Manage edge case on installing plugin
issea1015 Sep 22, 2021
320e3ca
Alter binding for plugin management related utils
issea1015 Sep 22, 2021
ca471c6
Improve interface of `getPluginInfo` function
issea1015 Sep 22, 2021
f00c9db
Don't change input arguments
issea1015 Sep 22, 2021
46b6db9
Reduce unnecessary log
issea1015 Sep 22, 2021
54f54f2
Stop doing things beyond the responsibilities
issea1015 Sep 22, 2021
d04f05f
Remove unused util
issea1015 Sep 22, 2021
c760f52
Align to sub-level commands naming policy
issea1015 Sep 26, 2021
4aefbb4
Tear down correctly after run standalone commands
issea1015 Sep 26, 2021
0f144f9
Stop taking care of peer dependencies when installing plugin
issea1015 Sep 26, 2021
96aab3b
Confirm whether plugin is not already installed
issea1015 Sep 26, 2021
afcdd19
Reduce duplicate calculations
issea1015 Sep 26, 2021
645344b
Rely on `@serverless/utils/log`
issea1015 Sep 26, 2021
ce0ff93
Unset `only` on mocha test
issea1015 Sep 26, 2021
f32b6c3
Prefer verb first function name
issea1015 Sep 27, 2021
25b5099
Enable to upgrade plugin
issea1015 Sep 27, 2021
7308c9f
Leave minimum conditions
issea1015 Sep 27, 2021
f1a8542
Reduce redundant variable
issea1015 Sep 27, 2021
29b4a7e
Ensure backward compatibility
issea1015 Sep 27, 2021
846859a
Ensure backward compatibility
issea1015 Sep 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
175 changes: 175 additions & 0 deletions commands/plugin/install.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
'use strict';
issea1015 marked this conversation as resolved.
Show resolved Hide resolved

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 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');
issea1015 marked this conversation as resolved.
Show resolved Hide resolved
const {
getPlugins,
getPluginInfo,
getServerlessFilePath,
validate,
} = require('../../lib/commands/plugin-management');

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';
issea1015 marked this conversation as resolved.
Show resolved Hide resolved

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');
}
issea1015 marked this conversation as resolved.
Show resolved Hide resolved

await pluginInstall({ configuration, serviceDir, options });
issea1015 marked this conversation as resolved.
Show resolved Hide resolved
await addPluginToServerlessFile({ serviceDir, configurationFilename, options });
await installPeerDependencies({ serviceDir, options });
issea1015 marked this conversation as resolved.
Show resolved Hide resolved

const message = [
'Successfully installed',
` "${options.pluginName}@${options.pluginVersion}"`,
].join('');
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: {},
issea1015 marked this conversation as resolved.
Show resolved Hide resolved
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 });
};

const addPluginToServerlessFile = async ({ serviceDir, configurationFilename, options }) => {
const serverlessFilePath = getServerlessFilePath({ serviceDir, configurationFilename });
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(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',
options.pluginName
);
};

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',
// To parse quotes used in module versions. E.g. 'serverless@"^1.60.0 || 2"'
// https://stackoverflow.com/a/48015470
shell: true,
});
};

const requestManualUpdate = (serverlessFilePath) =>
log(`
Can't automatically add plugin into "${path.basename(serverlessFilePath)}" file.
Please make it manually.
`);
91 changes: 91 additions & 0 deletions lib/commands/plugin-management.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'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');
const ServerlessError = require('../../lib/serverless-error');

const cli = new CLI(undefined);

module.exports = {
validate({ serviceDir }) {
if (!serviceDir) {
throw new ServerlessError(
'This command can only be run inside a service directory',
'MISSING_SERVICE_DIRECTORY'
);
}
},

getServerlessFilePath({ serviceDir, configurationFilename }) {
if (configurationFilename) {
return path.resolve(serviceDir, 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));
}

const result = await fetch(endpoint, options);
return result.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;
issea1015 marked this conversation as resolved.
Show resolved Hide resolved
},

async display(plugins) {
issea1015 marked this conversation as resolved.
Show resolved Hide resolved
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);
issea1015 marked this conversation as resolved.
Show resolved Hide resolved
},
};
1 change: 0 additions & 1 deletion lib/plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ module.exports = [
require('./rollback.js'),
require('./slstats.js'),
require('./plugin/plugin.js'),
require('./plugin/install.js'),
issea1015 marked this conversation as resolved.
Show resolved Hide resolved
require('./plugin/uninstall.js'),
require('./plugin/list.js'),
require('./plugin/search.js'),
Expand Down