Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(Plugins): Seclude
plugin uninstall
standalone command
(PR #10015)
- Loading branch information
Showing
5 changed files
with
182 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
`); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
// TODO: Remove in v3 | ||
|
||
'use strict'; | ||
|
||
const BbPromise = require('bluebird'); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}); | ||
}); |