'use strict'; /** * Serverless Project Class */ const SError = require('./ServerlessError'), SUtils = require('./utils/index'), SCli = require('./utils/cli'), BbPromise = require('bluebird'), path = require('path'), _ = require('lodash'), fs = require('fs'), os = require('os'); class ServerlessProject { /** * Constructor */ constructor(rootPath) { let _this = this; _this.rootPath = rootPath; // Default properties _this.name = 'serverless' + SUtils.generateShortId(6); _this.version = '0.0.1'; _this.profile = 'serverless-v' + require('../package.json').version; _this.location = 'https://github.com/...'; _this.author = ''; _this.description = 'A Slick New Serverless Project'; _this.custom = {}; _this.components = {}; _this.templates = {}; _this.plugins = []; } //TODO: get rid of this? setServerless( Serverless ) { this._S = Serverless; } static findProject( startDir ){ let jsonName = 's-project.json'; // Check up to 10 parent levels let previous = '.', project = undefined, i = 10; while( i >= 0 ) { let fullPath = path.resolve(startDir, previous); if (SUtils.fileExistsSync(path.join(fullPath, jsonName))) { let projectJson = require(path.join(fullPath, jsonName)); if (typeof projectJson.name !== 'undefined') { project = new ServerlessProject( fullPath ); project.load(); break; } } previous = path.join(previous, '..'); i--; } return project; } getPlugins(){ return this.plugins; } getRootPath(){ return this.rootPath; } getFilePath(){ let args = _.toArray( arguments ); args.unshift( this.getRootPath() ); return path.join.apply( path, args ); } getName(){ return this.name; } /** * Load * - Load from source (i.e., file system) * - Return promise */ load() { let _this = this, projectJson, projectContents; return BbPromise.try(function () { // Validate: Check project exists if (!SUtils.fileExistsSync(_this.getFilePath('s-project.json'))) { throw new SError('Project could not be loaded because it does not exist'); } projectJson = SUtils.readAndParseJsonSync(_this.getFilePath('s-project.json')); projectJson.components = {}; projectJson.templates = {}; projectContents = fs.readdirSync(_this.getRootPath()); return projectContents; }) .each(function (c, i) { // If template, load template if (c.indexOf('s-template') !== -1) { projectJson.templates = _.assign(projectJson.templates, SUtils.readAndParseJsonSync(_this.getFilePath(c))); return; } // If component, load component if (SUtils.fileExistsSync(_this.getFilePath(c, 's-component.json'))) { let component = new _this._S.classes.Component(_this._S, { sPath: c }); return component.load() .then(function (instance) { projectJson.components[c] = instance; }); } }) .then(function () { // Merge _.assign(_this, projectJson); return _this; }); } /** * Set * - Set data * - Accepts a data object */ set(data) { let _this = this; // Instantiate Components for (let prop in data.components) { if (data.components[prop] instanceof _this._S.classes.Component) { throw new SError('You cannot pass subclasses into the set method, only object literals'); } let instance = new _this._S.classes.Component(_this._S, { sPath: data.components[prop].name }); data.components[prop] = instance.set(data.components[prop]); } // Merge in _this = _.extend(_this, data); return _this; } /** * Get * - Returns clone of data */ get() { let clone = _.cloneDeep(this); for (let prop in this.components) { clone.components[prop] = this.components[prop].get(); } return SUtils.exportClassData(clone); } /** * getPopulated * - Fill in templates then variables * - Returns Promise */ getPopulated(options) { let _this = this; options = options || {}; // Validate: Check Stage & Region if (!options.stage || !options.region) throw new SError('Both "stage" and "region" params are required'); // Populate components let clone = _this.get(); clone = SUtils.populate(_this._S.state.getMeta(), _this.getTemplates(), clone, options.stage, options.region); clone.components = {}; for (let prop in _this.components) { clone.components[prop] = _this.components[prop].getPopulated(options); } return clone; } /** * Get Templates * - Returns clone of templates */ getTemplates() { return _.cloneDeep(this.templates ? this.templates : {}); } /** * Get Resources * - Returns Promise & clone of resources */ getResources(options) { let _this = this, resources; options = options || {}; // Check for s-resources-cf.json if (SUtils.fileExistsSync(_this.getFilePath('s-resources-cf.json'))) { resources = SUtils.readAndParseJsonSync(_this.getFilePath('s-resources-cf.json')); if (options.populate) { return BbPromise.resolve(SUtils.populate(_this._S.state.getMeta(), _this.getTemplates(), resources, options.stage, options.region)); } else { return BbPromise.resolve(resources); } } // Backward compat support for this.cloudFormation and s-module.json // TODO: Remove @ V1 when we can make breaking changes let cPaths = [], cfSnippets = []; return BbPromise.try(function() { resources = _this.get().cloudFormation; for (let c in _this.components) { cPaths.push(_this.components[c]._config.fullPath); } return cPaths; }) .each(function (cPath) { let cContents = fs.readdirSync(cPath); return BbPromise.resolve(cContents) .each(function (sf) { if (SUtils.fileExistsSync(path.join(cPath, sf, 's-module.json'))) { let moduleJson = SUtils.readAndParseJsonSync(path.join(cPath, sf, 's-module.json')); if (moduleJson.cloudFormation) cfSnippets.push(moduleJson.cloudFormation); } }); }) .then(function () { // Merge s-module.json CF syntax for (let i = 0; i < cfSnippets.length; i++) { let cf = cfSnippets[i]; // Merge Lambda Policy Statements if (cf.lambdaIamPolicyDocumentStatements && cf.lambdaIamPolicyDocumentStatements.length > 0) { cf.lambdaIamPolicyDocumentStatements.forEach(function (policyStmt) { try { resources.Resources.IamPolicyLambda.Properties.PolicyDocument.Statement.push(policyStmt); } catch (e) { } }); } // Merge Resources if (cf.resources) { let cfResourceKeys = Object.keys(cf.resources); cfResourceKeys.forEach(function (resourceKey) { if (resources.Resources[resourceKey]) { SCli.log(`WARN: Resource key ${resourceKey} already defined in CF template. Overwriting...`); } try { resources.Resources[resourceKey] = cf.resources[resourceKey]; } catch (e) { } }); } } // Return if (options.populate) { return SUtils.populate(_this._S.state.getMeta(), _this.getTemplates(), resources, options.stage, options.region); } else { return resources; } }); } /** * save * - Saves data to file system */ save(options) { let _this = this, files = []; return new BbPromise.try(function () { // If project folder does not exist, create it if (!SUtils.dirExistsSync(_this.getRootPath())) { fs.mkdirSync(_this.getRootPath()); } // Save all nested components if (options && options.deep) { return BbPromise.try(function () { return Object.keys(_this.components); }) .each(function (c) { return _this.components[c].save(); }) } }) .then(function () { let clone = _this.get(); // Strip properties if (clone.components) delete clone.components; if (clone.templates) delete clone.templates; // Save s-project.json files.push(SUtils.writeFile(_this.getFilePath('s-project.json'), JSON.stringify(clone, null, 2))); // Write files return BbPromise.all(files); }) .then(function () { return _this; }) } } module.exports = ServerlessProject;