Repository: serverless/serverless (JavaScript)
Date: 2017-12-19 10:25:08+00:00
Task type: Feature

Issue:
Support JS configuration to make configuration more flexible
# This is a Feature Proposal

## Description

I would like to see support for Javascript files as an additional configuration format. The current YAML format works fine, but a JS file could give the following advantages:

* For large services, you can easily **split** the configuration file into smaller pieces
* It would be easier to **make differences**, e.g. based on environment or region (side note: you need to have access to some environment variables to do this)
* This would allow **re-use** of configuration parts (e.g. from your internal NPM modules)
* This would allow **auto-generation** of (parts of) the configuration (e.g. from a Swagger/OpenAPI spec)

It could work similar to how Webpack does it. A file named `serverless.js` or `serverless.config.js` could simply export a CommonJS module, containing the configuration object.

Looking forward to your feedback!


Issue discussion:
This would be really awesome!  For those of us that despise YAML, json is the only option but JSON doesn't support comments so its a huge drag.  I make a lot of use of the node-config package and its ability to use js config files has been great.   In one of my serverless projects I've even gone as far as to wrap all my serverless calls with yarn and compile my own serverless.js file into a build directory as serverless.json and run it from there.  This works but has the unfortunate side effect of not being able to run serverless cli at the top level.

I was just poking around the repo to see how tricky this might be when I ran across this proposal.  It looks like it would be very simple to add support for this when loading the file but there are a few places around the code base where the various formats are mentioned.

Solution patch:
diff --git a/docs/providers/aws/guide/intro.md b/docs/providers/aws/guide/intro.md
index 8f042645434..e4ea181a567 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` (or `serverless.json`).  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` or `serverless.js`).  It looks like this:
 
 ```yml
 # serverless.yml
diff --git a/docs/providers/azure/guide/intro.md b/docs/providers/azure/guide/intro.md
index 4f5fe67ec5f..f212424a91b 100644
--- a/docs/providers/azure/guide/intro.md
+++ b/docs/providers/azure/guide/intro.md
@@ -64,7 +64,7 @@ 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:
+`serverless.json` or `serverless.js`). It looks like this:
 
 ```yml
 # serverless.yml
diff --git a/docs/providers/google/guide/intro.md b/docs/providers/google/guide/intro.md
index af49d99fb03..4fbf37d6a16 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` (or `serverless.json`). 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` or `serverless.js`). It looks like this:
 
 ```yml
 # serverless.yml
diff --git a/docs/providers/kubeless/guide/intro.md b/docs/providers/kubeless/guide/intro.md
index a6227c9418f..4a2fdd53354 100644
--- a/docs/providers/kubeless/guide/intro.md
+++ b/docs/providers/kubeless/guide/intro.md
@@ -42,7 +42,7 @@ Anything that triggers an Kubeless Event to execute is regarded by the Framework
 
 ### Services
 
-A **Service** is the Serverless Framework's unit of organization (not to be confused with [Kubernetes Services](https://kubernetes.io/docs/concepts/services-networking/service/).  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 and the Events that trigger them, all in one file entitled `serverless.yml` (or `serverless.json`).  It looks like this:
+A **Service** is the Serverless Framework's unit of organization (not to be confused with [Kubernetes Services](https://kubernetes.io/docs/concepts/services-networking/service/).  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 and the Events that trigger them, all in one file entitled `serverless.yml` (or `serverless.json` or `serverless.js`).  It looks like this:
 
 ```yml
 # serverless.yml
diff --git a/docs/providers/openwhisk/guide/intro.md b/docs/providers/openwhisk/guide/intro.md
index cbf4a6c57e8..1306d6254de 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` (or `serverless.json`).  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` or `serverless.js`).  It looks like this:
 
 ```yml
 # serverless.yml
diff --git a/docs/providers/spotinst/guide/intro.md b/docs/providers/spotinst/guide/intro.md
index fb34107c788..ea915470093 100644
--- a/docs/providers/spotinst/guide/intro.md
+++ b/docs/providers/spotinst/guide/intro.md
@@ -73,7 +73,7 @@ module.exports.main = function main (event, context, callback) {
 
 ### 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` (or `serverless.json`).  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` or `serverless.js`).  It looks like this:
 
 ```yml
 
diff --git a/docs/providers/webtasks/guide/intro.md b/docs/providers/webtasks/guide/intro.md
index 3dce6f97b9b..9a8b18cd094 100644
--- a/docs/providers/webtasks/guide/intro.md
+++ b/docs/providers/webtasks/guide/intro.md
@@ -24,7 +24,7 @@ Here are the Framework's main concepts and how they pertain to Auth0 Webtasks...
 
 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.  
 
-The Auth0 Webtasks platform was designed to be simple and easy to use with minimal configuration. Therefore, services that uses Auth0 Webtasks are just a few lines of configuration in a single file, entitled `serverless.yml` (or `serverless.json`). It looks like this:
+The Auth0 Webtasks platform was designed to be simple and easy to use with minimal configuration. Therefore, services that uses Auth0 Webtasks are just a few lines of configuration in a single file, entitled `serverless.yml` (or `serverless.json` or `serverless.js`). It looks like this:
 
 ```yml
 # serverless.yml
diff --git a/lib/classes/Service.js b/lib/classes/Service.js
index c69686c81da..68991debd7e 100644
--- a/lib/classes/Service.js
+++ b/lib/classes/Service.js
@@ -46,6 +46,7 @@ class Service {
       'serverless.yaml',
       'serverless.yml',
       'serverless.json',
+      'serverless.js',
     ];
 
     const serviceFilePaths = _.map(serviceFilenames, filename => path.join(servicePath, filename));
@@ -61,65 +62,85 @@ class Service {
       serviceFilenames[serviceFileIndex] :
       _.first(serviceFilenames);
 
+    if (serviceFilename === 'serverless.js') {
+      return BbPromise.try(() => {
+        // use require to load serverless.js file
+        // eslint-disable-next-line global-require
+        const config = require(serviceFilePath);
+
+        if (!_.isPlainObject(config)) {
+          throw new Error('serverless.js must export plain object');
+        }
+
+        return that.loadServiceFileParam(serviceFilename, config);
+      });
+    }
+
     return that.serverless.yamlParser
       .parse(serviceFilePath)
-      .then((serverlessFileParam) => {
-        const serverlessFile = serverlessFileParam;
-        // basic service level validation
-        const version = this.serverless.utils.getVersion();
-        const ymlVersion = serverlessFile.frameworkVersion;
-        if (ymlVersion && !semver.satisfies(version, ymlVersion)) {
-          const errorMessage = [
-            `The Serverless version (${version}) does not satisfy the`,
-            ` "frameworkVersion" (${ymlVersion}) in ${serviceFilename}`,
-          ].join('');
-          throw new ServerlessError(errorMessage);
-        }
-        if (!serverlessFile.service) {
-          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 ${serviceFilename}`);   // eslint-disable-line max-len
-        }
-        if (!serverlessFile.provider) {
-          throw new ServerlessError(`"provider" property is missing in ${serviceFilename}`);
-        }
+      .then((serverlessFileParam) =>
+        that.loadServiceFileParam(serviceFilename, serverlessFileParam)
+      );
+  }
 
-        if (typeof serverlessFile.provider !== 'object') {
-          const providerName = serverlessFile.provider;
-          serverlessFile.provider = {
-            name: providerName,
-          };
-        }
+  loadServiceFileParam(serviceFilename, serverlessFileParam) {
+    const that = this;
 
-        if (_.isObject(serverlessFile.service)) {
-          that.serviceObject = serverlessFile.service;
-          that.service = serverlessFile.service.name;
-        } else {
-          that.serviceObject = { name: serverlessFile.service };
-          that.service = serverlessFile.service;
-        }
+    const serverlessFile = serverlessFileParam;
+    // basic service level validation
+    const version = this.serverless.utils.getVersion();
+    const ymlVersion = serverlessFile.frameworkVersion;
+    if (ymlVersion && !semver.satisfies(version, ymlVersion)) {
+      const errorMessage = [
+        `The Serverless version (${version}) does not satisfy the`,
+        ` "frameworkVersion" (${ymlVersion}) in ${serviceFilename}`,
+      ].join('');
+      throw new ServerlessError(errorMessage);
+    }
+    if (!serverlessFile.service) {
+      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 ${serviceFilename}`);   // eslint-disable-line max-len
+    }
+    if (!serverlessFile.provider) {
+      throw new ServerlessError(`"provider" property is missing in ${serviceFilename}`);
+    }
 
-        that.custom = serverlessFile.custom;
-        that.plugins = serverlessFile.plugins;
-        that.resources = serverlessFile.resources;
-        that.functions = serverlessFile.functions || {};
-
-        // merge so that the default settings are still in place and
-        // won't be overwritten
-        that.provider = _.merge(that.provider, serverlessFile.provider);
-
-        if (serverlessFile.package) {
-          that.package.individually = serverlessFile.package.individually;
-          that.package.path = serverlessFile.package.path;
-          that.package.artifact = serverlessFile.package.artifact;
-          that.package.exclude = serverlessFile.package.exclude;
-          that.package.include = serverlessFile.package.include;
-          that.package.excludeDevDependencies = serverlessFile.package.excludeDevDependencies;
-        }
+    if (!_.isObject(serverlessFile.provider)) {
+      const providerName = serverlessFile.provider;
+      serverlessFile.provider = {
+        name: providerName,
+      };
+    }
 
-        return this;
-      });
+    if (_.isObject(serverlessFile.service)) {
+      that.serviceObject = serverlessFile.service;
+      that.service = serverlessFile.service.name;
+    } else {
+      that.serviceObject = { name: serverlessFile.service };
+      that.service = serverlessFile.service;
+    }
+
+    that.custom = serverlessFile.custom;
+    that.plugins = serverlessFile.plugins;
+    that.resources = serverlessFile.resources;
+    that.functions = serverlessFile.functions || {};
+
+    // merge so that the default settings are still in place and
+    // won't be overwritten
+    that.provider = _.merge(that.provider, serverlessFile.provider);
+
+    if (serverlessFile.package) {
+      that.package.individually = serverlessFile.package.individually;
+      that.package.path = serverlessFile.package.path;
+      that.package.artifact = serverlessFile.package.artifact;
+      that.package.exclude = serverlessFile.package.exclude;
+      that.package.include = serverlessFile.package.include;
+      that.package.excludeDevDependencies = serverlessFile.package.excludeDevDependencies;
+    }
+
+    return this;
   }
 
   setFunctionNames(rawOptions) {
diff --git a/lib/classes/Utils.js b/lib/classes/Utils.js
index e73a5d3741b..921a5ee596e 100644
--- a/lib/classes/Utils.js
+++ b/lib/classes/Utils.js
@@ -105,6 +105,8 @@ class Utils {
       servicePath = process.cwd();
     } else if (fileExistsSync(path.join(process.cwd(), 'serverless.json'))) {
       servicePath = process.cwd();
+    } else if (fileExistsSync(path.join(process.cwd(), 'serverless.js'))) {
+      servicePath = process.cwd();
     }
 
     return servicePath;
diff --git a/lib/plugins/package/lib/packageService.js b/lib/plugins/package/lib/packageService.js
index 0c43ff0b484..67a73208aca 100644
--- a/lib/plugins/package/lib/packageService.js
+++ b/lib/plugins/package/lib/packageService.js
@@ -14,6 +14,7 @@ module.exports = {
     'serverless.yml',
     'serverless.yaml',
     'serverless.json',
+    'serverless.js',
     '.serverless/**',
     '.serverless_plugins/**',
   ],
diff --git a/lib/plugins/plugin/install/install.js b/lib/plugins/plugin/install/install.js
index 932a6cc612b..c688128bba8 100644
--- a/lib/plugins/plugin/install/install.js
+++ b/lib/plugins/plugin/install/install.js
@@ -109,6 +109,14 @@ class PluginInstall {
 
   addPluginToServerlessFile() {
     return this.getServerlessFilePath().then(serverlessFilePath => {
+      if (_.last(_.split(serverlessFilePath, '.')) === 'js') {
+        this.serverless.cli.log(`
+          Can't automatically add plugin into "serverless.js" file.
+          Please make it manually.
+        `);
+        return BbPromise.resolve();
+      }
+
       if (_.last(_.split(serverlessFilePath, '.')) === 'json') {
         return fse.readJsonAsync(serverlessFilePath).then(serverlessFileObj => {
           const newServerlessFileObj = serverlessFileObj;
diff --git a/lib/plugins/plugin/lib/utils.js b/lib/plugins/plugin/lib/utils.js
index 6f74ec36bb7..2bed10b7fc6 100644
--- a/lib/plugins/plugin/lib/utils.js
+++ b/lib/plugins/plugin/lib/utils.js
@@ -19,22 +19,31 @@ module.exports = {
 
   getServerlessFilePath() {
     const servicePath = this.serverless.config.servicePath;
-    const serverlessYmlFilePath = path.join(servicePath, 'serverless.yml');
-    const serverlessYamlFilePath = path.join(servicePath, 'serverless.yaml');
-    const serverlessJsonFilePath = path.join(servicePath, 'serverless.json');
+    const ymlFilePath = path.join(servicePath, 'serverless.yml');
+    const yamlFilePath = path.join(servicePath, 'serverless.yaml');
+    const jsonFilePath = path.join(servicePath, 'serverless.json');
+    const jsFilePath = path.join(servicePath, 'serverless.js');
 
-    return fileExists(serverlessYmlFilePath)
-    .then(ymlExists => {
-      if (!ymlExists) {
-        return fileExists(serverlessYamlFilePath)
-        .then(yamlExists => {
-          if (!yamlExists) {
-            return serverlessJsonFilePath;
-          }
-          return serverlessYamlFilePath;
-        });
+    return BbPromise.props({
+      json: fileExists(jsonFilePath),
+      yml: fileExists(ymlFilePath),
+      yaml: fileExists(yamlFilePath),
+      js: fileExists(jsFilePath),
+    }).then((exists) => {
+      if (exists.yml) {
+        return ymlFilePath;
+      } else if (exists.yaml) {
+        return yamlFilePath;
+      } else if (exists.json) {
+        return jsonFilePath;
+      } else if (exists.js) {
+        return jsFilePath;
       }
-      return serverlessYmlFilePath;
+      return BbPromise.reject(
+          new this.serverless.classes.Error(
+            'Could not find any serverless service definition file.'
+          )
+        );
     });
   },
 
diff --git a/lib/plugins/plugin/uninstall/uninstall.js b/lib/plugins/plugin/uninstall/uninstall.js
index 55f55adba05..05580a98ab8 100644
--- a/lib/plugins/plugin/uninstall/uninstall.js
+++ b/lib/plugins/plugin/uninstall/uninstall.js
@@ -78,6 +78,14 @@ class PluginUninstall {
 
   removePluginFromServerlessFile() {
     return this.getServerlessFilePath().then(serverlessFilePath => {
+      if (_.last(_.split(serverlessFilePath, '.')) === 'js') {
+        this.serverless.cli.log(`
+          Can't automatically remove plugin from "serverless.js" file.
+          Please make it manually.
+        `);
+        return BbPromise.resolve();
+      }
+
       if (_.last(_.split(serverlessFilePath, '.')) === 'json') {
         return fse.readJsonAsync(serverlessFilePath).then(serverlessFileObj => {
           if (serverlessFileObj.plugins) {
diff --git a/lib/utils/getServerlessConfigFile.js b/lib/utils/getServerlessConfigFile.js
index 06e566033cb..a85a1b67b6f 100644
--- a/lib/utils/getServerlessConfigFile.js
+++ b/lib/utils/getServerlessConfigFile.js
@@ -10,11 +10,13 @@ const getServerlessConfigFile = _.memoize((servicePath) => {
   const jsonPath = path.join(servicePath, 'serverless.json');
   const ymlPath = path.join(servicePath, 'serverless.yml');
   const yamlPath = path.join(servicePath, 'serverless.yaml');
+  const jsPath = path.join(servicePath, 'serverless.js');
 
   return BbPromise.props({
     json: fileExists(jsonPath),
     yml: fileExists(ymlPath),
     yaml: fileExists(yamlPath),
+    js: fileExists(jsPath),
   }).then((exists) => {
     if (exists.json) {
       return readFile(jsonPath);
@@ -22,6 +24,17 @@ const getServerlessConfigFile = _.memoize((servicePath) => {
       return readFile(ymlPath);
     } else if (exists.yaml) {
       return readFile(yamlPath);
+    } else if (exists.js) {
+      return BbPromise.try(() => {
+        // use require to load serverless.js
+        // eslint-disable-next-line global-require
+        const config = require(jsPath);
+
+        if (_.isPlainObject(config)) {
+          return config;
+        }
+        throw new Error('serverless.js must export plain object');
+      });
     }
     return '';
   });


Test patch:
diff --git a/lib/classes/Service.test.js b/lib/classes/Service.test.js
index 5ea2eb39a98..96bf7529fad 100644
--- a/lib/classes/Service.test.js
+++ b/lib/classes/Service.test.js
@@ -287,6 +287,75 @@ describe('Service', () => {
       });
     });
 
+    it('should load serverless.js from filesystem', () => {
+      const SUtils = new Utils();
+      const serverlessJSON = {
+        service: 'new-service',
+        provider: {
+          name: 'aws',
+          stage: 'dev',
+          region: 'us-east-1',
+          variableSyntax: '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}',
+        },
+        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.js'),
+        `module.exports = ${JSON.stringify(serverlessJSON)};`);
+
+      const serverless = new Serverless();
+      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(
+          '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}'
+        );
+        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');
+        expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
+      });
+    });
+
+    it('should throw error if serverless.js exports invalid config', () => {
+      const SUtils = new Utils();
+
+      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.js'),
+        'module.exports = function config() {};');
+
+      const serverless = new Serverless();
+      serverless.config.update({ servicePath: tmpDirPath });
+      serviceInstance = new Service(serverless);
+
+      return expect(serviceInstance.load())
+        .to.be.rejectedWith('serverless.js must export plain object');
+    });
+
     it('should load YAML in favor of JSON', () => {
       const SUtils = new Utils();
       const serverlessJSON = {
diff --git a/lib/classes/Utils.test.js b/lib/classes/Utils.test.js
index 890b40191d6..4f8745e1245 100644
--- a/lib/classes/Utils.test.js
+++ b/lib/classes/Utils.test.js
@@ -281,6 +281,18 @@ describe('Utils', () => {
       expect(servicePath).to.not.equal(null);
     });
 
+    it('should detect if the CWD is a service directory when using Serverless .js files', () => {
+      const tmpDirPath = testUtils.getTmpDirPath();
+      const tmpFilePath = path.join(tmpDirPath, 'serverless.js');
+
+      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.test.js b/lib/plugins/package/lib/packageService.test.js
index 6a1c1d5f925..a625e0f3099 100644
--- a/lib/plugins/package/lib/packageService.test.js
+++ b/lib/plugins/package/lib/packageService.test.js
@@ -82,7 +82,7 @@ describe('#packageService()', () => {
       expect(exclude).to.deep.equal([
         '.git/**', '.gitignore', '.DS_Store',
         'npm-debug.log', 'serverless.yml',
-        'serverless.yaml', 'serverless.json',
+        'serverless.yaml', 'serverless.json', 'serverless.js',
         '.serverless/**', '.serverless_plugins/**',
         'dir', 'file.js',
       ]);
@@ -102,7 +102,7 @@ describe('#packageService()', () => {
       expect(exclude).to.deep.equal([
         '.git/**', '.gitignore', '.DS_Store',
         'npm-debug.log', 'serverless.yml',
-        'serverless.yaml', 'serverless.json',
+        'serverless.yaml', 'serverless.json', 'serverless.js',
         '.serverless/**', '.serverless_plugins/**',
         'dir', 'file.js', 'lib', 'other.js',
       ]);
diff --git a/lib/plugins/plugin/install/install.test.js b/lib/plugins/plugin/install/install.test.js
index 5faf7ba9a7d..396dd603a16 100644
--- a/lib/plugins/plugin/install/install.test.js
+++ b/lib/plugins/plugin/install/install.test.js
@@ -357,6 +357,24 @@ describe('PluginInstall', () => {
         });
       });
     });
+
+    it('should not modify serverless .js file', () => {
+      const serverlessJsFilePath = path.join(servicePath, 'serverless.js');
+      const serverlessJson = {
+        service: 'plugin-service',
+        provider: 'aws',
+        plugins: [],
+      };
+      serverless.utils
+        .writeFileSync(serverlessJsFilePath, `module.exports = ${JSON.stringify(serverlessJson)};`);
+      pluginInstall.options.pluginName = 'serverless-plugin-1';
+      return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
+        // use require to load serverless.js
+        // eslint-disable-next-line global-require
+        expect(require(serverlessJsFilePath).plugins)
+          .to.be.deep.equal([]);
+      });
+    });
   });
 
   describe('#installPeerDependencies()', () => {
diff --git a/lib/plugins/plugin/lib/utils.test.js b/lib/plugins/plugin/lib/utils.test.js
index d89a21b52b2..018883a2f57 100644
--- a/lib/plugins/plugin/lib/utils.test.js
+++ b/lib/plugins/plugin/lib/utils.test.js
@@ -99,6 +99,20 @@ describe('PluginUtils', () => {
         expect(serverlessFilePath).to.equal(serverlessJsonFilePath);
       });
     });
+
+    it('should return the correct serverless file path for a .js file', () => {
+      const serverlessJsFilePath = path.join(servicePath, 'serverless.js');
+      fse.ensureFileSync(serverlessJsFilePath);
+
+      return expect(pluginUtils.getServerlessFilePath()).to.be.fulfilled
+      .then(serverlessFilePath => {
+        expect(serverlessFilePath).to.equal(serverlessJsFilePath);
+      });
+    });
+
+    it('should reject if no configuration file exists', () =>
+      expect(pluginUtils.getServerlessFilePath())
+        .to.be.rejectedWith('Could not find any serverless service definition file.'));
   });
 
   describe('#getPlugins()', () => {
diff --git a/lib/plugins/plugin/uninstall/uninstall.test.js b/lib/plugins/plugin/uninstall/uninstall.test.js
index 9b86b85a89e..a119fbbc0ea 100644
--- a/lib/plugins/plugin/uninstall/uninstall.test.js
+++ b/lib/plugins/plugin/uninstall/uninstall.test.js
@@ -339,6 +339,31 @@ describe('PluginUninstall', () => {
           .to.not.have.property('plugins');
       }));
     });
+
+    it('should not modify serverless .js file', () => {
+      it('should not modify serverless .js file', () => {
+        const serverlessJsFilePath = path.join(servicePath, 'serverless.js');
+        const serverlessJson = {
+          service: 'plugin-service',
+          provider: 'aws',
+          plugins: [
+            'serverless-plugin-1',
+            'serverless-plugin-2',
+          ],
+        };
+        serverless.utils.writeFileSync(
+          serverlessJsFilePath,
+          `module.exports = ${JSON.stringify(serverlessJson)};`
+        );
+        pluginUninstall.options.pluginName = 'serverless-plugin-1';
+        return expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => {
+          // use require to load serverless.js
+          // eslint-disable-next-line global-require
+          expect(require(serverlessJsFilePath).plugins)
+            .to.be.deep.equal(serverlessJson.plugins);
+        });
+      });
+    });
   });
 
   describe('#uninstallPeerDependencies()', () => {
diff --git a/lib/utils/getServerlessConfigFile.test.js b/lib/utils/getServerlessConfigFile.test.js
index 3be22a09cb5..2d6c0fa488e 100644
--- a/lib/utils/getServerlessConfigFile.test.js
+++ b/lib/utils/getServerlessConfigFile.test.js
@@ -48,4 +48,29 @@ describe('#getServerlessConfigFile()', () => {
       expect(result).to.deep.equal({ service: 'my-json-service' });
     });
   });
+
+  it('should return the file content if a serverless.js file found', () => {
+    const serverlessFilePath = path.join(tmpDirPath, 'serverless.js');
+    writeFileSync(
+      serverlessFilePath,
+      'module.exports = {"service": "my-json-service"};'
+    );
+
+    return expect(getServerlessConfigFile(tmpDirPath)).to.be.fulfilled.then(
+      (result) => {
+        expect(result).to.deep.equal({ service: 'my-json-service' });
+      }
+    );
+  });
+
+  it('should throw an error, if serverless.js export not a plain object', () => {
+    const serverlessFilePath = path.join(tmpDirPath, 'serverless.js');
+    writeFileSync(
+      serverlessFilePath,
+      'module.exports = function config() {};'
+    );
+
+    return expect(getServerlessConfigFile(tmpDirPath))
+      .to.be.rejectedWith('serverless.js must export plain object');
+  });
 });