Repository: serverless-heaven/serverless-webpack
Pull request #254 (2017-10-17 06:17:50)

Make serverless-webpack more extensible
<!--
1. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the PR
2. Please follow the template, otherwise we'll have to ask you to update it and it will take longer until your PR is merged
-->

## What did you implement:
Use PluginManager to make serverless-webpack more extensible. I am trying to create my own plugin that works with serverless-webpack, but I am unable to extend before the validation step without this change.

Closes #XXXXX

<!--
Briefly describe the feature if no issue exists for this PR. If possible only
submit PRs for existing issues. If the PR is trivial (like doc changes or simple
code fixes) it can be submitted without a related issue, but as soon as it adds
or changes functionality, a related issue should be present.
-->

## How did you implement it:
Following the advice from serverless blog on Advanced Plugin Development. Used Plugin Manager. 
https://serverless.com/blog/advanced-plugin-development-extending-the-core-lifecycle/

<!--
If this is a nontrivial change please briefly describe your implementation so its easy for us to understand and review your code.
-->

## How can we verify it:
As stated in the blog above... this will enable better extensibility. 
Try to create a new serverless plugin that runs before the serverless-webpack before:deploy:createDeploymentArtifacts hook. 

<!--
Add any applicable config, commands, screenshots or other resources
to make it easy for us to verify this works. The easier you make it for us
to review a PR, the faster we can review and merge it.

Examples:
* Step by step description, how to verify
* Screenshots - Showing the difference between your output and the master
* Other - Anything else that comes to mind to help us evaluate
-->

## Todos:

- [x] Write tests
- [x] Write documentation
- [x] Fix linting errors
- [x] Make sure code coverage hasn't dropped
- [x] Provide verification config / commands / resources
- [x] Enable "Allow edits from maintainers" for this PR
- [x] Update the messages below

***Is this ready for review?:*** YES
***Is it a breaking change?:*** NO


HyperBrain: Hi @josephleehunsaker ,

thanks for the PR (and reading my blog post 😄 ). The idea to run a separate lifecycle that can be hooked is really beneficial.
I think the spproach to just "run" the webpack command is the wrong approach in this case. It would be better to declare a completely internal lifecycle as entrypoint and spawn that via plugin manager.

That offers a much better reusability of the internal steps like validation and does not invoke a complete command. I did such an implementation in the AWS deploy command which spawns it's internal lifecycle (aws:deploy:*) instead of executing a whole command.

Commands should not be run from a plugin (using "run") because they may introduce side effects special to that command.

A better solution would be to declare entrypoints webpack:validate, webpack:compile, webpack:packExternalModules and webpack:packModules and use pluginManager.spawn('....') to invoke their lifecycles. Then any plugin can hook wherever it wants to, may it be the validation, the compilation or the packaging (both levels). The public commands and hooks (webpack, before:....) will then only spawn the webpack lifecycles.

So the hookable lifecycle model would be:
```
-> webpack:validate:validate
-> webpack:compile:compile
-> webpack:package:packExternalModules
-> webpack:package:packModules
```

The webpack plugin hooks will then just spawn them like this:
```
-> webpack
  -> webpack:validate
  -> webpack:compile
  -> webpack:package

-> before:deploy:createDeploymentArtifacts
  -> webpack:validate
  -> webpack:compile
  -> webpack:package

-> before:deploy:function:packageFunction
  -> webpack:validate
  -> webpack:compile
  -> webpack:package
```

A 3rd party plugin can then hook any of these, and additionally the lifecycle events themselves are extensible, so that e.g. the  `webpack:package` lifecycle could easily get new events if needed.
The last benefit is, that the webpack command will be independent and could even be dropped or changed, without changing anything.
HyperBrain: If you have "enable edits by maintainers" enabled, I will change the PR accordingly - should be straight forward (and make the unit tests test the new behavior).
josephleehunsaker: Terrific, thx. I would appreciate it if you could get it working just right :) 

I am still learning webpack plugins and serverless plugins.. so I did just enough to get the job done on my end. 

I believe "enable edits by maintainers" is enabled. Let me know if you are unable to change the PR.
HyperBrain: @josephleehunsaker You should now be able to hook `before:webpack:validate:validate` or `after:webpack:validate:validate`. Your hook will be triggered on all occurences where the webpack plugin calls its validation (e.g. webpack command, sls package/deploy, etc).

Patch:
diff --git a/README.md b/README.md
index e14673b..0ea73c4 100644
--- a/README.md
+++ b/README.md
@@ -26,11 +26,7 @@ WebPack's [Tree-Shaking][link-webpack-tree] optimization.
 
 ## Recent improvements
 
-* Support for individual packaging and optimization
-* Integrate with `serverless invoke local` (including watch mode)
-* Stabilized package handling
-* Improved compatibility with other plugins
-* Updated examples
+* Improved extensibility for plugin authors (see _For Developers_ section)
 
 For the complete release notes see the end of this document.
 
@@ -406,6 +402,59 @@ Plugin commands are supported by the following providers. ⁇ indicates that com
 | invoke local          |      ✔︎     |         ✔︎        |        ⁇        |            ⁇           |
 | invoke local --watch  |      ✔︎     |         ✔︎        |        ⁇        |            ⁇           |
 
+## For developers
+
+The plugin exposes a complete lifecycle model that can be hooked by other plugins to extend
+the functionality of the plugin or add additional actions.
+
+### The event lifecycles and their hookable events (H)
+
+All events (H) can be hooked by a plugin.
+
+```
+-> webpack:validate
+   -> webpack:validate:validate (H)
+-> webpack:compile
+   -> webpack:compile:compile (H)
+-> webpack:package
+   -> webpack:package:packExternalModules (H)
+   -> webpack:package:packageModules (H)
+```
+
+### Integration of the lifecycles into the command invocations and hooks
+
+The following list shows all lifecycles that are invoked/started by the
+plugin when running a command or invoked by a hook.
+
+```
+-> before:package:createDeploymentArtifacts
+   -> webpack:validate
+   -> webpack:compile
+   -> webpack:package
+
+-> before:deploy:function:packageFunction
+   -> webpack:validate
+   -> webpack:compile
+   -> webpack:package
+
+-> before:invoke:local:invoke
+   -> webpack:validate
+   -> webpack:compile
+
+-> webpack
+   -> webpack:validate
+   -> webpack:compile
+   -> webpack:package
+
+-> before:offline:start
+   -> webpack:validate
+   -> webpack:compile
+
+-> before:offline:start:init
+   -> webpack:validate
+   -> webpack:compile
+```
+
 ## Release Notes
 
 * 3.1.2
diff --git a/index.js b/index.js
index 5a0ed57..5531c09 100644
--- a/index.js
+++ b/index.js
@@ -48,8 +48,7 @@ class ServerlessWebpack {
       webpack: {
         usage: 'Bundle with Webpack',
         lifecycleEvents: [
-          'validate',
-          'compile',
+          'webpack'
         ],
         options: {
           out: {
@@ -58,22 +57,23 @@ class ServerlessWebpack {
           },
         },
         commands: {
-          invoke: {
-            usage: 'Run a function locally from the webpack output bundle',
+          validate: {
+            type: 'entrypoint',
             lifecycleEvents: [
-              'invoke',
+              'validate',
             ],
           },
-          watch: {
-            usage: 'Run a function from the webpack output bundle every time the source is changed',
+          compile: {
+            type: 'entrypoint',
             lifecycleEvents: [
-              'watch',
+              'compile',
             ],
           },
-          serve: {
-            usage: 'Simulate the API Gateway and serves lambdas locally',
+          package: {
+            type: 'entrypoint',
             lifecycleEvents: [
-              'serve',
+              'packExternalModules',
+              'packageModules'
             ],
           },
         },
@@ -82,23 +82,21 @@ class ServerlessWebpack {
 
     this.hooks = {
       'before:package:createDeploymentArtifacts': () => BbPromise.bind(this)
-        .then(this.validate)
-        .then(this.compile)
-        .then(this.packExternalModules)
-        .then(this.packageModules),
+        .then(() => this.serverless.pluginManager.spawn('webpack:validate'))
+        .then(() => this.serverless.pluginManager.spawn('webpack:compile'))
+        .then(() => this.serverless.pluginManager.spawn('webpack:package')),
 
       'after:package:createDeploymentArtifacts': () => BbPromise.bind(this)
         .then(this.cleanup),
 
       'before:deploy:function:packageFunction': () => BbPromise.bind(this)
-        .then(this.validate)
-        .then(this.compile)
-        .then(this.packExternalModules)
-        .then(this.packageModules),
+        .then(() => this.serverless.pluginManager.spawn('webpack:validate'))
+        .then(() => this.serverless.pluginManager.spawn('webpack:compile'))
+        .then(() => this.serverless.pluginManager.spawn('webpack:package')),
 
       'before:invoke:local:invoke': () => BbPromise.bind(this)
-        .then(this.validate)
-        .then(this.compile)
+        .then(() => this.serverless.pluginManager.spawn('webpack:validate'))
+        .then(() => this.serverless.pluginManager.spawn('webpack:compile'))
         .then(this.prepareLocalInvoke),
 
       'after:invoke:local:invoke': () => BbPromise.bind(this)
@@ -109,31 +107,34 @@ class ServerlessWebpack {
           return BbPromise.resolve();
         }),
 
-      'webpack:validate': () => BbPromise.bind(this)
-        .then(this.validate),
+      'webpack:webpack': () => BbPromise.bind(this)
+        .then(() => this.serverless.pluginManager.spawn('webpack:validate'))
+        .then(() => this.serverless.pluginManager.spawn('webpack:compile'))
+        .then(() => this.serverless.pluginManager.spawn('webpack:package')),
 
-      'webpack:compile': () => BbPromise.bind(this)
-        .then(this.compile)
-        .then(this.packExternalModules)
-        .then(this.packageModules),
+      /*
+       * Internal webpack events (can be hooked by plugins)
+       */
+      'webpack:validate:validate': () => BbPromise.bind(this)
+        .then(this.validate),
 
-      'webpack:invoke:invoke': () => BbPromise.bind(this)
-        .then(() => BbPromise.reject(new this.serverless.classes.Error('Use "serverless invoke local" instead.'))),
+      'webpack:compile:compile': () => BbPromise.bind(this)
+        .then(this.compile),
 
-      'webpack:watch:watch': () => BbPromise.bind(this)
-        .then(() => BbPromise.reject(new this.serverless.classes.Error('Use "serverless invoke local --watch" instead.'))),
+      'webpack:package:packExternalModules': () => BbPromise.bind(this)
+        .then(this.packExternalModules),
 
-      'webpack:serve:serve': () => BbPromise.bind(this)
-        .then(() => BbPromise.reject(new this.serverless.classes.Error('serve has been removed. Use serverless-offline instead.'))),
+      'webpack:package:packageModules': () => BbPromise.bind(this)
+        .then(this.packageModules),
 
       'before:offline:start': () => BbPromise.bind(this)
         .then(this.prepareOfflineInvoke)
-        .then(this.compile)
+        .then(() => this.serverless.pluginManager.spawn('webpack:compile'))
         .then(this.wpwatch),
 
       'before:offline:start:init': () => BbPromise.bind(this)
         .then(this.prepareOfflineInvoke)
-        .then(this.compile)
+        .then(() => this.serverless.pluginManager.spawn('webpack:compile'))
         .then(this.wpwatch),
 
     };
diff --git a/lib/compile.js b/lib/compile.js
index c7d0afc..aff1f34 100644
--- a/lib/compile.js
+++ b/lib/compile.js
@@ -37,8 +37,9 @@ module.exports = {
         });
 
         this.compileOutputPaths = compileOutputPaths;
+        this.compileStats = stats;
 
-        return stats;
+        return BbPromise.resolve();
       });
   },
 };
diff --git a/lib/packExternalModules.js b/lib/packExternalModules.js
index acdfaa1..7b88e7c 100644
--- a/lib/packExternalModules.js
+++ b/lib/packExternalModules.js
@@ -158,7 +158,9 @@ module.exports = {
    * This will utilize the npm cache at its best and give us the needed results
    * and performance.
    */
-  packExternalModules(stats) {
+  packExternalModules() {
+
+    const stats = this.compileStats;
 
     const includes = (
       this.serverless.service.custom &&
@@ -166,7 +168,7 @@ module.exports = {
     );
 
     if (!includes) {
-      return BbPromise.resolve(stats);
+      return BbPromise.resolve();
     }
 
     // Read plugin configuration
@@ -231,7 +233,7 @@ module.exports = {
       if (_.isEmpty(compositeModules)) {
         // The compiled code does not reference any external modules at all
         this.serverless.cli.log('No external modules needed');
-        return BbPromise.resolve(stats);
+        return BbPromise.resolve();
       }
 
       // (1.a) Install all needed modules
@@ -304,7 +306,7 @@ module.exports = {
           .tap(() => this.options.verbose && this.serverless.cli.log(`Prune: ${modulePath} [${_.now() - startPrune} ms]`));
         });
       })
-      .return(stats);
+      .return();
     });
   }
 };
diff --git a/lib/packageModules.js b/lib/packageModules.js
index aa01218..dc21c77 100644
--- a/lib/packageModules.js
+++ b/lib/packageModules.js
@@ -77,7 +77,9 @@ function zip(directory, name) {
 }
 
 module.exports = {
-  packageModules(stats) {
+  packageModules() {
+    const stats = this.compileStats;
+
     return BbPromise.mapSeries(stats.stats, (compileStats, index) => {
       const entryFunction = _.get(this.entryFunctions, index, {});
       const filename = `${entryFunction.funcName || this.serverless.service.getServiceObject().name}.zip`;
diff --git a/lib/prepareOfflineInvoke.js b/lib/prepareOfflineInvoke.js
index 4abfd3b..e7d4683 100644
--- a/lib/prepareOfflineInvoke.js
+++ b/lib/prepareOfflineInvoke.js
@@ -13,7 +13,7 @@ module.exports = {
     // Use service packaging for compile
     _.set(this.serverless, 'service.package.individually', false);
 
-    return this.validate()
+    return this.serverless.pluginManager.spawn('webpack:validate')
     .then(() => {
       // Set offline location automatically if not set manually
       if (!this.options.location && !_.get(this.serverless, 'service.custom.serverless-offline.location')) {


Test patch:
diff --git a/tests/packExternalModules.test.js b/tests/packExternalModules.test.js
index 08dcc76..2188905 100644
--- a/tests/packExternalModules.test.js
+++ b/tests/packExternalModules.test.js
@@ -154,7 +154,8 @@ describe('packExternalModules', () => {
 
     it('should do nothing if webpackIncludeModules is not set', () => {
       _.unset(serverless, 'service.custom.webpackIncludeModules');
-      return expect(module.packExternalModules({ stats: [] })).to.eventually.deep.equal({ stats: [] })
+      module.compileStats = { stats: [] };
+      return expect(module.packExternalModules()).to.be.fulfilled
       .then(() => BbPromise.all([
         expect(fsExtraMock.copy).to.not.have.been.called,
         expect(childProcessMock.exec).to.not.have.been.called,
@@ -188,7 +189,8 @@ describe('packExternalModules', () => {
       childProcessMock.exec.onFirstCall().yields(null, '{}', '');
       childProcessMock.exec.onSecondCall().yields(null, '', '');
       childProcessMock.exec.onThirdCall().yields();
-      return expect(module.packExternalModules(stats)).to.be.fulfilled
+      module.compileStats = stats;
+      return expect(module.packExternalModules()).to.be.fulfilled
       .then(() => BbPromise.all([
         // The module package JSON and the composite one should have been stored
         expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -217,7 +219,8 @@ describe('packExternalModules', () => {
       childProcessMock.exec.onFirstCall().yields(null, '{}', '');
       childProcessMock.exec.onSecondCall().yields(new Error('npm install failed'));
       childProcessMock.exec.onThirdCall().yields();
-      return expect(module.packExternalModules(stats)).to.be.rejectedWith('npm install failed')
+      module.compileStats = stats;
+      return expect(module.packExternalModules()).to.be.rejectedWith('npm install failed')
       .then(() => BbPromise.all([
         // npm ls and npm install should have been called
         expect(childProcessMock.exec).to.have.been.calledTwice,
@@ -230,7 +233,8 @@ describe('packExternalModules', () => {
       fsExtraMock.pathExists.yields(null, false);
       fsExtraMock.copy.yields();
       childProcessMock.exec.yields(new Error('something went wrong'), '{}', stderr);
-      return expect(module.packExternalModules(stats)).to.be.rejectedWith('something went wrong')
+      module.compileStats = stats;
+      return expect(module.packExternalModules()).to.be.rejectedWith('something went wrong')
       .then(() => BbPromise.all([
         // The module package JSON and the composite one should have been stored
         expect(writeFileSyncStub).to.not.have.been.called,
@@ -250,7 +254,8 @@ describe('packExternalModules', () => {
       fsExtraMock.pathExists.yields(null, false);
       fsExtraMock.copy.yields();
       childProcessMock.exec.yields(new Error('something went wrong'), '{}', stderr);
-      return expect(module.packExternalModules(stats)).to.be.rejectedWith('something went wrong')
+      module.compileStats = stats;
+      return expect(module.packExternalModules()).to.be.rejectedWith('something went wrong')
       .then(() => BbPromise.all([
         // The module package JSON and the composite one should have been stored
         expect(writeFileSyncStub).to.not.have.been.called,
@@ -311,7 +316,8 @@ describe('packExternalModules', () => {
       childProcessMock.exec.onFirstCall().yields(new Error('NPM error'), JSON.stringify(lsResult), stderr);
       childProcessMock.exec.onSecondCall().yields(null, '', '');
       childProcessMock.exec.onThirdCall().yields();
-      return expect(module.packExternalModules(stats)).to.be.fulfilled
+      module.compileStats = stats;
+      return expect(module.packExternalModules()).to.be.fulfilled
       .then(() => BbPromise.all([
         // The module package JSON and the composite one should have been stored
         expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -338,9 +344,9 @@ describe('packExternalModules', () => {
       module.webpackOutputPath = 'outputPath';
       fsExtraMock.copy.yields();
       childProcessMock.exec.yields(null, '{}', '');
-      return expect(module.packExternalModules(noExtStats)).to.be.fulfilled
-      .then(stats => BbPromise.all([
-        expect(stats).to.deep.equal(noExtStats),
+      module.compileStats = noExtStats;
+      return expect(module.packExternalModules()).to.be.fulfilled
+      .then(() => BbPromise.all([
         // The module package JSON and the composite one should have been stored
         expect(writeFileSyncStub).to.not.have.been.called,
         // The modules should have been copied
@@ -382,7 +388,8 @@ describe('packExternalModules', () => {
       childProcessMock.exec.onFirstCall().yields(null, '{}', '');
       childProcessMock.exec.onSecondCall().yields(null, '', '');
       childProcessMock.exec.onThirdCall().yields();
-      return expect(module.packExternalModules(stats)).to.be.fulfilled
+      module.compileStats = stats;
+      return expect(module.packExternalModules()).to.be.fulfilled
       .then(() => BbPromise.all([
         // The module package JSON and the composite one should have been stored
         expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -436,7 +443,8 @@ describe('packExternalModules', () => {
       childProcessMock.exec.onFirstCall().yields(null, '{}', '');
       childProcessMock.exec.onSecondCall().yields(null, '', '');
       childProcessMock.exec.onThirdCall().yields();
-      return expect(module.packExternalModules(stats)).to.be.fulfilled
+      module.compileStats = stats;
+      return expect(module.packExternalModules()).to.be.fulfilled
       .then(() => BbPromise.all([
         // The module package JSON and the composite one should have been stored
         expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -489,7 +497,8 @@ describe('packExternalModules', () => {
       childProcessMock.exec.onFirstCall().yields(null, '{}', '');
       childProcessMock.exec.onSecondCall().yields(null, '', '');
       childProcessMock.exec.onThirdCall().yields();
-      return expect(module.packExternalModules(stats)).to.be.fulfilled
+      module.compileStats = stats;
+      return expect(module.packExternalModules()).to.be.fulfilled
       .then(() => BbPromise.all([
         // The module package JSON and the composite one should have been stored
         expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -537,7 +546,8 @@ describe('packExternalModules', () => {
       childProcessMock.exec.onFirstCall().yields(null, '{}', '');
       childProcessMock.exec.onSecondCall().yields(null, '', '');
       childProcessMock.exec.onThirdCall().yields();
-      return expect(module.packExternalModules(stats)).to.be.fulfilled
+      module.compileStats = stats;
+      return expect(module.packExternalModules()).to.be.fulfilled
       .then(() => BbPromise.all([
         // The module package JSON and the composite one should have been stored
         expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -589,7 +599,8 @@ describe('packExternalModules', () => {
       childProcessMock.exec.onFirstCall().yields(null, '{}', '');
       childProcessMock.exec.onSecondCall().yields(null, '', '');
       childProcessMock.exec.onThirdCall().yields();
-      return expect(module.packExternalModules(stats)).to.be.fulfilled
+      module.compileStats = stats;
+      return expect(module.packExternalModules()).to.be.fulfilled
       .then(() => BbPromise.all([
         // The module package JSON and the composite one should have been stored
         expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -671,7 +682,8 @@ describe('packExternalModules', () => {
         childProcessMock.exec.onFirstCall().yields(null, JSON.stringify(dependencyGraph), '');
         childProcessMock.exec.onSecondCall().yields(null, '', '');
         childProcessMock.exec.onThirdCall().yields();
-        return expect(module.packExternalModules(peerDepStats)).to.be.fulfilled
+        module.compileStats = peerDepStats;
+        return expect(module.packExternalModules()).to.be.fulfilled
         .then(() => BbPromise.all([
           // The module package JSON and the composite one should have been stored
           expect(writeFileSyncStub).to.have.been.calledTwice,
diff --git a/tests/packageModules.test.js b/tests/packageModules.test.js
index 679d9bc..1a176bb 100644
--- a/tests/packageModules.test.js
+++ b/tests/packageModules.test.js
@@ -83,7 +83,8 @@ describe('packageModules', () => {
 
   describe('packageModules()', () => {
     it('should do nothing if no compile stats are available', () => {
-      return expect(module.packageModules({ stats: [] })).to.be.fulfilled
+      module.compileStats = { stats: [] };
+      return expect(module.packageModules()).to.be.fulfilled
       .then(() => BbPromise.all([
         expect(archiverMock.create).to.not.have.been.called,
         expect(writeFileDirStub).to.not.have.been.called,
@@ -141,7 +142,8 @@ describe('packageModules', () => {
 
         const expectedArtifactPath = path.join('.serverless', 'test-service.zip');
 
-        return expect(module.packageModules(stats)).to.be.fulfilled
+        module.compileStats = stats;
+        return expect(module.packageModules()).to.be.fulfilled
         .then(() => BbPromise.all([
           expect(func1).to.have.a.nested.property('package.artifact').that.equals(expectedArtifactPath),
           expect(func2).to.have.a.nested.property('package.artifact').that.equals(expectedArtifactPath),
@@ -206,7 +208,8 @@ describe('packageModules', () => {
 
           const expectedArtifactPath = path.join('.serverless', 'test-service.zip');
 
-          return expect(module.packageModules(stats)).to.be.fulfilled
+          module.compileStats = stats;
+          return expect(module.packageModules()).to.be.fulfilled
           .then(() => expect(serverless.service).to.have.a.nested.property('package.artifact').that.equals(expectedArtifactPath));
         });
       });
@@ -252,9 +255,10 @@ describe('packageModules', () => {
 
         const expectedArtifactPath = path.join('.serverless', 'test-service.zip');
 
+        module.compileStats = stats;
         return BbPromise.each([ '1.18.1', '2.17.0', '10.15.3', ], version => {
           getVersionStub.returns(version);
-          return expect(module.packageModules(stats)).to.be.fulfilled
+          return expect(module.packageModules()).to.be.fulfilled
           .then(() => BbPromise.all([
             expect(func1).to.have.a.nested.property('package.artifact').that.equals(expectedArtifactPath),
             expect(func2).to.have.a.nested.property('package.artifact').that.equals(expectedArtifactPath),
@@ -262,7 +266,7 @@ describe('packageModules', () => {
         })
         .then(() => BbPromise.each([ '1.17.0', '1.16.0-alpha', '1.15.3', ], version => {
           getVersionStub.returns(version);
-          return expect(module.packageModules(stats)).to.be.fulfilled
+          return expect(module.packageModules()).to.be.fulfilled
           .then(() => BbPromise.all([
             expect(func1).to.have.a.nested.property('artifact').that.equals(expectedArtifactPath),
             expect(func2).to.have.a.nested.property('artifact').that.equals(expectedArtifactPath),
@@ -310,7 +314,8 @@ describe('packageModules', () => {
         fsMock._streamMock.on.withArgs('close').yields();
         fsMock._statMock.isDirectory.returns(false);
 
-        return expect(module.packageModules(stats)).to.be.rejectedWith('Packaging: No files found');
+        module.compileStats = stats;
+        return expect(module.packageModules()).to.be.rejectedWith('Packaging: No files found');
       });
     });
 
@@ -381,7 +386,8 @@ describe('packageModules', () => {
         fsMock._streamMock.on.withArgs('close').yields();
         fsMock._statMock.isDirectory.returns(false);
 
-        return expect(module.packageModules(stats)).to.be.fulfilled
+        module.compileStats = stats;
+        return expect(module.packageModules()).to.be.fulfilled
         .then(() => BbPromise.all([
           expect(func1).to.have.a.nested.property('package.artifact').that.equals(path.join('.serverless', 'func1.zip')),
           expect(func2).to.have.a.nested.property('package.artifact').that.equals(path.join('.serverless', 'func2.zip')),