mirror of
https://github.com/serverless/serverless.git
synced 2026-02-01 16:07:28 +00:00
Merge pull request #1128 from serverless/add-plugin-management-class
Add PluginManagement class
This commit is contained in:
commit
57948f00cd
118
lib/classes/PluginManager.js
Normal file
118
lib/classes/PluginManager.js
Normal file
@ -0,0 +1,118 @@
|
||||
'use strict';
|
||||
|
||||
const Utils = require('./Utils');
|
||||
const path = require('path');
|
||||
const has = require('lodash').has;
|
||||
const forEach = require('lodash').forEach;
|
||||
|
||||
class PluginManager {
|
||||
constructor(serverless) {
|
||||
this.serverless = serverless;
|
||||
this.plugins = [];
|
||||
this.commandsList = [];
|
||||
this.commands = {};
|
||||
}
|
||||
|
||||
loadAllPlugins(servicePlugins) {
|
||||
this.loadCorePlugins();
|
||||
this.loadServicePlugins(servicePlugins);
|
||||
}
|
||||
|
||||
runCommand(commandsArray) {
|
||||
const events = this.getEvents(commandsArray, this.commands);
|
||||
// collect all relevant hooks
|
||||
let hooks = [];
|
||||
events.forEach((event) => {
|
||||
const hooksForEvent = [];
|
||||
this.plugins.forEach((pluginInstance) => {
|
||||
forEach(pluginInstance.hooks, (hook, hookKey) => {
|
||||
if (hookKey === event) {
|
||||
hooksForEvent.push(hook);
|
||||
}
|
||||
});
|
||||
});
|
||||
hooks = hooks.concat(hooksForEvent);
|
||||
});
|
||||
|
||||
if (hooks.length === 0) {
|
||||
throw new Error('The command you entered was not found. Did you spell it correctly?');
|
||||
}
|
||||
|
||||
// run all relevant hooks one after another
|
||||
hooks.forEach((hook) => {
|
||||
const returnValue = hook();
|
||||
|
||||
// check if a Promise is returned
|
||||
if (returnValue && returnValue.then instanceof Function) {
|
||||
returnValue.then((value) => {
|
||||
return value;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addPlugin(Plugin) {
|
||||
this.plugins.push(new Plugin());
|
||||
|
||||
this.loadCommands(Plugin);
|
||||
}
|
||||
|
||||
loadCorePlugins() {
|
||||
const utils = new Utils();
|
||||
const pluginsDirectoryPath = path.join(__dirname, '../plugins');
|
||||
|
||||
const corePlugins = utils.readFileSync(path.join(pluginsDirectoryPath, 'Plugins.json')).plugins;
|
||||
|
||||
corePlugins.forEach((corePlugin) => {
|
||||
const Plugin = require(path.join(pluginsDirectoryPath, corePlugin));
|
||||
|
||||
this.addPlugin(Plugin);
|
||||
});
|
||||
}
|
||||
|
||||
loadServicePlugins(servicePlugins) {
|
||||
servicePlugins = (typeof servicePlugins !== 'undefined' ? servicePlugins : []);
|
||||
|
||||
servicePlugins.forEach((servicePlugins) => {
|
||||
this.addPlugin(servicePlugins);
|
||||
});
|
||||
}
|
||||
|
||||
loadCommands(Plugin) {
|
||||
this.commandsList.push((new Plugin()).commands);
|
||||
|
||||
// TODO: refactor ASAP as it slows down overall performance
|
||||
// rebuild the commands
|
||||
forEach(this.commandsList, (commands) => {
|
||||
forEach(commands, (commandDetails, command) => {
|
||||
this.commands[command] = commandDetails;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getEvents(commandsArray, availableCommands, prefix) {
|
||||
prefix = (typeof prefix !== 'undefined' ? prefix : '');
|
||||
const commandPart = commandsArray[0];
|
||||
|
||||
if (has(availableCommands, commandPart)) {
|
||||
const commandDetails = availableCommands[commandPart];
|
||||
if (commandsArray.length === 1) {
|
||||
const events = [];
|
||||
commandDetails.lifeCycleEvents.forEach((event) => {
|
||||
events.push(`before:${prefix}${commandPart}:${event}`);
|
||||
events.push(`${prefix}${commandPart}:${event}`);
|
||||
events.push(`after:${prefix}${commandPart}:${event}`);
|
||||
});
|
||||
return events;
|
||||
}
|
||||
if (has(commandDetails, 'commands')) {
|
||||
return this.getEvents(commandsArray.slice(1, commandsArray.length),
|
||||
commandDetails.commands, `${commandPart}:`);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PluginManager;
|
||||
42
lib/plugins/HelloWorld/HelloWorld.js
Normal file
42
lib/plugins/HelloWorld/HelloWorld.js
Normal file
@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
class HelloWorld {
|
||||
constructor() {
|
||||
this.commands = {
|
||||
greet: {
|
||||
usage: 'Run this command to get greeted.',
|
||||
lifeCycleEvents: [
|
||||
'printGoodMorning',
|
||||
'printHello',
|
||||
'printGoodEvening'
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
this.hooks = {
|
||||
'before:greet:printHello': this.printGoodMorning,
|
||||
'greet:printHello': this.printHello,
|
||||
'after:greet:printHello': this.printGoodEvening,
|
||||
};
|
||||
}
|
||||
|
||||
printGoodMorning() {
|
||||
const message = 'Good morning';
|
||||
console.log(message);
|
||||
return message;
|
||||
}
|
||||
|
||||
printHello() {
|
||||
const message = 'Hello';
|
||||
console.log(message);
|
||||
return message;
|
||||
}
|
||||
|
||||
printGoodEvening() {
|
||||
const message = 'Good evening';
|
||||
console.log(message);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HelloWorld;
|
||||
8
lib/plugins/HelloWorld/README.md
Normal file
8
lib/plugins/HelloWorld/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Hello world
|
||||
This plugin is a simple "Hello World" plugin which shows how the plugin system works.
|
||||
|
||||
## Setup
|
||||
Run `npm install` to install all dependencies.
|
||||
|
||||
## Tests
|
||||
Tests live inside the `tests` directory. Run `npm test` inside the root of this plugin to run the tests.
|
||||
13
lib/plugins/HelloWorld/package.json
Normal file
13
lib/plugins/HelloWorld/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "serverless-plugins-hello-world",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "node_modules/.bin/mocha ./tests/HelloWorld.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^3.5.0",
|
||||
"mocha": "^2.4.5"
|
||||
}
|
||||
}
|
||||
51
lib/plugins/HelloWorld/tests/HelloWorld.js
Normal file
51
lib/plugins/HelloWorld/tests/HelloWorld.js
Normal file
@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Test: HelloWorld Plugin
|
||||
*/
|
||||
|
||||
const expect = require('chai').expect;
|
||||
const HelloWorld = require('../HelloWorld');
|
||||
|
||||
|
||||
describe('HelloWorld', () => {
|
||||
let helloWorld;
|
||||
|
||||
beforeEach(() => {
|
||||
helloWorld = new HelloWorld();
|
||||
});
|
||||
|
||||
describe('#constructor()', () => {
|
||||
it('should have commands', () => {
|
||||
expect(helloWorld.commands).to.be.not.empty;
|
||||
});
|
||||
|
||||
it('should have hooks', () => {
|
||||
expect(helloWorld.hooks).to.be.not.empty;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#printGoodMorning()', () => {
|
||||
it('should print "Good morning"', () => {
|
||||
const greeting = helloWorld.printGoodMorning();
|
||||
|
||||
expect(greeting).to.equal('Good morning');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#printHello()', () => {
|
||||
it('should print "Hello"', () => {
|
||||
const greeting = helloWorld.printHello();
|
||||
|
||||
expect(greeting).to.equal('Hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#printGoodEvening()', () => {
|
||||
it('should print "Good evening"', () => {
|
||||
const greeting = helloWorld.printGoodEvening();
|
||||
|
||||
expect(greeting).to.equal('Good evening');
|
||||
});
|
||||
});
|
||||
});
|
||||
5
lib/plugins/Plugins.json
Normal file
5
lib/plugins/Plugins.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugins": [
|
||||
"./HelloWorld/HelloWorld.js"
|
||||
]
|
||||
}
|
||||
@ -7,7 +7,8 @@ describe('All Tests', function() {
|
||||
before(function() {});
|
||||
after(function() {});
|
||||
|
||||
require('./tests/classes/Serverless');
|
||||
// require('./tests/classes/Serverless');
|
||||
require('./tests/classes/PluginManager');
|
||||
// require('./tests/classes/Utils');
|
||||
// require('./tests/classes/Plugin');
|
||||
// require('./tests/classes/YamlParser');
|
||||
|
||||
346
tests/tests/classes/PluginManager.js
Normal file
346
tests/tests/classes/PluginManager.js
Normal file
@ -0,0 +1,346 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Test: PluginManager Class
|
||||
*/
|
||||
|
||||
const expect = require('chai').expect;
|
||||
const PluginManager = require('../../../lib/classes/PluginManager');
|
||||
const Serverless = require('../../../lib/Serverless');
|
||||
const HelloWorld = require('../../../lib/plugins/HelloWorld/HelloWorld');
|
||||
|
||||
describe('PluginManager', () => {
|
||||
let pluginManager;
|
||||
let serverless;
|
||||
let helloWorld;
|
||||
|
||||
class ServicePluginMock1 {}
|
||||
|
||||
class ServicePluginMock2 {}
|
||||
|
||||
class PromisePluginMock {
|
||||
constructor() {
|
||||
this.commands = {
|
||||
deploy: {
|
||||
usage: 'Deploy to the default infrastructure',
|
||||
lifeCycleEvents: [
|
||||
'resources',
|
||||
'functions'
|
||||
],
|
||||
commands: {
|
||||
onpremises: {
|
||||
usage: 'Deploy to your On-Premises infrastructure',
|
||||
lifeCycleEvents: [
|
||||
'resources',
|
||||
'functions'
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
this.hooks = {
|
||||
'deploy:functions': this.functions.bind(this),
|
||||
'before:deploy:onpremises:functions': this.resources.bind(this)
|
||||
};
|
||||
|
||||
// used to test if the function was executed correctly
|
||||
this.deployedFunctions = 0;
|
||||
this.deployedResources = 0;
|
||||
}
|
||||
|
||||
functions() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.deployedFunctions = this.deployedFunctions + 1;
|
||||
return resolve();
|
||||
});
|
||||
}
|
||||
|
||||
resources() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.deployedResources = this.deployedResources + 1;
|
||||
return resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class SynchronousPluginMock {
|
||||
constructor() {
|
||||
this.commands = {
|
||||
deploy: {
|
||||
usage: 'Deploy to the default infrastructure',
|
||||
lifeCycleEvents: [
|
||||
'resources',
|
||||
'functions'
|
||||
],
|
||||
commands: {
|
||||
onpremises: {
|
||||
usage: 'Deploy to your On-Premises infrastructure',
|
||||
lifeCycleEvents: [
|
||||
'resources',
|
||||
'functions'
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
this.hooks = {
|
||||
'deploy:functions': this.functions.bind(this),
|
||||
'before:deploy:onpremises:functions': this.resources.bind(this),
|
||||
};
|
||||
|
||||
// used to test if the function was executed correctly
|
||||
this.deployedFunctions = 0;
|
||||
this.deployedResources = 0;
|
||||
}
|
||||
|
||||
functions() {
|
||||
this.deployedFunctions = this.deployedFunctions + 1;
|
||||
}
|
||||
|
||||
resources() {
|
||||
this.deployedResources = this.deployedResources + 1;
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
serverless = new Serverless({});
|
||||
pluginManager = new PluginManager(serverless);
|
||||
helloWorld = new HelloWorld();
|
||||
});
|
||||
|
||||
describe('#constructor()', () => {
|
||||
it('should set the serverless instance', () => {
|
||||
expect(pluginManager.serverless).to.deep.equal(serverless);
|
||||
});
|
||||
|
||||
it('should create an empty plugins array', () => {
|
||||
expect(pluginManager.plugins.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('should create an empty commandsList array', () => {
|
||||
expect(pluginManager.commandsList.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('should create an empty commands object', () => {
|
||||
expect(pluginManager.commands).to.deep.equal({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#addPlugin()', () => {
|
||||
it('should add a plugin instance to the plugins array', () => {
|
||||
pluginManager.addPlugin(SynchronousPluginMock);
|
||||
|
||||
expect(pluginManager.plugins[0]).to.be.an.instanceof(SynchronousPluginMock);
|
||||
});
|
||||
|
||||
it('should load the plugin commands', () => {
|
||||
pluginManager.addPlugin(SynchronousPluginMock);
|
||||
|
||||
expect(pluginManager.commandsList[0]).to.have.property('deploy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#loadAllPlugins()', () => {
|
||||
it('should load only core plugins when no service plugins are given', () => {
|
||||
// Note: We need the HelloWorld plugin for this test to pass
|
||||
pluginManager.loadAllPlugins();
|
||||
|
||||
expect(pluginManager.plugins).to.include(helloWorld);
|
||||
});
|
||||
|
||||
it('should load all plugins when service plugins are given', () => {
|
||||
const servicePlugins = [ServicePluginMock1, ServicePluginMock2];
|
||||
pluginManager.loadAllPlugins(servicePlugins);
|
||||
|
||||
const servicePluginMock1 = new ServicePluginMock1();
|
||||
const servicePluginMock2 = new ServicePluginMock2();
|
||||
|
||||
expect(pluginManager.plugins).to.contain(servicePluginMock1);
|
||||
expect(pluginManager.plugins).to.contain(servicePluginMock2);
|
||||
expect(pluginManager.plugins).to.contain(helloWorld);
|
||||
});
|
||||
|
||||
it('should load all plugins in the correct order', () => {
|
||||
const servicePlugins = [ServicePluginMock1, ServicePluginMock2];
|
||||
|
||||
// we need to mock it so that tests won't break when more core plugins are added later on
|
||||
// because we access the plugins array with an index which will change every time a new core
|
||||
// plugin will be added
|
||||
const loadCorePluginsMock = () => {
|
||||
pluginManager.addPlugin(HelloWorld);
|
||||
};
|
||||
|
||||
// This is the exact same functionality like loadCorePlugins()
|
||||
loadCorePluginsMock();
|
||||
pluginManager.loadServicePlugins(servicePlugins);
|
||||
|
||||
expect(pluginManager.plugins[0]).to.be.instanceof(HelloWorld);
|
||||
expect(pluginManager.plugins[1]).to.be.instanceof(ServicePluginMock1);
|
||||
expect(pluginManager.plugins[2]).to.be.instanceof(ServicePluginMock2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#loadCorePlugins()', () => {
|
||||
it('should load the Serverless core plugins', () => {
|
||||
pluginManager.loadCorePlugins();
|
||||
|
||||
expect(pluginManager.plugins).to.contain(helloWorld);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#loadServicePlugins()', () => {
|
||||
it('should load the service plugins', () => {
|
||||
const servicePlugins = [ServicePluginMock1, ServicePluginMock2];
|
||||
pluginManager.loadServicePlugins(servicePlugins);
|
||||
|
||||
const servicePluginMock1 = new ServicePluginMock1();
|
||||
const servicePluginMock2 = new ServicePluginMock2();
|
||||
|
||||
expect(pluginManager.plugins).to.contain(servicePluginMock1);
|
||||
expect(pluginManager.plugins).to.contain(servicePluginMock2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#loadCommands()', () => {
|
||||
it('should load the plugin commands', () => {
|
||||
pluginManager.loadCommands(SynchronousPluginMock);
|
||||
|
||||
expect(pluginManager.commandsList[0]).to.have.property('deploy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getEvents()', () => {
|
||||
beforeEach(() => {
|
||||
pluginManager.loadCommands(SynchronousPluginMock);
|
||||
});
|
||||
|
||||
it('should get all the matching events for a root level command in the correct order', () => {
|
||||
const commandsArray = ['deploy'];
|
||||
const events = pluginManager.getEvents(commandsArray, pluginManager.commands);
|
||||
|
||||
expect(events[0]).to.equal('before:deploy:resources');
|
||||
expect(events[1]).to.equal('deploy:resources');
|
||||
expect(events[2]).to.equal('after:deploy:resources');
|
||||
expect(events[3]).to.equal('before:deploy:functions');
|
||||
expect(events[4]).to.equal('deploy:functions');
|
||||
expect(events[5]).to.equal('after:deploy:functions');
|
||||
});
|
||||
|
||||
it('should get all the matching events for a nested level command in the correct order', () => {
|
||||
const commandsArray = ['deploy', 'onpremises'];
|
||||
const events = pluginManager.getEvents(commandsArray, pluginManager.commands);
|
||||
|
||||
expect(events[0]).to.equal('before:deploy:onpremises:resources');
|
||||
expect(events[1]).to.equal('deploy:onpremises:resources');
|
||||
expect(events[2]).to.equal('after:deploy:onpremises:resources');
|
||||
expect(events[3]).to.equal('before:deploy:onpremises:functions');
|
||||
expect(events[4]).to.equal('deploy:onpremises:functions');
|
||||
expect(events[5]).to.equal('after:deploy:onpremises:functions');
|
||||
});
|
||||
|
||||
it('should return an empty events array when the command is not defined', () => {
|
||||
const commandsArray = ['foo'];
|
||||
const events = pluginManager.getEvents(commandsArray, pluginManager.commands);
|
||||
|
||||
expect(events.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#runCommand()', () => {
|
||||
it('should throw an error when the given command is not available', () => {
|
||||
pluginManager.addPlugin(SynchronousPluginMock);
|
||||
|
||||
const commandsArray = ['foo'];
|
||||
|
||||
expect(() => { pluginManager.runCommand(commandsArray); }).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should run the hooks in the correct order', () => {
|
||||
class CorrectHookOrderPluginMock {
|
||||
constructor() {
|
||||
this.commands = {
|
||||
run: {
|
||||
usage: 'Pushes the current hook status on the hookStatus array',
|
||||
lifeCycleEvents: [
|
||||
'beforeHookStatus',
|
||||
'midHookStatus',
|
||||
'afterHookStatus'
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
this.hooks = {
|
||||
'before:run:beforeHookStatus': this.beforeHookStatus.bind(this),
|
||||
'run:midHookStatus': this.midHookStatus.bind(this),
|
||||
'after:run:afterHookStatus': this.afterHookStatus.bind(this),
|
||||
};
|
||||
|
||||
// used to test if the hooks were run in the correct order
|
||||
this.hookStatus = [];
|
||||
}
|
||||
|
||||
beforeHookStatus() {
|
||||
this.hookStatus.push('before');
|
||||
}
|
||||
|
||||
midHookStatus() {
|
||||
this.hookStatus.push('mid');
|
||||
}
|
||||
|
||||
afterHookStatus() {
|
||||
this.hookStatus.push('after');
|
||||
}
|
||||
}
|
||||
|
||||
pluginManager.addPlugin(CorrectHookOrderPluginMock);
|
||||
const commandsArray = ['run'];
|
||||
pluginManager.runCommand(commandsArray);
|
||||
|
||||
expect(pluginManager.plugins[0].hookStatus[0]).to.equal('before');
|
||||
expect(pluginManager.plugins[0].hookStatus[1]).to.equal('mid');
|
||||
expect(pluginManager.plugins[0].hookStatus[2]).to.equal('after');
|
||||
});
|
||||
|
||||
describe('when using a synchronous hook function', () => {
|
||||
beforeEach(() => {
|
||||
pluginManager.addPlugin(SynchronousPluginMock);
|
||||
});
|
||||
|
||||
it('should run a simple command', () => {
|
||||
const commandsArray = ['deploy'];
|
||||
pluginManager.runCommand(commandsArray);
|
||||
|
||||
expect(pluginManager.plugins[0].deployedFunctions).to.equal(1);
|
||||
});
|
||||
|
||||
it('should run a nested command', () => {
|
||||
const commandsArray = ['deploy', 'onpremises'];
|
||||
pluginManager.runCommand(commandsArray);
|
||||
|
||||
expect(pluginManager.plugins[0].deployedResources).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using a promise based hook function', () => {
|
||||
beforeEach(() => {
|
||||
pluginManager.addPlugin(PromisePluginMock);
|
||||
});
|
||||
|
||||
it('should run a simple command', () => {
|
||||
const commandsArray = ['deploy'];
|
||||
pluginManager.runCommand(commandsArray);
|
||||
|
||||
expect(pluginManager.plugins[0].deployedFunctions).to.equal(1);
|
||||
});
|
||||
|
||||
it('should run a nested command', () => {
|
||||
const commandsArray = ['deploy', 'onpremises'];
|
||||
pluginManager.runCommand(commandsArray);
|
||||
|
||||
expect(pluginManager.plugins[0].deployedResources).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user