UNPKG

@connected-home/serverless

Version:

The Serverless Application Framework - Powered By Amazon Web Services - http://www.serverless.com

492 lines (383 loc) 14 kB
'use strict'; require('shelljs/global'); const path = require('path'), _ = require('lodash'), SCli = require('./utils/cli'), SError = require('./Error'), BbPromise = require('bluebird'), dotenv = require('dotenv'); // Global Bluebird Config BbPromise.onPossiblyUnhandledRejection(function(error) { throw error; }); BbPromise.longStackTraces(); class Serverless { constructor(config) { // Add version this._version = require('./../package.json').version; this._pipeline = null; // Set Default Config this.config = { interactive: false, serverlessPath: __dirname }; this.classes = {}; this.classes.Plugin = require('./Plugin')(this); this.classes.Serializer = require('./Serializer')(this); this.classes.ProviderAws = require('./ProviderAws')(this); this.classes.Project = require('./Project')(this); this.classes.Function = require('./Function')(this); this.classes.Endpoint = require('./Endpoint')(this); this.classes.Event = require('./Event')(this); this.classes.Stage = require('./Stage')(this); this.classes.Region = require('./Region')(this); this.classes.Templates = require('./Templates')(this); this.classes.Variables = require('./Variables')(this); this.classes.Resources = require('./Resources')(this); this.classes.Runtime = require('./Runtime')(this); this.classes.RuntimeNode = require('./RuntimeNode')(this); this.classes.RuntimeNode43 = require('./RuntimeNode43')(this); this.classes.RuntimeNode610 = require('./RuntimeNode610')(this); this.classes.RuntimeNode810 = require('./RuntimeNode810')(this); this.classes.RuntimeNode10x = require('./RuntimeNode10x')(this); this.classes.RuntimeNode12x = require('./RuntimeNode12x')(this); this.classes.RuntimeNode14x = require('./RuntimeNode14x')(this); this.classes.RuntimePython27 = require('./RuntimePython27')(this); // Add Config Settings this.updateConfig(config); // Add Defaults this.providers = {}; this.actions = {}; this.hooks = {}; this.commands = {}; this.cli = null; this.utils = require('./utils/index'); } /** * Init * - Initializes project * - Returns a Promise */ init() { let _this = this; return BbPromise.try(function() { // Load Core & Project Plugins before anything _this._loadPlugins(__dirname, require('./Actions.json').plugins); if (_this.hasProject()) _this.loadProjectPlugins(); }) .then(function() { if (_this.hasProject()) { // Instantiate Project _this._project = new _this.classes.Project(); // Load Project return _this._project.load() .then(function() { // Load Admin ENV information require('dotenv').config({ silent: true, // Don't display dotenv load failures for admin.env if we already have the required environment variables path: path.join(_this.getProject().getRootPath(), 'admin.env') }); }) .then(function() { _this.initProviders(); }); } else { _this.initProviders(); } }); } updateConfig(config) { this.config = _.assign(this.config, config); } getConfig() { return this.config; } getServerlessPath() { let args = _.toArray(arguments); args.unshift(this.config.serverlessPath); return path.join.apply(this.config.serverlessPath, args); } /** * Project */ hasProject() { return this.config.projectPath != undefined; } getProject() { return this._project; } setProject( project ) { this._project = project; } /** * Providers */ initProviders() { this.providers.aws = new this.classes.ProviderAws(this.config); } getProvider() { return this.providers.aws; } hasProvider(name) { return this.providers[name.toLowerCase()] != undefined; } /** * Execute */ _execute(actionQueue, evt, config) { let _this = this; // If no queue, create one if (!_this._pipeline) { _this._pipeline = BbPromise.try(function() { if (_this.cli) { // If CLI... // Set up evt.options evt = { options: _.assign(_this.cli.options, _this.cli.params) }; } else { // If Programmatic... // If no options object, auto-set options if (typeof evt.options === 'undefined' && Object.keys(evt).length) evt = { options: evt }; } }) .then(function() { return actionQueue.reduce(function (previous, current) { return previous.then(current); }, BbPromise.resolve(_this.middleware(evt, config))); }) .catch(SError, function(e) { _this._reset(); throw e; process.exit(e.messageId); }) .error(function(e) { console.error(e); _this._reset(); process.exit(1); }) .finally(function() { _this._reset(); }); return _this._pipeline; } else { // Otherwise, return promises in existing queue return actionQueue.reduce(function (previous, current) { return previous.then(current); }, BbPromise.resolve(_this.middleware(evt, config))); } } /** * Middleware */ middleware(evt, config) { // Always have properties if (!evt.options) evt.options = {}; if (!evt.data) evt.data = {}; return evt; } /** * Reset */ _reset() { this._pipeline = null; } /** * Load Project Plugins */ loadProjectPlugins() { // Get s-project.json let projectJson = require(path.join(this.config.projectPath, 's-project.json')); this._loadPlugins(this.config.projectPath, projectJson.plugins || []); } /** * Load Plugins * - @param relDir string path to start from when rel paths are specified * - @param pluginMetadata [{path:'path (re or loadable npm mod',config{}}] */ _loadPlugins(relDir, pluginsArray) { let _this = this; // Loads plugin and throws error if not found let loadPlugin = function(pluginPath) { let p; try { p = require(pluginPath); } catch(e) { throw new SError(`Error loading this plugin: ${pluginPath} ${e.message}`); } return p }; // Load plugins in provided array for (let pluginMetadatum of pluginsArray) { // Find Plugin let PluginClass; if (pluginMetadatum.indexOf('.') > -1 ) { // Load non-npm plugin from the private plugins folder let pluginAbsPath = path.join(relDir, pluginMetadatum); _this.utils.sDebug('Attempting to load plugin from ' + pluginAbsPath); PluginClass = loadPlugin(pluginAbsPath); PluginClass = PluginClass(_this); } else { // Load plugin from either plugins or node_modules folder if (_this.utils.dirExistsSync(path.join(relDir, 'node_modules', pluginMetadatum))) { let pluginPath = path.join(relDir, 'node_modules', pluginMetadatum); _this.utils.sDebug('Attempting to load plugin from ' + pluginPath); PluginClass = loadPlugin(pluginPath); PluginClass = PluginClass(_this); } else if (_this.utils.dirExistsSync(path.join(relDir, 'plugins', pluginMetadatum))) { let pluginPath = path.join(relDir, 'plugins', pluginMetadatum); _this.utils.sDebug('Attempting to load plugin from ' + pluginPath); PluginClass = loadPlugin(pluginPath); PluginClass = PluginClass(_this); } else { throw new SError(`This plugin could not be found: ${pluginMetadatum}`); } } // Load Plugin if (PluginClass) { _this.utils.sDebug(PluginClass.getName() + ' plugin loaded'); this.addPlugin(new PluginClass()); } } } /** * Command */ command(argv) { let _this = this; // Set CLI _this.cli = { context: null, action: null, options: {}, params: {}, raw: argv }; // If debug option, set to debug mode if (_this.cli.raw && _this.cli.raw.d) process.env.DEBUG = true; _this.utils.sDebug('CLI raw input: ', _this.cli.raw); // If version command, return version if (_this.cli.raw._[0] === 'version' || _this.cli.raw._[0] === 'v' | argv.v===true || argv.version===true) { console.log(_this._version); return BbPromise.resolve(); } // Get Context & Action _this.cli.context = _this.cli.raw._[0]; _this.cli.action = _this.cli.raw._[1]; // Show Help - if no context action, "help" or "h" is specified as params or options if (_this.cli.raw._.length === 0 || _this.cli.raw._[0] === 'help' || _this.cli.raw._[0] === 'h' || _this.cli.raw.help || _this.cli.raw.h) { if (!_this.commands[_this.cli.context]) { return SCli.generateMainHelp(_this.commands); } else if (_this.commands[_this.cli.context] && !_this.commands[_this.cli.context][_this.cli.action]) { return SCli.generateContextHelp(_this.cli.context, _this.commands); } else if (_this.commands[_this.cli.context] && _this.commands[_this.cli.context][_this.cli.action]) { return SCli.generateActionHelp(_this.commands[_this.cli.context][_this.cli.action]); } } // If command not found, throw error if (!_this.commands[_this.cli.context]) { return BbPromise.reject(new SError('Command not found. Enter "serverless help" to see all available commands.')); } if (!_this.commands[_this.cli.context][_this.cli.action]) { return BbPromise.reject(new SError('In the command you just typed, the "' + _this.cli.context + '" is valid but "' + _this.cli.action + '" is not. Enter "serverless help" to see the actions for this context.')); } // if not in project root and not creating project, throw error if (!this.hasProject() && _this.cli.context != 'project') { return BbPromise.reject(new SError('This command can only be run inside a Serverless project.')); } // Get Command Config let cmdConfig = _this.commands[_this.cli.context][_this.cli.action]; // Options - parse using command config cmdConfig.options.map(opt => { _this.cli.options[opt.option] = (_this.cli.raw[opt.option] ? _this.cli.raw[opt.option] : (_this.cli.raw[opt.shortcut] || null)); }); // Params - remove context and contextAction strings from params array let params = _this.cli.raw._.filter(v => { return ([cmdConfig.context, cmdConfig.contextAction].indexOf(v) == -1); }); // Params - parse params using command config if (cmdConfig.parameters) { cmdConfig.parameters.forEach(function(parameter) { if (parameter.position.indexOf('->') == -1) { _this.cli.params[parameter.parameter] = params.splice(parameter.position, parameter.position + 1); _this.cli.params[parameter.parameter] = _this.cli.params[parameter.parameter][0]; } else { _this.cli.params[parameter.parameter] = params.splice(parameter.position.split('->')[0], (parameter.position.split('->')[1] ? parameter.position.split('->')[1] : params.length)); } }); } _this.utils.sDebug('CLI processed input: ', _this.cli); _this.actions[cmdConfig.handler].apply(_this, {}); } /** * Add action * @param action must return an ES6 BbPromise that is resolved or rejected * @param config */ addAction(action, config) { let _this = this; // Add Hooks Array this.hooks[config.handler + 'Pre'] = []; this.hooks[config.handler + 'Post'] = []; // Handle optional configuration config.options = config.options || []; config.parameters = config.parameters || []; // Add Action this.actions[config.handler] = function(evt) { // Add pre hooks, action, then post hooks to queued let queue = _this.hooks[config.handler + 'Pre']; // Prevent duplicate actions from being added if (queue.indexOf(action) === -1) queue.push(action); // Use _execute() return _this._execute(queue.concat(_this.hooks[config.handler + 'Post']), evt, config); }; // Add command if (config.context && config.contextAction) { if (!this.commands[config.context]) { this.commands[config.context] = {}; } this.commands[config.context][config.contextAction] = config; } } /** * Add Hook */ addHook(hook, config) { let name = config.action + (config.event.charAt(0).toUpperCase() + config.event.slice(1)); this.hooks[name].push(hook); } /** * Add Plugin */ addPlugin(ServerlessPlugin) { return BbPromise.all([ ServerlessPlugin.registerActions(), ServerlessPlugin.registerHooks() ]); } /** * Get Runtime */ getRuntime(runtimeName) { let _this = this; for (let key in _this.classes) { if (key.indexOf('Runtime') !== -1 && _this.classes[key].getName() == runtimeName) { return new _this.classes[key]; } } } /** * Get All Runtimes */ getAllRuntimes() { return _.filter(this.classes, (cls, name) => name.startsWith('Runtime') && cls.getName()) .map(cls => cls.getName()); } } module.exports = Serverless;