Skip to content

Commit

Permalink
refactor(Plugins): Seclude plugin uninstall standalone command
Browse files Browse the repository at this point in the history
(PR #10015)
  • Loading branch information
issea1015 committed Sep 28, 2021
1 parent 6f88c11 commit 26ce1c6
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 9 deletions.
11 changes: 3 additions & 8 deletions commands/plugin-install.js
Expand Up @@ -30,17 +30,12 @@ module.exports = async ({ configuration, serviceDir, configurationFilename, opti
await installPlugin(context);
await addPluginToServerlessFile(context);

const message = ['Successfully installed', ` "${pluginName}@${pluginVersion}"`].join('');
log(message);
log(`Successfully installed "${pluginName}@${pluginVersion}"`);
};

const installPlugin = async ({ serviceDir, pluginName, pluginVersion }) => {
const pluginFullName = `${pluginName}@${pluginVersion}`;
const message = [
`Installing plugin "${pluginFullName}"`,
' (this might take a few seconds...)',
].join('');
log(message);
log(`Installing plugin "${pluginFullName}" (this might take a few seconds...)`);
await npmInstall(pluginFullName, { serviceDir });
};

Expand All @@ -54,7 +49,7 @@ const addPluginToServerlessFile = async ({ configurationFilePath, pluginName })
const checkIsArrayPluginsObject = (pluginsObject) =>
pluginsObject == null || Array.isArray(pluginsObject);
// pluginsObject type determined based on the value loaded during the serverless init.
if (_.last(configurationFilePath.split('.')) === 'json') {
if (fileExtension === '.json') {
const serverlessFileObj = await fse.readJson(configurationFilePath);
const newServerlessFileObj = serverlessFileObj;
const isArrayPluginsObject = checkIsArrayPluginsObject(newServerlessFileObj.plugins);
Expand Down
110 changes: 110 additions & 0 deletions commands/plugin-uninstall.js
@@ -0,0 +1,110 @@
'use strict';

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 yamlAstParser = require('../lib/utils/yamlAstParser');
const npmCommandDeferred = require('../lib/utils/npm-command-deferred');
const {
getPluginInfo,
getServerlessFilePath,
validate,
} = require('../lib/commands/plugin-management');

module.exports = async ({ configuration, serviceDir, configurationFilename, options }) => {
validate({ serviceDir });

const pluginInfo = getPluginInfo(options.name);
const pluginName = pluginInfo.name;
const configurationFilePath = getServerlessFilePath({ serviceDir, configurationFilename });

const context = { configuration, serviceDir, configurationFilePath, pluginName };
await uninstallPlugin(context);
await removePluginFromServerlessFile(context);

log(`Successfully uninstalled "${pluginName}"`);
};

const uninstallPlugin = async ({ serviceDir, pluginName }) => {
log(`Uninstalling plugin "${pluginName}" (this might take a few seconds...)`);
await npmUninstall(pluginName, { serviceDir });
};

const removePluginFromServerlessFile = async ({ configurationFilePath, pluginName }) => {
const fileExtension = path.extname(configurationFilePath);
if (fileExtension === '.js' || fileExtension === '.ts') {
requestManualUpdate(configurationFilePath);
return;
}

if (fileExtension === '.json') {
const serverlessFileObj = await fse.readJson(configurationFilePath);
const isArrayPluginsObject = Array.isArray(serverlessFileObj.plugins);
const plugins = isArrayPluginsObject
? serverlessFileObj.plugins
: serverlessFileObj.plugins && serverlessFileObj.plugins.modules;

if (plugins) {
_.pull(plugins, pluginName);
if (!plugins.length) {
if (isArrayPluginsObject) {
delete serverlessFileObj.plugins;
} else {
delete serverlessFileObj.plugins.modules;
}
}
await fse.writeJson(configurationFilePath, serverlessFileObj);
}
return;
}

const serverlessFileObj = yaml.load(await fsp.readFile(configurationFilePath, 'utf8'), {
filename: configurationFilePath,
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(configurationFilePath);
return;
}
} else if (!Array.isArray(serverlessFileObj.plugins)) {
requestManualUpdate(configurationFilePath);
return;
}
}
await yamlAstParser.removeExistingArrayItem(
configurationFilePath,
Array.isArray(serverlessFileObj.plugins) ? 'plugins' : 'plugins.modules',
pluginName
);
};

const npmUninstall = async (name, { serviceDir }) => {
const { command, args } = await npmCommandDeferred;
try {
await spawn(command, [...args, 'uninstall', '--save-dev', name], {
cwd: serviceDir,
stdio: 'pipe',
});
} catch (error) {
process.stdout.write(error.stderrBuffer);
throw error;
}
};

const requestManualUpdate = (configurationFilePath) =>
log(`
Can't automatically remove plugin from "${path.basename(configurationFilePath)}" file.
Please do it manually.
`);
2 changes: 2 additions & 0 deletions lib/plugins/plugin/uninstall.js
@@ -1,3 +1,5 @@
// TODO: Remove in v3

'use strict';

const BbPromise = require('bluebird');
Expand Down
2 changes: 1 addition & 1 deletion scripts/serverless.js
Expand Up @@ -41,7 +41,7 @@ let hasTelemetryBeenReported = false;
// to propery handle e.g. `SIGINT` interrupt
const keepAliveTimer = setTimeout(() => {}, 60 * 60 * 1000);

const standaloneCommands = new Set(['plugin install']);
const standaloneCommands = new Set(['plugin install', 'plugin uninstall']);

process.once('uncaughtException', (error) => {
clearTimeout(keepAliveTimer);
Expand Down
66 changes: 66 additions & 0 deletions test/unit/commands/plugin-uninstall.test.js
@@ -0,0 +1,66 @@
'use strict';

const chai = require('chai');
const sinon = require('sinon');
const yaml = require('js-yaml');
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('test/unit/commands/plugin-uninstall.test.js', async () => {
let spawnFake;
let serviceDir;
let configurationFilePath;

const pluginName = 'serverless-plugin-1';

before(async () => {
spawnFake = sinon.fake();
const uninstallPlugin = proxyquire('../../../commands/plugin-uninstall', {
'child-process-ext/spawn': spawnFake,
});

const fixture = await fixturesEngine.setup('function', {
configExt: {
plugins: [pluginName],
},
});

const configuration = fixture.serviceConfig;
serviceDir = fixture.servicePath;
configurationFilePath = await resolveConfigurationPath({
cwd: serviceDir,
});
const configurationFilename = configurationFilePath.slice(serviceDir.length + 1);
const options = {
name: pluginName,
};

await uninstallPlugin({
configuration,
serviceDir,
configurationFilename,
options,
});
});

it('should uninstall plugin', () => {
const firstCall = spawnFake.firstCall;
const command = [firstCall.args[0], ...firstCall.args[1]].join(' ');
const expectedCommand = `${npmCommand} uninstall --save-dev ${pluginName}`;
expect(command).to.have.string(expectedCommand);
});

it('should remove plugin from serverless file', async () => {
const serverlessFileObj = yaml.load(await fse.readFile(configurationFilePath, 'utf8'), {
filename: configurationFilePath,
});
expect(serverlessFileObj.plugins).to.be.undefined;
});
});

0 comments on commit 26ce1c6

Please sign in to comment.