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 install
standalone command (#9942)
- Loading branch information
Showing
6 changed files
with
259 additions
and
15 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
'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 ServerlessError = require('../lib/serverless-error'); | ||
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 pluginVersion = pluginInfo.version || 'latest'; | ||
const configurationFilePath = getServerlessFilePath({ serviceDir, configurationFilename }); | ||
|
||
const context = { configuration, serviceDir, configurationFilePath, pluginName, pluginVersion }; | ||
await installPlugin(context); | ||
await addPluginToServerlessFile(context); | ||
|
||
const message = ['Successfully installed', ` "${pluginName}@${pluginVersion}"`].join(''); | ||
log(message); | ||
}; | ||
|
||
const installPlugin = async ({ serviceDir, pluginName, pluginVersion }) => { | ||
const pluginFullName = `${pluginName}@${pluginVersion}`; | ||
const message = [ | ||
`Installing plugin "${pluginFullName}"`, | ||
' (this might take a few seconds...)', | ||
].join(''); | ||
log(message); | ||
await npmInstall(pluginFullName, { serviceDir }); | ||
}; | ||
|
||
const addPluginToServerlessFile = async ({ configurationFilePath, pluginName }) => { | ||
const fileExtension = path.extname(configurationFilePath); | ||
if (fileExtension === '.js' || fileExtension === '.ts') { | ||
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(configurationFilePath.split('.')) === 'json') { | ||
const serverlessFileObj = await fse.readJson(configurationFilePath); | ||
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(pluginName); | ||
plugins = _.sortedUniq(plugins); | ||
|
||
if (isArrayPluginsObject) { | ||
newServerlessFileObj.plugins = plugins; | ||
} else { | ||
newServerlessFileObj.plugins.modules = plugins; | ||
} | ||
|
||
await fse.writeJson(configurationFilePath, newServerlessFileObj); | ||
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.addNewArrayItem( | ||
configurationFilePath, | ||
checkIsArrayPluginsObject(serverlessFileObj.plugins) ? 'plugins' : 'plugins.modules', | ||
pluginName | ||
); | ||
}; | ||
|
||
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 = (configurationFilePath) => | ||
log(` | ||
Can't automatically add plugin into "${path.basename(configurationFilePath)}" file. | ||
Please make 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 |
---|---|---|
@@ -0,0 +1,37 @@ | ||
'use strict'; | ||
|
||
const path = require('path'); | ||
const ServerlessError = require('../../lib/serverless-error'); | ||
|
||
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' | ||
); | ||
}, | ||
|
||
getPluginInfo(name_) { | ||
let name; | ||
let version; | ||
if (name_.startsWith('@')) { | ||
[, name, version] = name_.split('@', 3); | ||
name = `@${name}`; | ||
} else { | ||
[name, version] = name_.split('@', 2); | ||
} | ||
return { name, version }; | ||
}, | ||
}; |
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
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,62 @@ | ||
'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-install.test.js', async () => { | ||
let spawnFake; | ||
let serviceDir; | ||
let configurationFilePath; | ||
|
||
const pluginName = 'serverless-plugin-1'; | ||
|
||
before(async () => { | ||
spawnFake = sinon.fake(); | ||
const installPlugin = proxyquire('../../../commands/plugin-install', { | ||
'child-process-ext/spawn': spawnFake, | ||
}); | ||
|
||
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 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); | ||
}); | ||
|
||
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); | ||
}); | ||
}); |