From a227e5b3d2da8dbd221478e7b2d4433f8060694b Mon Sep 17 00:00:00 2001 From: Frank Schmid Date: Thu, 18 May 2017 21:26:44 +0200 Subject: [PATCH 1/5] Added support for serverless.json. Improved error messages with filename. --- lib/classes/Service.js | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/lib/classes/Service.js b/lib/classes/Service.js index 72026fad6f2..49707a6e601 100644 --- a/lib/classes/Service.js +++ b/lib/classes/Service.js @@ -33,7 +33,7 @@ class Service { const options = rawOptions || {}; options.stage = options.stage || options.s; options.region = options.region || options.r; - const servicePath = that.serverless.config.servicePath; + const servicePath = this.serverless.config.servicePath; // skip if the service path is not found // because the user might be creating a new service @@ -41,15 +41,29 @@ class Service { return BbPromise.resolve(); } - let serverlessYmlPath = path.join(servicePath, 'serverless.yml'); - // change to serverless.yaml if the file could not be found - if (!this.serverless.utils.fileExistsSync(serverlessYmlPath)) { - serverlessYmlPath = path - .join(this.serverless.config.servicePath, 'serverless.yaml'); - } + // List of supported service filename variants. + // The order defines the precedence. + const serviceFilenames = [ + 'serverless.yaml', + 'serverless.yml', + 'serverless.json', + ]; + + const serviceFilePaths = _.map(serviceFilenames, filename => path.join(servicePath, filename)); + const serviceFileIndex = _.findIndex(serviceFilePaths, + filename => this.serverless.utils.fileExistsSync(filename) + ); + + // Set the filename if found, otherwise set the preferred variant. + const serviceFilePath = serviceFileIndex !== -1 ? + serviceFilePaths[serviceFileIndex] : + _.first(serviceFilePaths); + const serviceFilename = serviceFileIndex !== -1 ? + serviceFilenames[serviceFileIndex] : + _.first(serviceFilenames); return that.serverless.yamlParser - .parse(serverlessYmlPath) + .parse(serviceFilePath) .then((serverlessFileParam) => { const serverlessFile = serverlessFileParam; // basic service level validation @@ -58,18 +72,18 @@ class Service { if (ymlVersion && !semver.satisfies(version, ymlVersion)) { const errorMessage = [ `The Serverless version (${version}) does not satisfy the`, - ` "frameworkVersion" (${ymlVersion}) in serverless.yml`, + ` "frameworkVersion" (${ymlVersion}) in ${serviceFilename}`, ].join(''); throw new ServerlessError(errorMessage); } if (!serverlessFile.service) { - throw new ServerlessError('"service" property is missing in serverless.yml'); + throw new ServerlessError(`"service" property is missing in ${serviceFilename}`); } if (_.isObject(serverlessFile.service) && !serverlessFile.service.name) { - throw new ServerlessError('"service" is missing the "name" property in serverless.yml'); + throw new ServerlessError(`"service" is missing the "name" property in ${serviceFilename}`); // eslint-disable-line max-len } if (!serverlessFile.provider) { - throw new ServerlessError('"provider" property is missing in serverless.yml'); + throw new ServerlessError(`"provider" property is missing in ${serviceFilename}`); } if (typeof serverlessFile.provider !== 'object') { @@ -84,7 +98,7 @@ class Service { const errorMessage = [ `Provider "${serverlessFile.provider.name}" is not supported.`, ` Valid values for provider are: ${providers.join(', ')}.`, - ' Please provide one of those values to the "provider" property in serverless.yml.', + ` Please provide one of those values to the "provider" property in ${serviceFilename}`, ].join(''); throw new ServerlessError(errorMessage); } From 45a2b20467ac04b612e06e8e063ad1d4820c2cba Mon Sep 17 00:00:00 2001 From: Frank Schmid Date: Thu, 18 May 2017 21:47:15 +0200 Subject: [PATCH 2/5] Fixed unit tests - removed filename from literal error check --- lib/classes/Service.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/classes/Service.test.js b/lib/classes/Service.test.js index 49ab96f100a..08e0f7e3346 100644 --- a/lib/classes/Service.test.js +++ b/lib/classes/Service.test.js @@ -189,7 +189,7 @@ describe('Service', () => { serviceInstance = new Service(serverless); return expect(serviceInstance.load()).to.eventually.be - .rejectedWith('"service" is missing the "name" property in serverless.yml'); + .rejectedWith('"service" is missing the "name" property in'); }); it('should support service objects', () => { From 97399773a7f6ca6f05e50a2ebe52644b1f9b9a8e Mon Sep 17 00:00:00 2001 From: Frank Schmid Date: Fri, 19 May 2017 11:41:24 +0200 Subject: [PATCH 3/5] Added file load tests for YAML, YML and JSON. Added precedence test. --- lib/classes/Service.test.js | 155 +++++++++++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/lib/classes/Service.test.js b/lib/classes/Service.test.js index 08e0f7e3346..a03d7dc885c 100644 --- a/lib/classes/Service.test.js +++ b/lib/classes/Service.test.js @@ -122,7 +122,7 @@ describe('Service', () => { return expect(noService.load()).to.eventually.resolve; }); - it('should load from filesystem', () => { + it('should load serverless.yml from filesystem', () => { const SUtils = new Utils(); const serverlessYml = { service: 'new-service', @@ -175,6 +175,159 @@ describe('Service', () => { }); }); + it('should load serverless.yaml from filesystem', () => { + const SUtils = new Utils(); + const serverlessYml = { + service: 'new-service', + provider: { + name: 'aws', + stage: 'dev', + region: 'us-east-1', + variableSyntax: '\\${{([\\s\\S]+?)}}', + }, + plugins: ['testPlugin'], + functions: { + functionA: {}, + }, + resources: { + aws: { + resourcesProp: 'value', + }, + azure: {}, + google: {}, + }, + package: { + exclude: ['exclude-me'], + include: ['include-me'], + artifact: 'some/path/foo.zip', + }, + }; + + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), + YAML.dump(serverlessYml)); + + const serverless = new Serverless(); + serverless.init(); + serverless.config.update({ servicePath: tmpDirPath }); + serviceInstance = new Service(serverless); + + return expect(serviceInstance.load()).to.eventually.be.fulfilled + .then(() => { + expect(serviceInstance.service).to.be.equal('new-service'); + expect(serviceInstance.provider.name).to.deep.equal('aws'); + expect(serviceInstance.provider.variableSyntax).to.equal('\\${{([\\s\\S]+?)}}'); + expect(serviceInstance.plugins).to.deep.equal(['testPlugin']); + expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' }); + expect(serviceInstance.resources.azure).to.deep.equal({}); + expect(serviceInstance.resources.google).to.deep.equal({}); + expect(serviceInstance.package.exclude.length).to.equal(1); + expect(serviceInstance.package.exclude[0]).to.equal('exclude-me'); + expect(serviceInstance.package.include.length).to.equal(1); + expect(serviceInstance.package.include[0]).to.equal('include-me'); + expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip'); + }); + }); + + it('should load serverless.json from filesystem', () => { + const SUtils = new Utils(); + const serverlessJSON = { + service: 'new-service', + provider: { + name: 'aws', + stage: 'dev', + region: 'us-east-1', + variableSyntax: '\\${{([\\s\\S]+?)}}', + }, + plugins: ['testPlugin'], + functions: { + functionA: {}, + }, + resources: { + aws: { + resourcesProp: 'value', + }, + azure: {}, + google: {}, + }, + package: { + exclude: ['exclude-me'], + include: ['include-me'], + artifact: 'some/path/foo.zip', + }, + }; + + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.json'), + JSON.stringify(serverlessJSON)); + + const serverless = new Serverless(); + serverless.init(); + serverless.config.update({ servicePath: tmpDirPath }); + serviceInstance = new Service(serverless); + + return expect(serviceInstance.load()).to.eventually.be.fulfilled + .then(() => { + expect(serviceInstance.service).to.be.equal('new-service'); + expect(serviceInstance.provider.name).to.deep.equal('aws'); + expect(serviceInstance.provider.variableSyntax).to.equal('\\${{([\\s\\S]+?)}}'); + expect(serviceInstance.plugins).to.deep.equal(['testPlugin']); + expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' }); + expect(serviceInstance.resources.azure).to.deep.equal({}); + expect(serviceInstance.resources.google).to.deep.equal({}); + expect(serviceInstance.package.exclude.length).to.equal(1); + expect(serviceInstance.package.exclude[0]).to.equal('exclude-me'); + expect(serviceInstance.package.include.length).to.equal(1); + expect(serviceInstance.package.include[0]).to.equal('include-me'); + expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip'); + }); + }); + + it('should load YAML in favor of JSON', () => { + const SUtils = new Utils(); + const serverlessJSON = { + provider: { + name: 'aws', + stage: 'dev', + region: 'us-east-1', + variableSyntax: '\\${{([\\s\\S]+?)}}', + }, + plugins: ['testPlugin'], + functions: { + functionA: {}, + }, + resources: { + aws: { + resourcesProp: 'value', + }, + azure: {}, + google: {}, + }, + package: { + exclude: ['exclude-me'], + include: ['include-me'], + artifact: 'some/path/foo.zip', + }, + }; + + serverlessJSON.service = 'JSON service'; + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.json'), + JSON.stringify(serverlessJSON)); + + serverlessJSON.service = 'YAML service'; + SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), + YAML.dump(serverlessJSON)); + + const serverless = new Serverless(); + serverless.init(); + serverless.config.update({ servicePath: tmpDirPath }); + serviceInstance = new Service(serverless); + + return expect(serviceInstance.load()).to.eventually.be.fulfilled + .then(() => { + // YAML should have been loaded instead of JSON + expect(serviceInstance.service).to.be.equal('YAML service'); + }); + }); + it('should reject when the service name is missing', () => { const SUtils = new Utils(); const serverlessYaml = { From e6f2c2d3aad0b26c1d43f81fa00573f9257e7dd1 Mon Sep 17 00:00:00 2001 From: Philipp Muens Date: Fri, 19 May 2017 15:08:35 +0200 Subject: [PATCH 4/5] Update other relevant parts of the codebase --- lib/classes/Utils.js | 2 ++ lib/classes/Utils.test.js | 12 ++++++++++++ lib/plugins/package/lib/packageService.js | 3 ++- lib/plugins/package/lib/packageService.test.js | 8 ++++---- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/classes/Utils.js b/lib/classes/Utils.js index 6790e646037..f5c6ac843d1 100644 --- a/lib/classes/Utils.js +++ b/lib/classes/Utils.js @@ -155,6 +155,8 @@ class Utils { servicePath = process.cwd(); } else if (this.serverless.utils.fileExistsSync(path.join(process.cwd(), 'serverless.yaml'))) { servicePath = process.cwd(); + } else if (this.serverless.utils.fileExistsSync(path.join(process.cwd(), 'serverless.json'))) { + servicePath = process.cwd(); } return servicePath; diff --git a/lib/classes/Utils.test.js b/lib/classes/Utils.test.js index 6dd47b1be7c..361fb01b1b8 100644 --- a/lib/classes/Utils.test.js +++ b/lib/classes/Utils.test.js @@ -272,6 +272,18 @@ describe('Utils', () => { expect(servicePath).to.not.equal(null); }); + it('should detect if the CWD is a service directory when using Serverless .json files', () => { + const tmpDirPath = testUtils.getTmpDirPath(); + const tmpFilePath = path.join(tmpDirPath, 'serverless.json'); + + serverless.utils.writeFileSync(tmpFilePath, 'foo'); + process.chdir(tmpDirPath); + + const servicePath = serverless.utils.findServicePath(); + + expect(servicePath).to.not.equal(null); + }); + it('should detect if the CWD is not a service directory', () => { // just use the root of the tmpdir because findServicePath will // also check parent directories (and may find matching tmp dirs diff --git a/lib/plugins/package/lib/packageService.js b/lib/plugins/package/lib/packageService.js index e1556f70f76..3609583644f 100644 --- a/lib/plugins/package/lib/packageService.js +++ b/lib/plugins/package/lib/packageService.js @@ -9,8 +9,9 @@ module.exports = { '.gitignore', '.DS_Store', 'npm-debug.log', - 'serverless.yaml', 'serverless.yml', + 'serverless.yaml', + 'serverless.json', '.serverless/**', ], diff --git a/lib/plugins/package/lib/packageService.test.js b/lib/plugins/package/lib/packageService.test.js index 8d76af4afa7..00c4a592f96 100644 --- a/lib/plugins/package/lib/packageService.test.js +++ b/lib/plugins/package/lib/packageService.test.js @@ -80,8 +80,8 @@ describe('#packageService()', () => { const exclude = packagePlugin.getExcludes(); expect(exclude).to.deep.equal([ '.git/**', '.gitignore', '.DS_Store', - 'npm-debug.log', - 'serverless.yaml', 'serverless.yml', + 'npm-debug.log', 'serverless.yml', + 'serverless.yaml', 'serverless.json', '.serverless/**', 'dir', 'file.js', ]); }); @@ -99,8 +99,8 @@ describe('#packageService()', () => { const exclude = packagePlugin.getExcludes(funcExcludes); expect(exclude).to.deep.equal([ '.git/**', '.gitignore', '.DS_Store', - 'npm-debug.log', - 'serverless.yaml', 'serverless.yml', + 'npm-debug.log', 'serverless.yml', + 'serverless.yaml', 'serverless.json', '.serverless/**', 'dir', 'file.js', 'lib', 'other.js', ]); From 081c612814f6452e01b91fc75490e5883abdd91e Mon Sep 17 00:00:00 2001 From: Philipp Muens Date: Thu, 25 May 2017 11:48:55 +0200 Subject: [PATCH 5/5] Add documentation --- docs/providers/aws/guide/intro.md | 2 +- docs/providers/azure/guide/intro.md | 2 +- docs/providers/google/guide/intro.md | 2 +- docs/providers/openwhisk/guide/intro.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/providers/aws/guide/intro.md b/docs/providers/aws/guide/intro.md index dfb190472a1..2bcb2833d76 100644 --- a/docs/providers/aws/guide/intro.md +++ b/docs/providers/aws/guide/intro.md @@ -58,7 +58,7 @@ The Serverless Framework not only deploys your Functions and the Events that tri ### Services -A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml`. It looks like this: +A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this: ```yml # serverless.yml diff --git a/docs/providers/azure/guide/intro.md b/docs/providers/azure/guide/intro.md index afd78a3b330..55fe71ca348 100644 --- a/docs/providers/azure/guide/intro.md +++ b/docs/providers/azure/guide/intro.md @@ -47,7 +47,7 @@ When you define an event for your Azure Function in the Serverless Framework, th ### Services -A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml`. It looks like this: +A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this: ```yml # serverless.yml diff --git a/docs/providers/google/guide/intro.md b/docs/providers/google/guide/intro.md index 321ed26ebd7..af49d99fb03 100644 --- a/docs/providers/google/guide/intro.md +++ b/docs/providers/google/guide/intro.md @@ -46,7 +46,7 @@ When you define an event for your Google Cloud Function in the Serverless Framew ### Services -A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml`. It looks like this: +A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this: ```yml # serverless.yml diff --git a/docs/providers/openwhisk/guide/intro.md b/docs/providers/openwhisk/guide/intro.md index 26d848c8bb5..215093ac081 100644 --- a/docs/providers/openwhisk/guide/intro.md +++ b/docs/providers/openwhisk/guide/intro.md @@ -47,7 +47,7 @@ When you define an event for your Apache OpenWhisk Action in the Serverless Fram ### Services -A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml`. It looks like this: +A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application. It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this: ```yml # serverless.yml