'use strict'; /** * Action: FunctionCreate * - takes existing component name and new function name * - validates that component exists * - validates that function does NOT already exists in component * - generates function structure based on runtime * * Event Options: * - sPath: (String) The relative path of the function from project root * - name: (String) Name of the new function for your existing component */ module.exports = function(SPlugin, serverlessPath) { const path = require('path'), SError = require(path.join(serverlessPath, 'ServerlessError')), SCli = require(path.join(serverlessPath, 'utils/cli')), BbPromise = require('bluebird'), SUtils = require(path.join(serverlessPath, 'utils')); let fs = require('fs'); BbPromise.promisifyAll(fs); /** * FunctionCreate Class */ class FunctionCreate extends SPlugin { constructor(S, config) { super(S, config); } static getName() { return 'serverless.core.' + FunctionCreate.name; } registerActions() { this.S.addAction(this.functionCreate.bind(this), { handler: 'functionCreate', description: `Creates scaffolding for a new function. usage: serverless function create `, context: 'function', contextAction: 'create', options: [ { option: 'name', shortcut: 'n', description: 'The name of your new function' } ], parameters: [ { parameter: 'sPath', description: 'One path to your function relative to the project root', position: '0' } ] }); return BbPromise.resolve(); } /** * Action */ functionCreate(evt) { let _this = this; _this.evt = evt; return _this._prompt() .bind(_this) .then(_this._validateAndPrepare) .then(_this._createFunctionSkeleton) .then(function() { SCli.log('Successfully created function: "' + _this.evt.options.name + '"'); /** * Return Event */ return _this.evt; }); } /** * Prompt sPath & function if they're missing */ _prompt() { let _this = this, overrides = {}; if (!_this.S.config.interactive) return BbPromise.resolve(); ['name'].forEach(memberVarKey => { overrides[memberVarKey] = _this.evt.options[memberVarKey]; }); let prompts = { properties: { name: { description: 'Enter a new function name: '.yellow, message: 'Function name must contain only letters, numbers, hyphens, or underscores.', required: true, conform: function(functionName) { return SUtils.isFunctionNameValid(functionName); } } } }; return _this.cliPromptInput(prompts, overrides) .then(function(answers) { _this.evt.options.name = answers.name; }); }; /** * Validate and prepare data before creating module */ _validateAndPrepare() { let _this = this; // Validate: check name if (!_this.evt.options.name) { return BbPromise.reject(new SError('name is required.')); } // Validate: If interactive and no sPath, check they are in a component, and get sPath if (_this.S.config.interactive && !_this.evt.options.sPath) { let cwdArray = process.cwd().split(path.sep); if (SUtils.fileExistsSync(path.join(process.cwd(), 's-component.json'))) { _this.evt.options.sPath = cwdArray.splice(cwdArray.length - 1, 1).join('/') } else if (SUtils.fileExistsSync(path.join(process.cwd(), '..', 's-component.json'))) { _this.evt.options.sPath = cwdArray.splice(cwdArray.length - 2, 2).join('/') } else if (SUtils.fileExistsSync(path.join(process.cwd(), '..', '..', 's-component.json'))) { _this.evt.options.sPath = cwdArray.splice(cwdArray.length - 3, 3).join('/') } else { return BbPromise.reject(new SError('You must be in a component or two subfolders max in a component to create a function')); } } // Validate: check sPath if (!_this.evt.options.sPath) { return BbPromise.reject(new SError('sPath is required.')); } // Validate: Don't allow function creation within a function if (SUtils.fileExistsSync(path.join(_this.S.config.projectPath, _this.evt.options.sPath.split('/').join(path.sep), 's-function.json'))) { return BbPromise.reject(new SError('You cannot create a function in another function')); } // Validate: check sPath isn't too long if (_this.evt.options.sPath.split('/').length > 3) { return BbPromise.reject(new SError('You can only create functions 2 subfolders deep in a component: component/subfolder/subfolder')); } // If component does not exist in project, throw error if (!_this.S.state.getComponents({ paths: [_this.evt.options.sPath] }).length) { return BbPromise.reject(new SError( 'Component does NOT exist in ' + _this.evt.options.sPath, SError.errorCodes.INVALID_PROJECT_SERVERLESS )); } // If subfolders are missing, create them if (_this.evt.options.sPath.split('/').length > 1) { let dir = _this.evt.options.sPath.split('/'); let c = dir.shift(); if (dir[0] && !SUtils.dirExistsSync(path.join(_this.S.config.projectPath, c, dir[0]))) { fs.mkdirSync(path.join(_this.S.config.projectPath, c, dir[0])); } if (dir[1] && !SUtils.dirExistsSync(path.join(_this.S.config.projectPath, c, dir[0], dir[1]))) { fs.mkdirSync(path.join(_this.S.config.projectPath, c, dir[0], dir[1])); } } // If function already exists in component, throw error if (_this.S.state.getFunctions({ paths: [_this.evt.options.sPath + '/' + _this.evt.options.name] }).length) { return BbPromise.reject(new SError( 'Function ' + _this.evt.options.name + ' already exists in ' + _this.evt.options.sPath, SError.errorCodes.INVALID_PROJECT_SERVERLESS )); } return BbPromise.resolve(); }; /** * Create Function Skeleton */ _createFunctionSkeleton() { let func = new this.S.classes.Function(this.S, { sPath: this.evt.options.sPath + '/' + this.evt.options.name }); func.name = this.evt.options.name; this.S.state.setAsset(func); this.evt.data.sPath = func._config.sPath; return func.save(); }; } return( FunctionCreate ); };