diff --git a/lib/classes/Error.js b/lib/classes/Error.js index bae4edff7..ce8f4bf5a 100644 --- a/lib/classes/Error.js +++ b/lib/classes/Error.js @@ -21,9 +21,26 @@ module.exports.logError = (e) => { line = `${line}-`; } + consoleLog(' '); consoleLog(chalk.yellow(` ${errorType} ${line}`)); consoleLog(' '); - consoleLog(chalk.yellow(` ${e.message}`)); + + // format error message to fit neatly + const words = e.message.split(' '); + let logLine = []; + words.forEach(word => { + logLine.push(word); + const logLineString = logLine.join(' '); + if (logLineString.length > 50) { + consoleLog(chalk.yellow(` ${logLineString}`)); + logLine = []; + } + }); + + if (logLine.length !== 0) { + consoleLog(chalk.yellow(` ${logLine.join(' ')}`)); + } + consoleLog(' '); if (e.name !== 'ServerlessError') { diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js index 1d52ee027..4a9b6a46d 100644 --- a/lib/classes/PluginManager.js +++ b/lib/classes/PluginManager.js @@ -38,7 +38,11 @@ class PluginManager { validateCommands(commandsArray) { // TODO: implement an option to get deeper than one level if (!this.commands[commandsArray[0]]) { - throw new this.serverless.classes.Error(`command "${commandsArray[0]}" not found.`); + const errorMessage = [ + `command "${commandsArray[0]}" not found`, + ' Run "serverless help" for a list of all available commands.', + ].join(); + throw new this.serverless.classes.Error(errorMessage); } } diff --git a/lib/classes/Service.js b/lib/classes/Service.js index 77a436ba7..5fcb0a3b8 100644 --- a/lib/classes/Service.js +++ b/lib/classes/Service.js @@ -54,6 +54,15 @@ class Service { throw new SError('"functions" property is missing in serverless.yaml'); } + if (['aws', 'azure', 'google', 'ibm'].indexOf(serverlessYaml.provider)) { + const errorMessage = [ + `Provider "${serverlessYaml.provider}" is not supported.`, + ' Valid values for provider are: aws, azure, google, ibm.', + ' Please provide one of those values to the "provider" property in serverless.yaml.', + ].join(''); + throw new SError(errorMessage); + } + that.service = serverlessYaml.service; that.provider = serverlessYaml.provider; that.variableSyntax = serverlessYaml.variableSyntax; @@ -167,12 +176,22 @@ class Service { // Populate if (!value && !value !== '') { + const errorMessage = [ + `Variable "${variableName}" doesn't exist in serverless.env.yaml.`, + ' Please add it to serverless.env.yaml.', + ].join(''); throw new that.serverless.classes - .Error(`Variable "${variableName}" doesn't exist in serverless.env.yaml.`); + .Error(errorMessage); } else if (typeof value === 'string') { if (variableString.split('.').length > 1) { + const errorMessage = [ + `Trying to access sub properties of a string variable "${variableName}".`, + ' Please make sure the variable in serverless.env.yaml', + ' is an object, otherwise you cannot use the', + ' dot notation for that variable in serverless.yaml', + ].join(''); throw new that.serverless.classes - .Error('Trying to access sub properties of a string variable'); + .Error(errorMessage); } // for string variables, we use replaceall in case the user // includes the variable as a substring (ie. "hello ${name}") @@ -185,8 +204,14 @@ class Service { subProperties.splice(0, 1); subProperties.forEach(subProperty => { if (!value[subProperty]) { + const errorMessage = [ + `Variable "${variableName}" doesn't have sub property "${subProperty}".`, + ' Please make sure the variable is', + ' the intended object in serverless.env.yaml,', + ' or reference the correct sub property in serverless.yaml', + ].join(''); throw new that.serverless.classes - .Error(`Invalid sub property for variable "${variableName}"`); + .Error(errorMessage); } value = value[subProperty]; }); @@ -195,18 +220,35 @@ class Service { val = replaceall(variableSyntax, value, val); } else { if (val !== variableSyntax) { + const errorMessage = [ + 'Trying to populate non string variables into', + ` a string for variable "${variableName}".`, + ' Please make sure the variable value in', + ' serverless.env.yaml is a string', + ].join(''); throw new that.serverless.classes - .Error('Trying to populate non string variables into a string'); + .Error(errorMessage); } val = value; } } else if (variableString.split('.').length > 1) { + const errorMessage = [ + `Trying to access sub properties of a non-object variable "${variableName}"`, + ' Please make sure the variable is an object in serverless.env.yaml,', + ' otherwise, you cannot use the dot notation', + ' for that variable in serverless.yaml', + ].join(''); throw new that.serverless.classes - .Error('Trying to access sub properties of a non-object variable'); + .Error(errorMessage); } else { if (val !== variableSyntax) { + const errorMessage = [ + 'Trying to populate non string variables', + ` into a string for variable "${variableName}".`, + ' Please make sure the variable value in serverless.env.yaml is a string', + ].join(''); throw new that.serverless.classes - .Error('Trying to populate non string variables into a string'); + .Error(errorMessage); } val = value; // not string nor object } @@ -240,14 +282,14 @@ class Service { if (functionName in this.functions) { return this.functions[functionName]; } - throw new SError(`function ${functionName} doesn't exist in this Service`); + throw new SError(`Function "${functionName}" doesn't exist in this Service`); } getEventInFunction(eventName, functionName) { if (eventName in this.getFunction(functionName).events) { return this.getFunction(functionName).events[eventName]; } - throw new SError(`event ${eventName} doesn't exist in function ${functionName}`); + throw new SError(`Event "${eventName}" doesn't exist in function "${functionName}"`); } getAllEventsInFunction(functionName) { @@ -258,7 +300,7 @@ class Service { if (stageName in this.environment.stages) { return this.environment.stages[stageName]; } - throw new SError(`stage ${stageName} doesn't exist in this Service`); + throw new SError(`Stage "${stageName}" doesn't exist in this service.`); } getAllStages() { @@ -269,7 +311,7 @@ class Service { if (regionName in this.getStage(stageName).regions) { return this.getStage(stageName).regions[regionName]; } - throw new SError(`region ${regionName} doesn't exist in stage ${stageName}`); + throw new SError(`Region "${regionName}" doesn't exist in stage "${stageName}"`); } getAllRegionsInStage(stageName) { diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js index 929e8d430..2a8f39221 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js @@ -18,8 +18,14 @@ module.exports = { method = event.http.split(' ')[0]; path = event.http.split(' ')[1]; } else { + const errorMessage = [ + `HTTP event of function ${functionName} is not an object nor a string.`, + ' The correct syntax is: http: get users/list', + ' OR an object with "path" and "method" proeprties.', + ' Please check the docs for more info.', + ].join(''); throw new this.serverless.classes - .Error(`HTTP event of function ${functionName} is not an object nor a string`); + .Error(errorMessage); } const resourceLogicalId = this.resourceLogicalIds[path]; diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/permissions.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/permissions.js index 3f3fd7cf0..2ec265a44 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/permissions.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/permissions.js @@ -18,8 +18,14 @@ module.exports = { method = event.http.split(' ')[0]; path = event.http.split(' ')[1]; } else { + const errorMessage = [ + `HTTP event of function ${functionName} is not an object nor a string.`, + ' The correct syntax is: http: get users/list', + ' OR an object with "path" and "method" proeprties.', + ' Please check the docs for more info.', + ].join(''); throw new this.serverless.classes - .Error(`HTTP event of function ${functionName} is not an object nor a string`); + .Error(errorMessage); } const normalizedMethod = method[0].toUpperCase() + diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/resources.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/resources.js index 504625ea3..83981c9be 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/resources.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/resources.js @@ -18,8 +18,14 @@ module.exports = { } else if (typeof event.http === 'string') { path = event.http.split(' ')[1]; } else { + const errorMessage = [ + `HTTP event of function ${functionName} is not an object nor a string.`, + ' The correct syntax is: http: get users/list', + ' OR an object with "path" and "method" proeprties.', + ' Please check the docs for more info.', + ].join(''); throw new this.serverless.classes - .Error(`HTTP event of function ${functionName} is not an object nor a string`); + .Error(errorMessage); } while (path !== '') { diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js index 80fd107b8..ec526384b 100644 --- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js +++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/validate.js @@ -22,7 +22,7 @@ module.exports = { this.serverless.service.getRegionInStage(this.options.stage, this.options.region); // validate that path and method exists for each http event in service - forEach(this.serverless.service.functions, (functionObject) => { + forEach(this.serverless.service.functions, (functionObject, functionName) => { functionObject.events.forEach(event => { if (event.http) { let method; @@ -36,14 +36,35 @@ module.exports = { path = event.http.split(' ')[1]; } - // TODO validate the values of these properties as well if (!path) { + const errorMessage = [ + `Missing "path" property in function "${functionName}"`, + ' for http event in serverless.yaml.', + ' If you define an http event, make sure you pass a valid value for it,', + ' either as string syntax, or object syntax.', + ' Please check the docs for more options.', + ].join(''); throw new this.serverless.classes - .Error('Missing "path" property in serverless.yaml for http event.'); + .Error(errorMessage); } if (!method) { + const errorMessage = [ + `Missing "method" property in function "${functionName}"`, + ' for http event in serverless.yaml.', + ' If you define an http event, make sure you pass a valid value for it,', + ' either as string syntax, or object syntax.', + ' Please check the docs for more options.', + ].join(''); throw new this.serverless.classes - .Error('Missing "method" property in serverless.yaml for http event.'); + .Error(errorMessage); + } + + if (['get', 'post', 'put', 'patch', 'options', 'head', 'delete'].indexOf(method) === -1) { + const errorMessage = [ + `Invalid APIG method "${method}" in function "${functionName}".`, + ' AWS supported methods are: get, post, put, patch, options, head, delete.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); } } }); diff --git a/lib/plugins/aws/deploy/compile/events/s3/index.js b/lib/plugins/aws/deploy/compile/events/s3/index.js index 9e0583081..70a630109 100644 --- a/lib/plugins/aws/deploy/compile/events/s3/index.js +++ b/lib/plugins/aws/deploy/compile/events/s3/index.js @@ -31,8 +31,13 @@ class AwsCompileS3Events { if (typeof event.s3 === 'object') { if (!event.s3.bucket) { + const errorMessage = [ + `Missing "bucket" property for s3 event in function ${functionName}.`, + ' The correct syntax is: s3: bucketName OR an object with "bucket" property.', + ' Please check the docs for more info.', + ].join(''); throw new this.serverless.classes - .Error(`Missing "bucket" property in s3 event in function ${functionName}`); + .Error(errorMessage); } BucketName = event.s3.bucket + i; if (event.s3.event) { @@ -41,8 +46,13 @@ class AwsCompileS3Events { } else if (typeof event.s3 === 'string') { BucketName = event.s3 + i; } else { + const errorMessage = [ + `S3 event of function ${functionName} is not an object nor a string.`, + ' The correct syntax is: s3: bucketName OR an object with "bucket" property.', + ' Please check the docs for more info.', + ].join(''); throw new this.serverless.classes - .Error(`S3 event of function ${functionName} is not an object nor a string`); + .Error(errorMessage); } const bucketTemplate = ` { diff --git a/lib/plugins/aws/deploy/compile/events/schedule/index.js b/lib/plugins/aws/deploy/compile/events/schedule/index.js index 41c4afd36..73ce6514a 100644 --- a/lib/plugins/aws/deploy/compile/events/schedule/index.js +++ b/lib/plugins/aws/deploy/compile/events/schedule/index.js @@ -31,8 +31,14 @@ class AwsCompileScheduledEvents { // TODO validate rate syntax if (typeof event.schedule === 'object') { if (!event.schedule.rate) { + const errorMessage = [ + `Missing "rate" property for schedule event in function ${functionName}`, + ' The correct syntax is: schedule: rate(10 minutes)', + ' OR an object with "rate" property.', + ' Please check the docs for more info.', + ].join(''); throw new this.serverless.classes - .Error(`Missing "rate" property in schedule event in function ${functionName}`); + .Error(errorMessage); } ScheduleExpression = event.schedule.rate; State = event.schedule.enabled ? 'ENABLED' : 'DISABLED'; @@ -40,8 +46,14 @@ class AwsCompileScheduledEvents { ScheduleExpression = event.schedule; State = 'ENABLED'; } else { + const errorMessage = [ + `Schedule event of function ${functionName} is not an object nor a string`, + ' The correct syntax is: schedule: rate(10 minutes)', + ' OR an object with "rate" property.', + ' Please check the docs for more info.', + ].join(''); throw new this.serverless.classes - .Error(`Schedule event of function ${functionName} is not an object nor a string`); + .Error(errorMessage); } const scheduleTemplate = ` diff --git a/lib/plugins/aws/deploy/compile/functions/index.js b/lib/plugins/aws/deploy/compile/functions/index.js index 5eb6da521..4219a0013 100644 --- a/lib/plugins/aws/deploy/compile/functions/index.js +++ b/lib/plugins/aws/deploy/compile/functions/index.js @@ -48,19 +48,24 @@ class AwsCompileFunctions { .S3Key = ''; // will be replaced in a further step if (!functionObject.handler) { + const errorMessage = [ + `Missing "handler" property in function ${functionName}`, + ' Please make sure you point to the correct lambda handler.', + ' For example: handler.hello.', + ' Please check the docs for more info', + ].join(''); throw new this.serverless.classes - .Error(`Missing "handler" property in function ${functionName}`); + .Error(errorMessage); } - // TODO validate the values of each of those properties const Handler = functionObject.handler; const FunctionName = functionObject.name || `${this.serverless.service.service}-${this.options.stage}-${functionName}`; - const MemorySize = functionObject.memory - || this.serverless.service.defaults.memory + const MemorySize = Number(functionObject.memory) + || Number(this.serverless.service.defaults.memory) || 1024; - const Timeout = functionObject.timeout - || this.serverless.service.defaults.timeout + const Timeout = Number(functionObject.timeout) + || Number(this.serverless.service.defaults.timeout) || 6; newFunction.Properties.Handler = Handler; diff --git a/lib/plugins/aws/deploy/lib/deployFunctions.js b/lib/plugins/aws/deploy/lib/deployFunctions.js index 6698f0166..238b8ba36 100644 --- a/lib/plugins/aws/deploy/lib/deployFunctions.js +++ b/lib/plugins/aws/deploy/lib/deployFunctions.js @@ -51,7 +51,12 @@ module.exports = { const zipFileName = `${func.name}-${(new Date).getTime().toString()}.zip`; if (!handlerFullPath.endsWith(func.handler)) { - throw new this.serverless.classes.Error(`The handler ${func.handler} was not found`); + const errorMessage = [ + `The handler ${func.handler} was not found.`, + ' Please make sure you have this handler in your service at the referenced location.', + ' Please check the docs for more info', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); } const packageRoot = handlerFullPath.replace(func.handler, ''); diff --git a/lib/plugins/aws/deploy/lib/validate.js b/lib/plugins/aws/deploy/lib/validate.js index e11ffee1e..4a81e17b7 100644 --- a/lib/plugins/aws/deploy/lib/validate.js +++ b/lib/plugins/aws/deploy/lib/validate.js @@ -5,7 +5,8 @@ const BbPromise = require('bluebird'); module.exports = { validate() { if (!this.serverless.config.servicePath) { - throw new this.serverless.classes.Error('This command can only be run inside a service'); + throw new this.serverless.classes + .Error('This command can only be run inside a service directory'); } this.options.stage = this.options.stage @@ -19,12 +20,6 @@ module.exports = { this.serverless.service.getStage(this.options.stage); this.serverless.service.getRegionInStage(this.options.stage, this.options.region); - if (Object.keys(this.serverless.service.environment - .stages[this.options.stage].regions[this.options.region]).indexOf('vars') === -1) { - throw new this.serverless.classes - .Error('region vars object does not exist in serverless.env.yaml'); - } - return BbPromise.resolve(); }, }; diff --git a/lib/plugins/aws/deploy/tests/validate.js b/lib/plugins/aws/deploy/tests/validate.js index 7222a0aaa..ffd29b041 100644 --- a/lib/plugins/aws/deploy/tests/validate.js +++ b/lib/plugins/aws/deploy/tests/validate.js @@ -54,10 +54,5 @@ describe('#validate()', () => { awsDeploy.serverless.config.servicePath = false; expect(() => awsDeploy.validate()).to.throw(Error); }); - - it('should throw error if region vars object does not exist', () => { - awsDeploy.serverless.service.environment.stages.dev.regions['us-east-1'] = {}; - expect(() => awsDeploy.validate()).to.throw(Error); - }); }); diff --git a/lib/plugins/aws/invoke/index.js b/lib/plugins/aws/invoke/index.js index 521cfebb7..0ce003f26 100644 --- a/lib/plugins/aws/invoke/index.js +++ b/lib/plugins/aws/invoke/index.js @@ -41,7 +41,7 @@ class AwsInvoke { if (this.options.path) { if (!this.serverless.utils .fileExistsSync(path.join(this.serverless.config.servicePath, this.options.path))) { - throw new this.serverless.classes.Error('The file path you provided does not exist.'); + throw new this.serverless.classes.Error('The file you provided does not exist.'); } this.options.data = this.serverless.utils diff --git a/lib/plugins/create/create.js b/lib/plugins/create/create.js index 995a310ea..d1bddeceb 100644 --- a/lib/plugins/create/create.js +++ b/lib/plugins/create/create.js @@ -49,7 +49,11 @@ class Create { } if (['aws', 'azure', 'google', 'ibm'].indexOf(this.options.provider)) { - throw new this.serverless.classes.Error('Please provide a valid provider.'); + const errorMessage = [ + `Provider "${this.options.provider}" is not supported.`, + ' Valid values for provider are: aws, azure, google, ibm.', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); } this.serverless.config diff --git a/tests/classes/Service.js b/tests/classes/Service.js index 4e51b1cce..672a81a0f 100644 --- a/tests/classes/Service.js +++ b/tests/classes/Service.js @@ -507,6 +507,45 @@ describe('Service', () => { }); }); + it('should throw error if provider property is invalid', () => { + const SUtils = new Utils(); + const tmpDirPath = path.join(os.tmpdir(), (new Date).getTime().toString()); + const serverlessYaml = { + service: 'service-name', + provider: 'invalid', + functions: {}, + }; + const serverlessEnvYaml = { + vars: {}, + stages: { + dev: { + vars: {}, + regions: {}, + }, + }, + }; + + serverlessEnvYaml.stages.dev.regions['us-east-1'] = { + vars: {}, + }; + + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), + YAML.dump(serverlessYaml)); + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.env.yaml'), + YAML.dump(serverlessEnvYaml)); + + const serverless = new Serverless({ servicePath: tmpDirPath }); + serviceInstance = new Service(serverless); + + return serviceInstance.load().then(() => { + // if we reach this, then no error was thrown as expected + // so make assertion fail intentionally to let us know something is wrong + expect(1).to.equal(2); + }).catch(e => { + expect(e.name).to.be.equal('ServerlessError'); + }); + }); + it('should throw error if functions property is missing', () => { const SUtils = new Utils(); const tmpDirPath = path.join(os.tmpdir(), (new Date).getTime().toString());