Repository: serverless/serverless (JavaScript)
Date: 2019-02-16 20:06:25+00:00
Task type: Bug Fix

Issue:
serverless.js files not packaged even if it isn't the config file being used
# This is a Bug Report

## Description

* What went wrong?
If I have a folder named `serverless` and a js file named `serverless.js`. The JS file won't be packed and there is no warnings.
* What did you expect should have happened?
The JS file should be packed.
* What was the config you used?
I didn't `include/exclude` any files in my config
* What stacktrace or error message from your provider did you see?
No.

Similar or dependent issues:
None

## Additional Data

* ***Serverless Framework Version you're using***: 1.36.3
* ***Operating System***: Mac OS
* ***Stack Trace***: N/A
* ***Provider Error messages***: N/A



Issue discussion:
This isn't because the files are of the same name (after ignoring extensions). It's that `serverless.js` is a valid configuraiton file for serverless, and as such, is [ignored by the default ignore patterns](https://github.com/serverless/serverless/blob/master/lib/plugins/package/lib/packageService.js#L18) (just as `serverless.yml` is ignored).

A work around would be to explicitly add it back to the include directive:
```yaml
package:
  include:
    - serverless.js
```

But I'll leave this open as we can do better by only ignoring the config file that is used rather than all possible config files (will be useful with #5589 too, so that customized config file names are ignored too)

Solution patch:
diff --git a/docs/providers/aws/guide/packaging.md b/docs/providers/aws/guide/packaging.md
index 62ff61611ef..46bb8955651 100644
--- a/docs/providers/aws/guide/packaging.md
+++ b/docs/providers/aws/guide/packaging.md
@@ -43,6 +43,17 @@ Serverless will run the glob patterns in order.
 At first it will apply the globs defined in `exclude`. After that it'll add all the globs from `include`. This way you can always re-include
 previously excluded files and directories.
 
+By default, serverless will exclude the following patterns:
+
+- .git/**
+- .gitignore
+- .DS_Store
+- npm-debug.log
+- .serverless/**
+- .serverless_plugins/**
+
+and the serverless configuration file being used (i.e. `serverless.yml`)
+
 ### Examples
 
 Exclude all node_modules but then re-include a specific modules (in this case node-fetch) using `exclude` exclusively
diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js
index 877d2efef8f..ebe0d2e45be 100644
--- a/lib/classes/PluginManager.js
+++ b/lib/classes/PluginManager.js
@@ -6,7 +6,7 @@ const BbPromise = require('bluebird');
 const _ = require('lodash');
 const writeFile = require('../utils/fs/writeFile');
 const getCacheFilePath = require('../utils/getCacheFilePath');
-const getServerlessConfigFile = require('../utils/getServerlessConfigFile');
+const serverlessConfigFileUtils = require('../utils/getServerlessConfigFile');
 const crypto = require('crypto');
 const getCommandSuggestion = require('../utils/getCommandSuggestion');
 
@@ -42,7 +42,8 @@ class PluginManager {
   }
 
   loadConfigFile() {
-    return getServerlessConfigFile(this.serverless.config.servicePath)
+    return serverlessConfigFileUtils
+      .getServerlessConfigFile(this.serverless.config.servicePath)
       .then((serverlessConfigFile) => {
         this.serverlessConfigFile = serverlessConfigFile;
         return;
diff --git a/lib/plugins/package/lib/packageService.js b/lib/plugins/package/lib/packageService.js
index ea8099f04c1..6bacbadf0c0 100644
--- a/lib/plugins/package/lib/packageService.js
+++ b/lib/plugins/package/lib/packageService.js
@@ -5,7 +5,7 @@ const path = require('path');
 const globby = require('globby');
 const _ = require('lodash');
 const nanomatch = require('nanomatch');
-
+const serverlessConfigFileUtils = require('../../../../lib/utils/getServerlessConfigFile');
 
 module.exports = {
   defaultExcludes: [
@@ -13,10 +13,6 @@ module.exports = {
     '.gitignore',
     '.DS_Store',
     'npm-debug.log',
-    'serverless.yml',
-    'serverless.yaml',
-    'serverless.json',
-    'serverless.js',
     '.serverless/**',
     '.serverless_plugins/**',
   ],
@@ -27,17 +23,30 @@ module.exports = {
   },
 
   getExcludes(exclude, excludeLayers) {
-    const packageExcludes = this.serverless.service.package.exclude || [];
-    // add local service plugins Path
-    const pluginsLocalPath = this.serverless.pluginManager
-      .parsePluginsObject(this.serverless.service.plugins).localPath;
-    const localPathExcludes = pluginsLocalPath ? [pluginsLocalPath] : [];
-    // add layer paths
-    const layerExcludes = excludeLayers ? this.serverless.service.getAllLayers().map(
-      (layer) => `${this.serverless.service.getLayer(layer).path}/**`) : [];
-    // add defaults for exclude
-    return _.union(
-      this.defaultExcludes, localPathExcludes, packageExcludes, layerExcludes, exclude);
+    return serverlessConfigFileUtils
+      .getServerlessConfigFilePath(this.serverless.config.servicePath)
+      .then(configFilePath => {
+        const packageExcludes = this.serverless.service.package.exclude || [];
+        // add local service plugins Path
+        const pluginsLocalPath = this.serverless.pluginManager
+          .parsePluginsObject(this.serverless.service.plugins).localPath;
+        const localPathExcludes = pluginsLocalPath ? [pluginsLocalPath] : [];
+        // add layer paths
+        const layerExcludes = excludeLayers ? this.serverless.service.getAllLayers().map(
+          (layer) => `${this.serverless.service.getLayer(layer).path}/**`) : [];
+        // add defaults for exclude
+
+        const serverlessConfigFileExclude = configFilePath ? [path.basename(configFilePath)] : [];
+
+        return _.union(
+          this.defaultExcludes,
+          serverlessConfigFileExclude,
+          localPathExcludes,
+          packageExcludes,
+          layerExcludes,
+          exclude
+        );
+      });
   },
 
   packageService() {
@@ -162,33 +171,39 @@ module.exports = {
   },
 
   resolveFilePathsAll() {
-    const params = { exclude: this.getExcludes([], true), include: this.getIncludes() };
-    return this.excludeDevDependencies(params).then(() =>
-      this.resolveFilePathsFromPatterns(params));
+    return this.getExcludes([], true)
+      .then(exclude => {
+        const params = { exclude, include: this.getIncludes() };
+        return params;
+      })
+      .then(params => this.excludeDevDependencies(params))
+      .then(params => this.resolveFilePathsFromPatterns(params));
   },
 
   resolveFilePathsFunction(functionName) {
     const functionObject = this.serverless.service.getFunction(functionName);
     const funcPackageConfig = functionObject.package || {};
 
-    const params = {
-      exclude: this.getExcludes(funcPackageConfig.exclude, true),
-      include: this.getIncludes(funcPackageConfig.include),
-    };
-    return this.excludeDevDependencies(params).then(() =>
-      this.resolveFilePathsFromPatterns(params));
+    return this.getExcludes(funcPackageConfig.exclude, true)
+      .then(exclude => {
+        const params = { exclude, include: this.getIncludes(funcPackageConfig.include) };
+        return params;
+      })
+      .then(params => this.excludeDevDependencies(params))
+      .then(params => this.resolveFilePathsFromPatterns(params));
   },
 
   resolveFilePathsLayer(layerName) {
     const layerObject = this.serverless.service.getLayer(layerName);
     const layerPackageConfig = layerObject.package || {};
 
-    const params = {
-      exclude: this.getExcludes(layerPackageConfig.exclude),
-      include: this.getIncludes(layerPackageConfig.include),
-    };
-    return this.excludeDevDependencies(params).then(() => this.resolveFilePathsFromPatterns(
-      params, layerObject.path));
+    return this.getExcludes(layerPackageConfig.exclude, true)
+      .then(exclude => {
+        const params = { exclude, include: this.getIncludes(layerPackageConfig.include) };
+        return params;
+      })
+      .then(params => this.excludeDevDependencies(params))
+      .then(params => this.resolveFilePathsFromPatterns(params, layerObject.path));
   },
 
   resolveFilePathsFromPatterns(params, prefix) {
diff --git a/lib/utils/getServerlessConfigFile.js b/lib/utils/getServerlessConfigFile.js
index 16ac7084d1d..c0f07a6b2bd 100644
--- a/lib/utils/getServerlessConfigFile.js
+++ b/lib/utils/getServerlessConfigFile.js
@@ -6,7 +6,7 @@ const path = require('path');
 const fileExists = require('./fs/fileExists');
 const readFile = require('./fs/readFile');
 
-const getServerlessConfigFile = _.memoize((srvcPath) => {
+const getServerlessConfigFilePath = (srvcPath) => {
   const servicePath = srvcPath || process.cwd();
   const jsonPath = path.join(servicePath, 'serverless.json');
   const ymlPath = path.join(servicePath, 'serverless.yml');
@@ -18,29 +18,48 @@ const getServerlessConfigFile = _.memoize((srvcPath) => {
     yml: fileExists(ymlPath),
     yaml: fileExists(yamlPath),
     js: fileExists(jsPath),
-  }).then((exists) => {
+  }).then(exists => {
     if (exists.json) {
-      return readFile(jsonPath);
+      return jsonPath;
     } else if (exists.yml) {
-      return readFile(ymlPath);
+      return ymlPath;
     } else if (exists.yaml) {
-      return readFile(yamlPath);
+      return yamlPath;
     } else if (exists.js) {
-      return BbPromise.try(() => {
-        // use require to load serverless.js
-        // eslint-disable-next-line global-require
-        const configExport = require(jsPath);
-        // In case of a promise result, first resolve it.
-        return configExport;
-      }).then(config => {
-        if (_.isPlainObject(config)) {
-          return config;
-        }
-        throw new Error('serverless.js must export plain object');
-      });
+      return jsPath;
     }
-    return '';
+
+    return null;
   });
+};
+
+const handleJsConfigFile = (jsConfigFile) => BbPromise.try(() => {
+  // use require to load serverless.js
+  // eslint-disable-next-line global-require
+  const configExport = require(jsConfigFile);
+  // In case of a promise result, first resolve it.
+  return configExport;
+}).then(config => {
+  if (_.isPlainObject(config)) {
+    return config;
+  }
+  throw new Error('serverless.js must export plain object');
 });
 
-module.exports = getServerlessConfigFile;
+const getServerlessConfigFile = _.memoize(srvcPath => getServerlessConfigFilePath(srvcPath)
+  .then(configFilePath => {
+    if (configFilePath !== null) {
+      const isJSConfigFile = _.last(_.split(configFilePath, '.')) === 'js';
+
+      if (isJSConfigFile) {
+        return handleJsConfigFile(configFilePath);
+      }
+
+      return readFile(configFilePath);
+    }
+
+    return '';
+  })
+);
+
+module.exports = { getServerlessConfigFile, getServerlessConfigFilePath };


Test patch:
diff --git a/lib/plugins/package/lib/packageService.test.js b/lib/plugins/package/lib/packageService.test.js
index c483d1b0342..9747566c7ef 100644
--- a/lib/plugins/package/lib/packageService.test.js
+++ b/lib/plugins/package/lib/packageService.test.js
@@ -7,6 +7,7 @@ const chai = require('chai');
 const sinon = require('sinon');
 const Package = require('../package');
 const Serverless = require('../../../Serverless');
+const serverlessConfigFileUtils = require('../../../../lib/utils/getServerlessConfigFile');
 
 // Configure chai
 chai.use(require('chai-as-promised'));
@@ -68,33 +69,72 @@ describe('#packageService()', () => {
   });
 
   describe('#getExcludes()', () => {
-    it('should exclude defaults', () => {
-      const exclude = packagePlugin.getExcludes();
-      expect(exclude).to.deep.equal(packagePlugin.defaultExcludes);
+    const serverlessConfigFileName = 'serverless.xyz';
+    let getServerlessConfigFilePathStub;
+
+    beforeEach(() => {
+      getServerlessConfigFilePathStub = sinon
+        .stub(serverlessConfigFileUtils, 'getServerlessConfigFilePath')
+        .returns(BbPromise.resolve(`/path/to/${serverlessConfigFileName}`));
+    });
+
+    afterEach(() => {
+      serverlessConfigFileUtils.getServerlessConfigFilePath.restore();
     });
 
+    it('should exclude defaults and serverless config file being used', () =>
+      expect(packagePlugin.getExcludes()).to.be.fulfilled
+        .then(exclude => BbPromise.join(
+          expect(getServerlessConfigFilePathStub).to.be.calledOnce,
+          expect(exclude).to.deep.equal(
+            _.union(packagePlugin.defaultExcludes, [serverlessConfigFileName])
+          )
+        ))
+      );
+
     it('should exclude plugins localPath defaults', () => {
       const localPath = './myplugins';
       serverless.service.plugins = { localPath };
 
-      const exclude = packagePlugin.getExcludes();
-      expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes, [localPath]));
+      return expect(packagePlugin.getExcludes()).to.be.fulfilled
+        .then(exclude =>
+          expect(exclude).to.deep.equal(
+            _.union(packagePlugin.defaultExcludes, [serverlessConfigFileName], [localPath])
+          )
+        );
     });
 
     it('should not exclude plugins localPath if it is empty', () => {
       const localPath = '';
       serverless.service.plugins = { localPath };
 
-      const exclude = packagePlugin.getExcludes();
-      expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes));
+      return expect(packagePlugin.getExcludes()).to.be.fulfilled
+        .then(exclude =>
+          expect(exclude).to.deep.equal(
+            _.union(packagePlugin.defaultExcludes, [serverlessConfigFileName])
+          )
+        );
     });
 
     it('should not exclude plugins localPath if it is not a string', () => {
       const localPath = {};
       serverless.service.plugins = { localPath };
 
-      const exclude = packagePlugin.getExcludes();
-      expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes));
+      return expect(packagePlugin.getExcludes()).to.be.fulfilled
+        .then(exclude =>
+          expect(exclude).to.deep.equal(
+            _.union(packagePlugin.defaultExcludes, [serverlessConfigFileName])
+          )
+        );
+    });
+
+    it('should not exclude serverlessConfigFilePath if is not found', () => {
+      getServerlessConfigFilePathStub.returns(BbPromise.resolve(null));
+
+      return expect(packagePlugin.getExcludes()).to.be.fulfilled
+        .then(exclude =>
+          expect(exclude).to.deep.equal(packagePlugin.defaultExcludes)
+        );
     });
 
     it('should merge defaults with plugin localPath and excludes', () => {
@@ -106,10 +146,12 @@ describe('#packageService()', () => {
       ];
       serverless.service.package.exclude = packageExcludes;
 
-
-      const exclude = packagePlugin.getExcludes();
-      expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes,
-        [localPath], packageExcludes));
+      return expect(packagePlugin.getExcludes()).to.be.fulfilled
+        .then(exclude =>
+          expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes,
+            [serverlessConfigFileName], [localPath], packageExcludes)
+          )
+        );
     });
 
     it('should merge defaults with plugin localPath package and func excludes', () => {
@@ -125,9 +167,12 @@ describe('#packageService()', () => {
         'lib', 'other.js',
       ];
 
-      const exclude = packagePlugin.getExcludes(funcExcludes);
-      expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes,
-        [localPath], packageExcludes, funcExcludes));
+      return expect(packagePlugin.getExcludes(funcExcludes)).to.be.fulfilled
+        .then(exclude =>
+          expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes,
+            [serverlessConfigFileName], [localPath], packageExcludes, funcExcludes)
+          )
+        );
     });
   });
 
@@ -257,7 +302,7 @@ describe('#packageService()', () => {
 
     beforeEach(() => {
       getExcludesStub = sinon
-        .stub(packagePlugin, 'getExcludes').returns(exclude);
+        .stub(packagePlugin, 'getExcludes').returns(BbPromise.resolve(exclude));
       getIncludesStub = sinon
         .stub(packagePlugin, 'getIncludes').returns(include);
       resolveFilePathsFromPatternsStub = sinon
@@ -330,7 +375,7 @@ describe('#packageService()', () => {
 
     beforeEach(() => {
       getExcludesStub = sinon
-        .stub(packagePlugin, 'getExcludes').returns(exclude);
+        .stub(packagePlugin, 'getExcludes').returns(BbPromise.resolve(exclude));
       getIncludesStub = sinon
         .stub(packagePlugin, 'getIncludes').returns(include);
       resolveFilePathsFromPatternsStub = sinon
@@ -460,7 +505,7 @@ describe('#packageService()', () => {
 
     beforeEach(() => {
       getExcludesStub = sinon
-        .stub(packagePlugin, 'getExcludes').returns(exclude);
+        .stub(packagePlugin, 'getExcludes').returns(BbPromise.resolve(exclude));
       getIncludesStub = sinon
         .stub(packagePlugin, 'getIncludes').returns(include);
       resolveFilePathsFromPatternsStub = sinon
diff --git a/lib/utils/getServerlessConfigFile.test.js b/lib/utils/getServerlessConfigFile.test.js
index e3feb9ac57f..17318bb56b3 100644
--- a/lib/utils/getServerlessConfigFile.test.js
+++ b/lib/utils/getServerlessConfigFile.test.js
@@ -4,7 +4,9 @@ const path = require('path');
 const expect = require('chai').expect;
 const testUtils = require('../../tests/utils');
 const writeFileSync = require('./fs/writeFileSync');
-const getServerlessConfigFile = require('./getServerlessConfigFile');
+const serverlessConfigFileUtils = require('./getServerlessConfigFile');
+
+const getServerlessConfigFile = serverlessConfigFileUtils.getServerlessConfigFile;
 
 describe('#getServerlessConfigFile()', () => {
   let tmpDirPath;