Repository: serverless-heaven/serverless-webpack
Pull request #683 (2021-01-25 09:59:31)

Merge 5.4.0 into master
I would like to merge the 5.4.0 branch into master so the master branch will be ready to accept current PR targeting the 5.4.0 milestone and then I'll rebase all PRs.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1ac706a..9d180b3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Change Log
 
+* 5.4.0
+  * Skip compile & packaging if --no-build is set [#560](https://github.com/serverless-heaven/serverless-webpack/pull/560)
+  * Serialized compile to address [#299](https://github.com/serverless-heaven/serverless-webpack/pull/299) [#517](https://github.com/serverless-heaven/serverless-webpack/pull/517)
+  * Option to exclude files using regular expression [#612](https://github.com/serverless-heaven/serverless-webpack/pull/612)
+  * Speed up cleanup process [#462](https://github.com/serverless-heaven/serverless-webpack/pull/462)
+  
 * 5.3.5
   * Improve runtime validation [#629](https://github.com/serverless-heaven/serverless-webpack/pull/629)
   * Move `ts-node` as optional dependency [#636](https://github.com/serverless-heaven/serverless-webpack/pull/636)
diff --git a/README.md b/README.md
index 3cb9c37..c9d7fe1 100644
--- a/README.md
+++ b/README.md
@@ -439,7 +439,7 @@ If you have a project structure that uses something like `index.js` and a
 co-located `index.test.js` then you have likely seen an error like:
 `WARNING: More than one matching handlers found for index. Using index.js`
 
-This config option allows you to exlcude files that match a glob from function
+This config option allows you to exclude files that match a glob from function
 resolution. Just add: `excludeFiles: **/*.test.js` (with whatever glob you want
 to exclude).
 
@@ -452,6 +452,19 @@ custom:
 
 This is also useful for projects that use TypeScript.
 
+#### Exclude Files with Regular Expression
+
+This config option allows you to filter files that match a regex pattern before
+adding to the zip file. Just add: `excludeRegex: \.ts|test|\.map` (with whatever
+regex you want to exclude).
+
+```yaml
+# serverless.yml
+custom:
+  webpack:
+    excludeRegex: /\.ts|test|\.map/
+```
+
 #### Keep output directory after packaging
 
 You can keep the output directory (defaults to `.webpack`) from being removed
@@ -527,6 +540,15 @@ if you are trying to override the entry in webpack.config.js with other unsuppor
 The individual packaging needs more time at the packaging phase, but you'll
 get that paid back twice at runtime.
 
+#### Individual packaging serializedCompile
+```yaml
+# serverless.yml
+custom:
+  webpack:
+    serializedCompile: true
+```
+Will run each webpack build one at a time which helps reduce memory usage and in some cases impoves overall build performance.
+
 ## Usage
 
 ### Automatic bundling
diff --git a/index.js b/index.js
index 326669c..e370bbe 100644
--- a/index.js
+++ b/index.js
@@ -81,7 +81,7 @@ class ServerlessWebpack {
           },
           package: {
             type: 'entrypoint',
-            lifecycleEvents: [ 'packExternalModules', 'packageModules' ]
+            lifecycleEvents: [ 'packExternalModules', 'packageModules', 'copyExistingArtifacts' ]
           }
         }
       }
@@ -91,7 +91,7 @@ class ServerlessWebpack {
       'before:package:createDeploymentArtifacts': () =>
         BbPromise.bind(this)
           .then(() => this.serverless.pluginManager.spawn('webpack:validate'))
-          .then(() => this.serverless.pluginManager.spawn('webpack:compile'))
+          .then(() => (this.skipCompile ? BbPromise.resolve() : this.serverless.pluginManager.spawn('webpack:compile')))
           .then(() => this.serverless.pluginManager.spawn('webpack:package')),
 
       'after:package:createDeploymentArtifacts': () => BbPromise.bind(this).then(this.cleanup),
@@ -106,10 +106,6 @@ class ServerlessWebpack {
         BbPromise.bind(this)
           .then(() => {
             lib.webpack.isLocal = true;
-            // --no-build override
-            if (this.options.build === false) {
-              this.skipCompile = true;
-            }
 
             return this.serverless.pluginManager.spawn('webpack:validate');
           })
@@ -159,6 +155,8 @@ class ServerlessWebpack {
 
       'webpack:package:packageModules': () => BbPromise.bind(this).then(this.packageModules),
 
+      'webpack:package:copyExistingArtifacts': () => BbPromise.bind(this).then(this.copyExistingArtifacts),
+
       'before:offline:start': () =>
         BbPromise.bind(this)
           .tap(() => {
diff --git a/lib/Configuration.js b/lib/Configuration.js
index a3fde81..d2fea80 100644
--- a/lib/Configuration.js
+++ b/lib/Configuration.js
@@ -14,7 +14,8 @@ const DefaultConfig = {
   packager: 'npm',
   packagerOptions: {},
   keepOutputDirectory: false,
-  config: null
+  config: null,
+  serializedCompile: false
 };
 
 class Configuration {
@@ -52,6 +53,10 @@ class Configuration {
   get excludeFiles() {
     return this._config.excludeFiles;
   }
+  
+  get excludeRegex() {
+    return this._config.excludeRegex;
+  }
 
   get packager() {
     return this._config.packager;
@@ -73,6 +78,10 @@ class Configuration {
     return this._config.keepOutputDirectory;
   }
 
+  get serializedCompile() {
+    return this._config.serializedCompile;
+  }
+
   toJSON() {
     return _.omitBy(this._config, _.isNil);
   }
diff --git a/lib/cleanup.js b/lib/cleanup.js
index 63bfcc7..9664c9b 100644
--- a/lib/cleanup.js
+++ b/lib/cleanup.js
@@ -1,20 +1,26 @@
 'use strict';
 
+const _ = require('lodash');
 const BbPromise = require('bluebird');
 const fse = require('fs-extra');
 
 module.exports = {
   cleanup() {
     const webpackOutputPath = this.webpackOutputPath;
+    const keepOutputDirectory = this.keepOutputDirectory;
+    const cli = this.options.verbose ? this.serverless.cli : { log: _.noop };
 
-    const keepOutputDirectory = this.configuration.keepOutputDirectory;
     if (!keepOutputDirectory) {
-      this.options.verbose && this.serverless.cli.log(`Remove ${webpackOutputPath}`);
+      cli.log(`Remove ${webpackOutputPath}`);
       if (this.serverless.utils.dirExistsSync(webpackOutputPath)) {
-        fse.removeSync(webpackOutputPath);
+        // Remove async to speed up process
+        fse
+          .remove(webpackOutputPath)
+          .then(() => cli.log(`Removing ${webpackOutputPath} done`))
+          .catch(error => cli.log(`Error occurred while removing ${webpackOutputPath}: ${error}`));
       }
     } else {
-      this.options.verbose && this.serverless.cli.log(`Keeping ${webpackOutputPath}`);
+      cli.log(`Keeping ${webpackOutputPath}`);
     }
 
     return BbPromise.resolve();
diff --git a/lib/compile.js b/lib/compile.js
index e5c2363..683242f 100644
--- a/lib/compile.js
+++ b/lib/compile.js
@@ -5,44 +5,57 @@ const BbPromise = require('bluebird');
 const webpack = require('webpack');
 const tty = require('tty');
 
-module.exports = {
-  compile() {
-    this.serverless.cli.log('Bundling with Webpack...');
+const defaultStatsConfig = {
+  colors: tty.isatty(process.stdout.fd),
+  hash: false,
+  version: false,
+  chunks: false,
+  children: false
+};
 
-    const compiler = webpack(this.webpackConfig);
-
-    return BbPromise.fromCallback(cb => compiler.run(cb)).then(stats => {
-      if (!this.multiCompile) {
-        stats = { stats: [stats] };
-      }
-
-      const compileOutputPaths = [];
-      const consoleStats = this.webpackConfig.stats ||
-        _.get(this, 'webpackConfig[0].stats') || {
-        colors: tty.isatty(process.stdout.fd),
-        hash: false,
-        version: false,
-        chunks: false,
-        children: false
-      };
-
-      _.forEach(stats.stats, compileStats => {
-        const statsOutput = compileStats.toString(consoleStats);
-        if (statsOutput) {
-          this.serverless.cli.consoleLog(statsOutput);
+function ensureArray(obj) {
+  return _.isArray(obj) ? obj : [obj];
+}
+
+function getStatsLogger(statsConfig, consoleLog) {
+  return stats => consoleLog(stats.toString(statsConfig || defaultStatsConfig));
+}
+
+function webpackCompile(config, logStats) {
+  return BbPromise
+    .fromCallback(cb => webpack(config).run(cb))
+    .then(stats => {
+      // ensure stats in any array in the case of multiCompile
+      stats = stats.stats ? stats.stats : [stats];
+
+      _.forEach(stats, compileStats => {
+        logStats(compileStats);
+        if (compileStats.hasErrors()) {
+          throw new Error('Webpack compilation error, see stats above');
         }
+      });
 
-        if (compileStats.compilation.errors.length) {
-          throw new Error('Webpack compilation error, see above');
-        }
+      return stats;
+    });
+}
 
-        compileOutputPaths.push(compileStats.compilation.compiler.outputPath);
-      });
+function webpackCompileSerial(configs, logStats) {
+  return BbPromise
+    .mapSeries(configs, config => webpackCompile(config, logStats))
+    .then(stats => _.flatten(stats));
+}
 
-      this.compileOutputPaths = compileOutputPaths;
-      this.compileStats = stats;
+module.exports = {
+  compile() {
+    this.serverless.cli.log('Bundling with Webpack...');
 
-      return BbPromise.resolve();
-    });
+    const configs = ensureArray(this.webpackConfig);
+    const logStats = getStatsLogger(configs[0].stats, this.serverless.cli.consoleLog);
+
+    return (this.serializedCompile ? webpackCompileSerial : webpackCompile)(configs, logStats)
+      .then(stats => {
+        this.compileStats = { stats };
+        return BbPromise.resolve();
+      });
   }
 };
diff --git a/lib/packExternalModules.js b/lib/packExternalModules.js
index cd762a3..8ae24e6 100644
--- a/lib/packExternalModules.js
+++ b/lib/packExternalModules.js
@@ -228,6 +228,10 @@ module.exports = {
    * and performance.
    */
   packExternalModules() {
+    if (this.skipCompile) {
+      return BbPromise.resolve();
+    }
+
     const stats = this.compileStats;
 
     const includes = this.configuration.includeModules;
diff --git a/lib/packageModules.js b/lib/packageModules.js
index 422af38..040cd95 100644
--- a/lib/packageModules.js
+++ b/lib/packageModules.js
@@ -3,14 +3,16 @@
 const BbPromise = require('bluebird');
 const _ = require('lodash');
 const path = require('path');
-const archiver = require('archiver');
-const fs = require('fs');
+const bestzip = require('bestzip');
 const glob = require('glob');
 const semver = require('semver');
+const fs = require('fs');
 
 function setArtifactPath(funcName, func, artifactPath) {
   const version = this.serverless.getVersion();
 
+  this.options.verbose && this.serverless.cli.log(`Setting artifact for function '${funcName}' to '${artifactPath}'`);
+
   // Serverless changed the artifact path location in version 1.18
   if (semver.lt(version, '1.18.0')) {
     func.artifact = artifactPath;
@@ -24,58 +26,89 @@ function setArtifactPath(funcName, func, artifactPath) {
 }
 
 function zip(directory, name) {
-  const zip = archiver.create('zip');
-  // Create artifact in temp path and move it to the package path (if any) later
-  const artifactFilePath = path.join(this.serverless.config.servicePath, '.serverless', name);
-  this.serverless.utils.writeFileDir(artifactFilePath);
-
-  const output = fs.createWriteStream(artifactFilePath);
-
-  const files = glob.sync('**', {
+  // Check that files exist to be zipped
+  let files = glob.sync('**', {
     cwd: directory,
     dot: true,
     silent: true,
     follow: true
   });
 
+  if (this.configuration.excludeRegex) {
+    files = _.filter(files, f => f.match(this.configuration.excludeRegex) === null);
+  }
+
   if (_.isEmpty(files)) {
     const error = new this.serverless.classes.Error('Packaging: No files found');
     return BbPromise.reject(error);
   }
 
-  output.on('open', () => {
-    zip.pipe(output);
+  // Create artifact in temp path and move it to the package path (if any) later
+  // This allows us to persist the webpackOutputPath and re-use the compiled output
+  const artifactFilePath = path.join(this.webpackOutputPath, name);
+  this.serverless.utils.writeFileDir(artifactFilePath);
 
-    _.forEach(files, filePath => {
-      const fullPath = path.resolve(directory, filePath);
+  const cwd = directory;
+  const source = '*';
+  const destination = path.relative(cwd, artifactFilePath);
+  const zipArgs = {
+    source,
+    cwd,
+    destination
+  };
+  return new BbPromise((resolve, reject) => {
+    bestzip(zipArgs)
+      .then(() => {
+        resolve(artifactFilePath);
+        return null;
+      })
+      .catch(err => {
+        reject(err);
+      });
+  });
+}
 
-      const stats = fs.statSync(fullPath);
+function getArtifactLocations(name) {
+  const archiveName = `${name}.zip`;
 
-      if (!stats.isDirectory(fullPath)) {
-        zip.append(fs.readFileSync(fullPath), {
-          name: filePath,
-          mode: stats.mode,
-          date: new Date(0) // necessary to get the same hash when zipping the same content
-        });
-      }
-    });
+  const webpackArtifact = path.join(this.webpackOutputPath, archiveName);
+  const serverlessArtifact = path.join('.serverless', archiveName);
 
-    zip.finalize();
-  });
+  return { webpackArtifact, serverlessArtifact };
+}
 
-  return new BbPromise((resolve, reject) => {
-    output.on('close', () => resolve(artifactFilePath));
-    zip.on('error', err => reject(err));
-  });
+function copyArtifactByName(artifactName) {
+  const { webpackArtifact, serverlessArtifact } = getArtifactLocations.call(this, artifactName);
+
+  // Make sure the destination dir exists
+  this.serverless.utils.writeFileDir(serverlessArtifact);
+
+  fs.copyFileSync(webpackArtifact, serverlessArtifact);
+}
+
+function setServiceArtifactPath(artifactPath) {
+  _.set(this.serverless, 'service.package.artifact', artifactPath);
+}
+
+function isIndividialPackaging() {
+  return _.get(this.serverless, 'service.package.individually');
+}
+
+function getArtifactName(entryFunction) {
+  return `${entryFunction.funcName || this.serverless.service.getServiceObject().name}.zip`;
 }
 
 module.exports = {
   packageModules() {
+    if (this.skipCompile) {
+      return BbPromise.resolve();
+    }
+
     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`;
+      const filename = getArtifactName.call(this, entryFunction);
       const modulePath = compileStats.compilation.compiler.outputPath;
 
       const startZip = _.now();
@@ -87,37 +120,52 @@ module.exports = {
             this.serverless.cli.log(
               `Zip ${_.isEmpty(entryFunction) ? 'service' : 'function'}: ${modulePath} [${_.now() - startZip} ms]`
             )
-        )
-        .then(artifactPath => {
-          if (_.get(this.serverless, 'service.package.individually')) {
-            setArtifactPath.call(
-              this,
-              entryFunction.funcName,
-              entryFunction.func,
-              path.relative(this.serverless.config.servicePath, artifactPath)
-            );
-          }
-          return artifactPath;
-        });
-    }).then(artifacts => {
-      if (!_.get(this.serverless, 'service.package.individually') && !_.isEmpty(artifacts)) {
-        // Set the service artifact to all functions
-        const allFunctionNames = this.serverless.service.getAllFunctions();
-        _.forEach(allFunctionNames, funcName => {
-          const func = this.serverless.service.getFunction(funcName);
-          setArtifactPath.call(this, funcName, func, path.relative(this.serverless.config.servicePath, artifacts[0]));
-        });
-        // For Google set the service artifact path
-        if (_.get(this.serverless, 'service.provider.name') === 'google') {
-          _.set(
-            this.serverless,
-            'service.package.artifact',
-            path.relative(this.serverless.config.servicePath, artifacts[0])
-          );
-        }
-      }
+        );
+    });
+  },
+
+  copyExistingArtifacts() {
+    this.serverless.cli.log('Copying existing artifacts...');
+    const allFunctionNames = this.serverless.service.getAllFunctions();
 
-      return null;
+    // Copy artifacts to package location
+    if (isIndividialPackaging.call(this)) {
+      _.forEach(allFunctionNames, funcName => copyArtifactByName.call(this, funcName));
+    } else {
+      // Copy service packaged artifact
+      const serviceName = this.serverless.service.getServiceObject().name;
+      copyArtifactByName.call(this, serviceName);
+    }
+
+    _.forEach(allFunctionNames, funcName => {
+      const func = this.serverless.service.getFunction(funcName);
+
+      const archiveName = isIndividialPackaging.call(this) ? funcName : this.serverless.service.getServiceObject().name;
+
+      const { serverlessArtifact } = getArtifactLocations.call(this, archiveName);
+      setArtifactPath.call(this, funcName, func, serverlessArtifact);
     });
+
+    // Set artifact locations
+    if (isIndividialPackaging.call(this)) {
+      _.forEach(allFunctionNames, funcName => {
+        const func = this.serverless.service.getFunction(funcName);
+
+        const archiveName = funcName;
+
+        const { serverlessArtifact } = getArtifactLocations.call(this, archiveName);
+        setArtifactPath.call(this, funcName, func, serverlessArtifact);
+      });
+    } else {
+      const archiveName = this.serverless.service.getServiceObject().name;
+
+      const { serverlessArtifact } = getArtifactLocations.call(this, archiveName);
+
+      if (_.get(this.serverless, 'service.provider.name') === 'google') {
+        setServiceArtifactPath.call(this, serverlessArtifact);
+      }
+    }
+
+    return BbPromise.resolve();
   }
 };
diff --git a/lib/packagers/yarn.js b/lib/packagers/yarn.js
index d1dcd74..8895f5c 100644
--- a/lib/packagers/yarn.js
+++ b/lib/packagers/yarn.js
@@ -117,7 +117,7 @@ class Yarn {
 
   static install(cwd, packagerOptions) {
     const command = /^win/.test(process.platform) ? 'yarn.cmd' : 'yarn';
-    const args = [ 'install', '--frozen-lockfile', '--non-interactive' ];
+    const args = [ 'install', '--pure-lockfile', '--non-interactive' ];
 
     // Convert supported packagerOptions
     if (packagerOptions.ignoreScripts) {
diff --git a/lib/validate.js b/lib/validate.js
index 80c0eb7..80d8999 100644
--- a/lib/validate.js
+++ b/lib/validate.js
@@ -171,7 +171,9 @@ module.exports = {
         this.webpackConfig.output.path = path.join(this.serverless.config.servicePath, this.options.out);
       }
 
-      if (this.skipCompile) {
+      // Skip compilation with --no-build
+      if (this.options.build === false) {
+        this.skipCompile = true;
         this.serverless.cli.log('Skipping build and using existing compiled output');
         if (!fse.pathExistsSync(this.webpackConfig.output.path)) {
           return BbPromise.reject(new this.serverless.classes.Error('No compiled output found'));
@@ -187,8 +189,9 @@ module.exports = {
 
       // In case of individual packaging we have to create a separate config for each function
       if (_.has(this.serverless, 'service.package') && this.serverless.service.package.individually) {
-        this.options.verbose && this.serverless.cli.log('Using multi-compile (individual packaging)');
         this.multiCompile = true;
+        this.serializedCompile = this.configuration.serializedCompile;
+        this.options.verbose && this.serverless.cli.log(`Using ${this.serializedCompile ? 'serialized' : 'multi'}-compile (individual packaging)`);
 
         if (this.webpackConfig.entry && !_.isEqual(this.webpackConfig.entry, entries)) {
           return BbPromise.reject(
diff --git a/package-lock.json b/package-lock.json
index 76fa462..0e6e995 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1377,7 +1377,6 @@
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
       "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dev": true,
       "requires": {
         "color-convert": "^1.9.0"
       }
@@ -1419,17 +1418,17 @@
       }
     },
     "archiver": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.0.2.tgz",
-      "integrity": "sha512-Tq3yV/T4wxBsD2Wign8W9VQKhaUxzzRmjEiSoOK0SLqPgDP/N1TKdYyBeIEu56T4I9iO4fKTTR0mN9NWkBA0sg==",
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/archiver/-/archiver-4.0.2.tgz",
+      "integrity": "sha512-B9IZjlGwaxF33UN4oPbfBkyA4V1SxNLeIhR1qY8sRXSsbdUkEHrrOvwlYFPx+8uQeCe9M+FG6KgO+imDmQ79CQ==",
       "requires": {
         "archiver-utils": "^2.1.0",
         "async": "^3.2.0",
         "buffer-crc32": "^0.2.1",
+        "glob": "^7.1.6",
         "readable-stream": "^3.6.0",
-        "readdir-glob": "^1.0.0",
-        "tar-stream": "^2.1.4",
-        "zip-stream": "^4.0.0"
+        "tar-stream": "^2.1.2",
+        "zip-stream": "^3.0.1"
       }
     },
     "archiver-utils": {
@@ -1961,6 +1960,118 @@
         "tweetnacl": "^0.14.3"
       }
     },
+    "bestzip": {
+      "version": "2.1.7",
+      "resolved": "https://registry.npmjs.org/bestzip/-/bestzip-2.1.7.tgz",
+      "integrity": "sha512-Eg5ZP0Viw1beJydZLbW246oCUnvtKGi7DhcB6IlKxP03NaxKCGVhKJD/jY4MLFRINhepfVEhAhnlc/uIxc9dHA==",
+      "requires": {
+        "archiver": "^4.0.2",
+        "async": "^3.2.0",
+        "glob": "^7.1.3",
+        "which": "^1.3.1",
+        "yargs": "^13.2.4"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
+        },
+        "cliui": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+          "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+          "requires": {
+            "string-width": "^3.1.0",
+            "strip-ansi": "^5.2.0",
+            "wrap-ansi": "^5.1.0"
+          }
+        },
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "locate-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+          "requires": {
+            "p-locate": "^3.0.0",
+            "path-exists": "^3.0.0"
+          }
+        },
+        "p-limit": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+          "requires": {
+            "p-limit": "^2.0.0"
+          }
+        },
+        "p-try": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+          "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        },
+        "wrap-ansi": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+          "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+          "requires": {
+            "ansi-styles": "^3.2.0",
+            "string-width": "^3.0.0",
+            "strip-ansi": "^5.0.0"
+          }
+        },
+        "yargs": {
+          "version": "13.3.2",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+          "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+          "requires": {
+            "cliui": "^5.0.0",
+            "find-up": "^3.0.0",
+            "get-caller-file": "^2.0.1",
+            "require-directory": "^2.1.1",
+            "require-main-filename": "^2.0.0",
+            "set-blocking": "^2.0.0",
+            "string-width": "^3.0.0",
+            "which-module": "^2.0.0",
+            "y18n": "^4.0.0",
+            "yargs-parser": "^13.1.2"
+          }
+        }
+      }
+    },
     "better-assert": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
@@ -2225,8 +2336,7 @@
     "camelcase": {
       "version": "5.3.1",
       "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
-      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
-      "dev": true
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
     },
     "camelcase-keys": {
       "version": "6.0.0",
@@ -2586,7 +2696,6 @@
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
       "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
       "requires": {
         "color-name": "1.1.3"
       }
@@ -2594,8 +2703,7 @@
     "color-name": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
-      "dev": true
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
     },
     "color-string": {
       "version": "1.5.3",
@@ -2681,14 +2789,30 @@
       "dev": true
     },
     "compress-commons": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.0.1.tgz",
-      "integrity": "sha512-xZm9o6iikekkI0GnXCmAl3LQGZj5TBDj0zLowsqi7tJtEa3FMGSEcHcqrSJIrOAk1UG/NBbDn/F1q+MG/p/EsA==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-3.0.0.tgz",
+      "integrity": "sha512-FyDqr8TKX5/X0qo+aVfaZ+PVmNJHJeckFBlq8jZGSJOgnynhfifoyl24qaqdUdDIBe0EVTHByN6NAkqYvE/2Xg==",
       "requires": {
         "buffer-crc32": "^0.2.13",
-        "crc32-stream": "^4.0.0",
+        "crc32-stream": "^3.0.1",
         "normalize-path": "^3.0.0",
-        "readable-stream": "^3.6.0"
+        "readable-stream": "^2.3.7"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "2.3.7",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+          "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        }
       }
     },
     "concat-map": {
@@ -2808,9 +2932,9 @@
       }
     },
     "crc32-stream": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.0.tgz",
-      "integrity": "sha512-tyMw2IeUX6t9jhgXI6um0eKfWq4EIDpfv5m7GX4Jzp7eVelQ360xd8EPXJhp2mHwLQIkqlnMLjzqSZI3a+0wRw==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz",
+      "integrity": "sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==",
       "requires": {
         "crc": "^3.4.4",
         "readable-stream": "^3.4.0"
@@ -2882,8 +3006,7 @@
     "decamelize": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
-      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
-      "dev": true
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
     },
     "decode-uri-component": {
       "version": "0.2.0",
@@ -3421,8 +3544,7 @@
     "emoji-regex": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
-      "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
-      "dev": true
+      "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
     },
     "enabled": {
       "version": "1.0.2",
@@ -5020,8 +5142,7 @@
     "get-caller-file": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
-      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
-      "dev": true
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
     },
     "get-func-name": {
       "version": "2.0.0",
@@ -6100,8 +6221,7 @@
     "is-fullwidth-code-point": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
-      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
-      "dev": true
+      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
     },
     "is-glob": {
       "version": "4.0.1",
@@ -6256,8 +6376,7 @@
     "isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
-      "dev": true
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
     },
     "iso8601-duration": {
       "version": "1.2.0",
@@ -8513,8 +8632,7 @@
     "path-exists": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
-      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
-      "dev": true
+      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
     },
     "path-is-absolute": {
       "version": "1.0.1",
@@ -9455,14 +9573,6 @@
         "util-deprecate": "^1.0.1"
       }
     },
-    "readdir-glob": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.0.0.tgz",
-      "integrity": "sha512-km0DIcwQVZ1ZUhXhMWpF74/Wm5aFEd5/jDiVWF1Hkw2myPQovG8vCQ8+FQO2KXE9npQQvCnAMZhhWuUee4WcCQ==",
-      "requires": {
-        "minimatch": "^3.0.4"
-      }
-    },
     "readdirp": {
       "version": "3.4.0",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
@@ -9598,14 +9708,12 @@
     "require-directory": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
-      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
-      "dev": true
+      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
     },
     "require-main-filename": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
-      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
-      "dev": true
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
     },
     "require-relative": {
       "version": "0.8.7",
@@ -10078,8 +10186,7 @@
     "set-blocking": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
-      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
-      "dev": true
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
     },
     "set-immediate-shim": {
       "version": "1.0.1",
@@ -11524,7 +11631,6 @@
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
       "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-      "dev": true,
       "requires": {
         "isexe": "^2.0.0"
       }
@@ -11532,8 +11638,7 @@
     "which-module": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
-      "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
-      "dev": true
+      "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
     },
     "which-pm-runs": {
       "version": "1.0.0",
@@ -11764,8 +11869,7 @@
     "y18n": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
-      "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
-      "dev": true
+      "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
     },
     "yallist": {
       "version": "2.1.2",
@@ -11917,7 +12021,6 @@
       "version": "13.1.2",
       "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
       "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
-      "dev": true,
       "requires": {
         "camelcase": "^5.0.0",
         "decamelize": "^1.2.0"
@@ -12081,12 +12184,12 @@
       "optional": true
     },
     "zip-stream": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.0.2.tgz",
-      "integrity": "sha512-TGxB2g+1ur6MHkvM644DuZr8Uzyz0k0OYWtS3YlpfWBEmK4woaC2t3+pozEL3dBfIPmpgmClR5B2QRcMgGt22g==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-3.0.1.tgz",
+      "integrity": "sha512-r+JdDipt93ttDjsOVPU5zaq5bAyY+3H19bDrThkvuVxC0xMQzU1PJcS6D+KrP3u96gH9XLomcHPb+2skoDjulQ==",
       "requires": {
         "archiver-utils": "^2.1.0",
-        "compress-commons": "^4.0.0",
+        "compress-commons": "^3.0.0",
         "readable-stream": "^3.6.0"
       }
     }
diff --git a/package.json b/package.json
index e5d33f7..df91642 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
     "report-dir": "./coverage"
   },
   "dependencies": {
-    "archiver": "^5.0.2",
+    "bestzip": "^2.1.7",
     "bluebird": "^3.7.2",
     "fs-extra": "^9.0.1",
     "glob": "^7.1.6",


Test patch:
diff --git a/index.test.js b/index.test.js
index ab0a03b..585e8b7 100644
--- a/index.test.js
+++ b/index.test.js
@@ -133,6 +133,7 @@ describe('ServerlessWebpack', () => {
       sandbox.stub(slsw, 'watch').returns(BbPromise.resolve());
       sandbox.stub(slsw, 'wpwatch').returns(BbPromise.resolve());
       sandbox.stub(slsw, 'packExternalModules').returns(BbPromise.resolve());
+      sandbox.stub(slsw, 'copyExistingArtifacts').returns(BbPromise.resolve());
       sandbox.stub(slsw, 'prepareRun').returns(BbPromise.resolve());
       sandbox.stub(slsw, 'watchRun').returns(BbPromise.resolve());
       sandbox.stub(slsw, 'validate').returns(BbPromise.resolve());
@@ -145,6 +146,7 @@ describe('ServerlessWebpack', () => {
 
     beforeEach(() => {
       ServerlessWebpack.lib.webpack.isLocal = false;
+      slsw.skipCompile = false;
     });
 
     after(() => {
@@ -169,6 +171,20 @@ describe('ServerlessWebpack', () => {
                 return null;
               });
             });
+
+            it('should skip compile if requested', () => {
+              slsw.skipCompile = true;
+              return expect(slsw.hooks['before:package:createDeploymentArtifacts']()).to.be.fulfilled.then(() => {
+                expect(slsw.serverless.pluginManager.spawn).to.have.been.calledTwice;
+                expect(slsw.serverless.pluginManager.spawn.firstCall).to.have.been.calledWithExactly(
+                  'webpack:validate'
+                );
+                expect(slsw.serverless.pluginManager.spawn.secondCall).to.have.been.calledWithExactly(
+                  'webpack:package'
+                );
+                return null;
+              });
+            });
           }
         },
         {
@@ -237,7 +253,7 @@ describe('ServerlessWebpack', () => {
             });
 
             it('should skip compile if requested', () => {
-              slsw.options.build = false;
+              slsw.skipCompile = true;
               return expect(slsw.hooks['before:invoke:local:invoke']()).to.be.fulfilled.then(() => {
                 expect(slsw.serverless.pluginManager.spawn).to.have.been.calledOnce;
                 expect(slsw.serverless.pluginManager.spawn).to.have.been.calledWithExactly('webpack:validate');
@@ -361,6 +377,17 @@ describe('ServerlessWebpack', () => {
             });
           }
         },
+        {
+          name: 'webpack:package:copyExistingArtifacts',
+          test: () => {
+            it('should call copyExistingArtifacts', () => {
+              return expect(slsw.hooks['webpack:package:copyExistingArtifacts']()).to.be.fulfilled.then(() => {
+                expect(slsw.copyExistingArtifacts).to.have.been.calledOnce;
+                return null;
+              });
+            });
+          }
+        },
         {
           name: 'before:offline:start',
           test: () => {
diff --git a/lib/Configuration.test.js b/lib/Configuration.test.js
index b978eff..4324b4b 100644
--- a/lib/Configuration.test.js
+++ b/lib/Configuration.test.js
@@ -19,7 +19,8 @@ describe('Configuration', () => {
         packager: 'npm',
         packagerOptions: {},
         keepOutputDirectory: false,
-        config: null
+        config: null,
+        serializedCompile: false
       };
     });
 
@@ -68,7 +69,8 @@ describe('Configuration', () => {
         packager: 'npm',
         packagerOptions: {},
         keepOutputDirectory: false,
-        config: null
+        config: null,
+        serializedCompile: false
       });
     });
   });
@@ -88,7 +90,8 @@ describe('Configuration', () => {
         packager: 'npm',
         packagerOptions: {},
         keepOutputDirectory: false,
-        config: null
+        config: null,
+        serializedCompile: false
       });
     });
 
@@ -107,7 +110,8 @@ describe('Configuration', () => {
         packager: 'npm',
         packagerOptions: {},
         keepOutputDirectory: false,
-        config: null
+        config: null,
+        serializedCompile: false
       });
     });
   });
diff --git a/lib/packagers/yarn.test.js b/lib/packagers/yarn.test.js
index 52445ad..676aa3d 100644
--- a/lib/packagers/yarn.test.js
+++ b/lib/packagers/yarn.test.js
@@ -63,14 +63,14 @@ describe('yarn', () => {
     it('should transform yarn trees to npm dependencies', () => {
       const testYarnResult =
         '{"type":"activityStart","data":{"id":0}}\n' +
-        '{"type":"activityTick","data":{"id":0,"name":"archiver@^2.1.1"}}\n' +
+        '{"type":"activityTick","data":{"id":0,"name":"bestzip@^2.1.5"}}\n' +
         '{"type":"activityTick","data":{"id":0,"name":"bluebird@^3.5.1"}}\n' +
         '{"type":"activityTick","data":{"id":0,"name":"fs-extra@^4.0.3"}}\n' +
         '{"type":"activityTick","data":{"id":0,"name":"mkdirp@^0.5.1"}}\n' +
         '{"type":"activityTick","data":{"id":0,"name":"minimist@^0.0.8"}}\n' +
         '{"type":"activityTick","data":{"id":0,"name":"@sls/webpack@^1.0.0"}}\n' +
         '{"type":"tree","data":{"type":"list","trees":[' +
-        '{"name":"archiver@2.1.1","children":[],"hint":null,"color":"bold",' +
+        '{"name":"bestzip@2.1.5","children":[],"hint":null,"color":"bold",' +
         '"depth":0},{"name":"bluebird@3.5.1","children":[],"hint":null,"color":' +
         '"bold","depth":0},{"name":"fs-extra@4.0.3","children":[],"hint":null,' +
         '"color":"bold","depth":0},{"name":"mkdirp@0.5.1","children":[{"name":' +
@@ -80,8 +80,8 @@ describe('yarn', () => {
       const expectedResult = {
         problems: [],
         dependencies: {
-          archiver: {
-            version: '2.1.1',
+          bestzip: {
+            version: '2.1.5',
             dependencies: {}
           },
           bluebird: {
@@ -134,11 +134,11 @@ describe('yarn', () => {
       acorn@^2.1.0, acorn@^2.4.0:
         version "2.7.0"
         resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7"
-    
+
       acorn@^3.0.4:
         version "3.3.0"
         resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-      
+
       otherModule@file:../../otherModule/the-new-version:
         version "1.2.0"
 
@@ -162,7 +162,7 @@ describe('yarn', () => {
           request "^2.83.0"
           ulid "^0.1.0"
           uuid "^3.1.0"
-    
+
         acorn@^5.0.0, acorn@^5.5.0:
           version "5.5.3"
           resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
@@ -172,11 +172,11 @@ describe('yarn', () => {
       acorn@^2.1.0, acorn@^2.4.0:
         version "2.7.0"
         resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7"
-    
+
       acorn@^3.0.4:
         version "3.3.0"
         resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-      
+
       otherModule@file:../../project/../../otherModule/the-new-version:
         version "1.2.0"
 
@@ -200,7 +200,7 @@ describe('yarn', () => {
           request "^2.83.0"
           ulid "^0.1.0"
           uuid "^3.1.0"
-    
+
         acorn@^5.0.0, acorn@^5.5.0:
           version "5.5.3"
           resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
@@ -218,7 +218,7 @@ describe('yarn', () => {
         expect(Utils.spawnProcess).to.have.been.calledOnce;
         expect(Utils.spawnProcess).to.have.been.calledWithExactly(
           sinon.match(/^yarn/),
-          [ 'install', '--frozen-lockfile', '--non-interactive' ],
+          [ 'install', '--pure-lockfile', '--non-interactive' ],
           {
             cwd: 'myPath'
           }
@@ -234,7 +234,7 @@ describe('yarn', () => {
         expect(Utils.spawnProcess).to.have.been.calledOnce;
         expect(Utils.spawnProcess).to.have.been.calledWithExactly(
           sinon.match(/^yarn/),
-          [ 'install', '--frozen-lockfile', '--non-interactive', '--ignore-scripts' ],
+          [ 'install', '--pure-lockfile', '--non-interactive', '--ignore-scripts' ],
           {
             cwd: 'myPath'
           }
diff --git a/tests/cleanup.test.js b/tests/cleanup.test.js
index 4d9eda2..dec1889 100644
--- a/tests/cleanup.test.js
+++ b/tests/cleanup.test.js
@@ -14,7 +14,7 @@ const expect = chai.expect;
 
 const FseMock = sandbox => ({
   copy: sandbox.stub(),
-  removeSync: sandbox.stub()
+  remove: sandbox.stub()
 });
 
 describe('cleanup', () => {
@@ -47,6 +47,7 @@ describe('cleanup', () => {
     serverless = new Serverless();
     serverless.cli = {
       log: sandbox.stub(),
+      error: sandbox.stub(),
       consoleLog: sandbox.stub()
     };
     dirExistsSyncStub = sandbox.stub(serverless.utils, 'dirExistsSync');
@@ -54,9 +55,10 @@ describe('cleanup', () => {
     module = _.assign(
       {
         serverless,
-        options: {},
-        webpackOutputPath: 'my/Output/Path',
-        configuration: {}
+        options: {
+          verbose: true
+        },
+        webpackOutputPath: 'my/Output/Path'
       },
       baseModule
     );
@@ -65,42 +67,78 @@ describe('cleanup', () => {
   afterEach(() => {
     // This will reset the webpackMock too
     sandbox.restore();
+    fseMock.remove.reset();
+    serverless.cli.log.reset();
   });
 
   it('should remove output dir if it exists', () => {
     dirExistsSyncStub.returns(true);
-    fseMock.removeSync.reset();
+    fseMock.remove.resolves(true);
 
     return expect(module.cleanup()).to.be.fulfilled.then(() => {
       expect(dirExistsSyncStub).to.have.been.calledOnce;
       expect(dirExistsSyncStub).to.have.been.calledWith('my/Output/Path');
-      expect(fseMock.removeSync).to.have.been.calledOnce;
+      expect(fseMock.remove).to.have.been.calledOnce;
+      expect(serverless.cli.log).to.have.been.calledWith('Removing my/Output/Path done');
       return null;
     });
   });
 
-  it('should not call removeSync if output dir does not exists', () => {
+  it('should log nothing is verbose is false', () => {
+    dirExistsSyncStub.returns(true);
+    fseMock.remove.resolves(true);
+
+    module = _.assign(
+      {
+        serverless,
+        options: {
+          verbose: false
+        },
+        webpackOutputPath: 'my/Output/Path'
+      },
+      baseModule
+    );
+
+    return expect(module.cleanup()).to.be.fulfilled.then(() => {
+      expect(dirExistsSyncStub).to.have.been.calledOnce;
+      expect(dirExistsSyncStub).to.have.been.calledWith('my/Output/Path');
+      expect(fseMock.remove).to.have.been.calledOnce;
+      expect(serverless.cli.log).not.to.have.been.called;
+      return null;
+    });
+  });
+
+  it('should log an error if it occurs', () => {
+    dirExistsSyncStub.returns(true);
+    fseMock.remove.rejects('remove error');
+
+    return expect(module.cleanup()).to.be.fulfilled.then(() => {
+      expect(serverless.cli.log).to.have.been.calledWith('Error occurred while removing my/Output/Path: remove error');
+
+      return null;
+    });
+  });
+
+  it('should not call remove if output dir does not exists', () => {
     dirExistsSyncStub.returns(false);
-    fseMock.removeSync.reset();
 
     return expect(module.cleanup()).to.be.fulfilled.then(() => {
       expect(dirExistsSyncStub).to.have.been.calledOnce;
       expect(dirExistsSyncStub).to.have.been.calledWith('my/Output/Path');
-      expect(fseMock.removeSync).to.not.have.been.called;
+      expect(fseMock.remove).to.not.have.been.called;
       return null;
     });
   });
 
   it('should keep output dir if keepOutputDir = true', () => {
     dirExistsSyncStub.returns(true);
-    fseMock.removeSync.reset();
 
     const configuredModule = _.assign({}, module, {
-      configuration: { keepOutputDirectory: true }
+      keepOutputDirectory: true
     });
     return expect(configuredModule.cleanup()).to.be.fulfilled.then(() => {
       expect(dirExistsSyncStub).to.not.have.been.calledOnce;
-      expect(fseMock.removeSync).to.not.have.been.called;
+      expect(fseMock.remove).to.not.have.been.called;
       return null;
     });
   });
diff --git a/tests/compile.test.js b/tests/compile.test.js
index c0bcb67..58f6820 100644
--- a/tests/compile.test.js
+++ b/tests/compile.test.js
@@ -44,13 +44,10 @@ describe('compile', () => {
       consoleLog: sandbox.stub()
     };
 
-    module = _.assign(
-      {
-        serverless,
-        options: {}
-      },
-      baseModule
-    );
+    module = _.assign({
+      serverless,
+      options: {},
+    }, baseModule);
   });
 
   afterEach(() => {
@@ -65,8 +62,9 @@ describe('compile', () => {
   it('should compile with webpack from a context configuration', () => {
     const testWebpackConfig = 'testconfig';
     module.webpackConfig = testWebpackConfig;
-    return expect(module.compile()).to.be.fulfilled.then(() => {
-      expect(webpackMock).to.have.been.calledWith(testWebpackConfig);
+    return expect(module.compile()).to.be.fulfilled
+    .then(() => {
+      expect(webpackMock).to.have.been.calledWith([testWebpackConfig]);
       expect(webpackMock.compilerMock.run).to.have.been.calledOnce;
       return null;
     });
@@ -80,29 +78,58 @@ describe('compile', () => {
   });
 
   it('should work with multi compile', () => {
-    const testWebpackConfig = 'testconfig';
-    const multiStats = [
-      {
+    const testWebpackConfig = ['testconfig'];
+    const multiStats = {
+      stats: [{
         compilation: {
           errors: [],
           compiler: {
-            outputPath: 'statsMock-outputPath'
-          }
+            outputPath: 'statsMock-outputPath',
+          },
         },
-        toString: sandbox.stub().returns('testStats')
-      }
-    ];
+        toString: sandbox.stub().returns('testStats'),
+        hasErrors: _.constant(false)
+      }]
+    };
     module.webpackConfig = testWebpackConfig;
     module.multiCompile = true;
     webpackMock.compilerMock.run.reset();
     webpackMock.compilerMock.run.yields(null, multiStats);
-    return expect(module.compile()).to.be.fulfilled.then(() => {
+    return expect(module.compile()).to.be.fulfilled
+    .then(() => {
       expect(webpackMock).to.have.been.calledWith(testWebpackConfig);
       expect(webpackMock.compilerMock.run).to.have.been.calledOnce;
       return null;
     });
   });
 
+  it('should work with serialized compile', () => {
+    const testWebpackConfig = ['testconfig'];
+    const multiStats = {
+      stats: [{
+        compilation: {
+          errors: [],
+          compiler: {
+            outputPath: 'statsMock-outputPath',
+          },
+        },
+        toString: sandbox.stub().returns('testStats'),
+        hasErrors: _.constant(false)
+      }]
+    };
+    module.webpackConfig = testWebpackConfig;
+    module.multiCompile = true;
+    module.serializedCompile = true;
+    webpackMock.compilerMock.run.reset();
+    webpackMock.compilerMock.run.yields(null, multiStats);
+    return expect(module.compile()).to.be.fulfilled
+      .then(() => {
+        expect(webpackMock).to.have.been.calledWith(testWebpackConfig);
+        expect(webpackMock.compilerMock.run).to.have.been.calledOnce;
+        return null;
+      });
+  });
+
   it('should use correct stats option', () => {
     const testWebpackConfig = {
       stats: 'minimal'
@@ -114,23 +141,24 @@ describe('compile', () => {
           outputPath: 'statsMock-outputPath'
         }
       },
-      toString: sandbox.stub().returns('testStats')
+      toString: sandbox.stub().returns('testStats'),
+      hasErrors: _.constant(false)
     };
 
     module.webpackConfig = testWebpackConfig;
     webpackMock.compilerMock.run.reset();
     webpackMock.compilerMock.run.yields(null, mockStats);
-    return expect(module.compile())
-      .to.be.fulfilled.then(() => {
-        expect(webpackMock).to.have.been.calledWith(testWebpackConfig);
-        expect(mockStats.toString.firstCall.args).to.eql([testWebpackConfig.stats]);
-        module.webpackConfig = [testWebpackConfig];
-        return expect(module.compile()).to.be.fulfilled;
-      })
-      .then(() => {
-        expect(webpackMock).to.have.been.calledWith([testWebpackConfig]);
-        expect(mockStats.toString.args).to.eql([ [testWebpackConfig.stats], [testWebpackConfig.stats] ]);
-        return null;
-      });
+    return (expect(module.compile()).to.be.fulfilled)
+    .then(() => {
+      expect(webpackMock).to.have.been.calledWith([testWebpackConfig]);
+      expect(mockStats.toString.firstCall.args).to.eql([testWebpackConfig.stats]);
+      module.webpackConfig = [testWebpackConfig];
+      return (expect(module.compile()).to.be.fulfilled);
+    })
+    .then(() => {
+      expect(webpackMock).to.have.been.calledWith([testWebpackConfig]);
+      expect(mockStats.toString.args).to.eql([ [testWebpackConfig.stats], [testWebpackConfig.stats] ]);
+      return null;
+    });
   });
 });
diff --git a/tests/mocks/archiver.mock.js b/tests/mocks/archiver.mock.js
deleted file mode 100644
index 1726b01..0000000
--- a/tests/mocks/archiver.mock.js
+++ /dev/null
@@ -1,23 +0,0 @@
-'use strict';
-
-/**
- * Mock object for glob
- */
-
-const sinon = require('sinon');
-
-const ZipMock = sandbox => ({
-  pipe: sandbox.stub(),
-  append: sandbox.stub(),
-  finalize: sandbox.stub(),
-  on: sandbox.stub()
-});
-
-module.exports.create = sandbox => {
-  const zipMock = ZipMock(sandbox);
-  const mock = {
-    create: sinon.stub().returns(zipMock),
-    _zipMock: zipMock
-  };
-  return mock;
-};
diff --git a/tests/mocks/bestzip.mock.js b/tests/mocks/bestzip.mock.js
new file mode 100644
index 0000000..99b9886
--- /dev/null
+++ b/tests/mocks/bestzip.mock.js
@@ -0,0 +1,15 @@
+'use strict';
+
+/**
+ * Mock object for bestzip
+ */
+
+const sinon = require('sinon');
+
+const BestZipMock = sandbox => sandbox.stub();
+
+module.exports.create = sandbox => {
+  const bestzipMock = BestZipMock(sandbox);
+  const mock = sinon.stub().resolves(bestzipMock);
+  return mock;
+};
diff --git a/tests/mocks/fs.mock.js b/tests/mocks/fs.mock.js
index 8590528..a9e06ce 100644
--- a/tests/mocks/fs.mock.js
+++ b/tests/mocks/fs.mock.js
@@ -23,6 +23,7 @@ module.exports.create = sandbox => {
     readFileSync: sandbox.stub(),
     statSync: sinon.stub().returns(statMock), // Persistent stub
     writeFileSync: sandbox.stub(),
+    copyFileSync: sandbox.stub(),
 
     _streamMock: streamMock,
     _statMock: statMock
diff --git a/tests/mocks/package.mock.json b/tests/mocks/package.mock.json
index 11835da..27a50f9 100644
--- a/tests/mocks/package.mock.json
+++ b/tests/mocks/package.mock.json
@@ -1,6 +1,6 @@
 {
   "dependencies": {
-    "archiver": "^2.0.0",
+    "bestzip": "^2.1.5",
     "bluebird": "^3.4.0",
     "fs-extra": "^0.26.7",
     "glob": "^7.1.2",
diff --git a/tests/mocks/packageIgnoredDevDeps.mock.json b/tests/mocks/packageIgnoredDevDeps.mock.json
index fa6ef53..304b3c3 100644
--- a/tests/mocks/packageIgnoredDevDeps.mock.json
+++ b/tests/mocks/packageIgnoredDevDeps.mock.json
@@ -1,6 +1,6 @@
 {
   "dependencies": {
-    "archiver": "^2.0.0",
+    "bestzip": "^2.1.5",
     "bluebird": "^3.4.0",
     "fs-extra": "^0.26.7",
     "glob": "^7.1.2",
diff --git a/tests/mocks/packageLocalRef.mock.json b/tests/mocks/packageLocalRef.mock.json
index ea120d9..cbc30f3 100644
--- a/tests/mocks/packageLocalRef.mock.json
+++ b/tests/mocks/packageLocalRef.mock.json
@@ -1,6 +1,6 @@
 {
   "dependencies": {
-    "archiver": "^2.0.0",
+    "bestzip": "^2.1.5",
     "bluebird": "^3.4.0",
     "fs-extra": "^0.26.7",
     "glob": "^7.1.2",
diff --git a/tests/packExternalModules.test.js b/tests/packExternalModules.test.js
index b1abd43..236ea59 100644
--- a/tests/packExternalModules.test.js
+++ b/tests/packExternalModules.test.js
@@ -334,6 +334,24 @@ describe('packExternalModules', () => {
       );
     });
 
+    it('should do nothing if skipCompile is true', () => {
+      module.configuration = new Configuration({
+        webpack: {
+          includeModules: {
+            packagePath: path.join('locals', 'package.json')
+          }
+        }
+      });
+      module.skipCompile = true;
+      return expect(module.packExternalModules()).to.be.fulfilled.then(() =>
+        BbPromise.all([
+          expect(fsExtraMock.copy).to.not.have.been.called,
+          expect(packagerFactoryMock.get).to.not.have.been.called,
+          expect(writeFileSyncStub).to.not.have.been.called
+        ])
+      );
+    });
+
     it('should copy needed package sections if available', () => {
       const originalPackageJSON = {
         name: 'test-service',
diff --git a/tests/packageModules.test.js b/tests/packageModules.test.js
index 0d5665b..d9c0329 100644
--- a/tests/packageModules.test.js
+++ b/tests/packageModules.test.js
@@ -7,11 +7,12 @@ const path = require('path');
 const sinon = require('sinon');
 const mockery = require('mockery');
 const Serverless = require('serverless');
+const Configuration = require('../lib/Configuration');
 
 // Mocks
 const fsMockFactory = require('./mocks/fs.mock');
 const globMockFactory = require('./mocks/glob.mock');
-const archiverMockFactory = require('./mocks/archiver.mock');
+const bestzipMockFactory = require('./mocks/bestzip.mock');
 
 chai.use(require('chai-as-promised'));
 chai.use(require('sinon-chai'));
@@ -27,7 +28,7 @@ describe('packageModules', () => {
   // Mocks
   let fsMock;
   let globMock;
-  let archiverMock;
+  let bestzipMock;
   // Serverless stubs
   let writeFileDirStub;
   let getAllFunctionsStub;
@@ -40,11 +41,11 @@ describe('packageModules', () => {
     sandbox.usingPromise(BbPromise);
 
     fsMock = fsMockFactory.create(sandbox);
-    archiverMock = archiverMockFactory.create(sandbox);
+    bestzipMock = bestzipMockFactory.create(sandbox);
     globMock = globMockFactory.create(sandbox);
 
     mockery.enable({ warnOnUnregistered: false });
-    mockery.registerMock('archiver', archiverMock);
+    mockery.registerMock('bestzip', bestzipMock);
     mockery.registerMock('fs', fsMock);
     mockery.registerMock('glob', globMock);
     baseModule = require('../lib/packageModules');
@@ -72,7 +73,9 @@ describe('packageModules', () => {
     module = _.assign(
       {
         serverless,
-        options: {}
+        options: {},
+        webpackOutputPath: '.webpack',
+        configuration: new Configuration()
       },
       baseModule
     );
@@ -89,7 +92,19 @@ describe('packageModules', () => {
       module.compileStats = { stats: [] };
       return expect(module.packageModules()).to.be.fulfilled.then(() =>
         BbPromise.all([
-          expect(archiverMock.create).to.not.have.been.called,
+          expect(bestzipMock).to.not.have.been.called,
+          expect(writeFileDirStub).to.not.have.been.called,
+          expect(fsMock.createWriteStream).to.not.have.been.called,
+          expect(globMock.sync).to.not.have.been.called
+        ])
+      );
+    });
+
+    it('should do nothing if skipCompile is true', () => {
+      module.skipCompile = true;
+      return expect(module.packageModules()).to.be.fulfilled.then(() =>
+        BbPromise.all([
+          expect(bestzipMock).to.not.have.been.called,
           expect(writeFileDirStub).to.not.have.been.called,
           expect(fsMock.createWriteStream).to.not.have.been.called,
           expect(globMock.sync).to.not.have.been.called
@@ -142,15 +157,9 @@ describe('packageModules', () => {
         fsMock._streamMock.on.withArgs('close').yields();
         fsMock._statMock.isDirectory.returns(false);
 
-        const expectedArtifactPath = path.join('.serverless', 'test-service.zip');
-
         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)
-          ])
-        );
+
+        return expect(module.packageModules()).to.be.fulfilled.then(() => BbPromise.all([]));
       });
 
       describe('with the Google provider', () => {
@@ -207,12 +216,8 @@ describe('packageModules', () => {
           fsMock._streamMock.on.withArgs('close').yields();
           fsMock._statMock.isDirectory.returns(false);
 
-          const expectedArtifactPath = path.join('.serverless', 'test-service.zip');
-
           module.compileStats = stats;
-          return expect(module.packageModules()).to.be.fulfilled.then(() =>
-            expect(serverless.service).to.have.a.nested.property('package.artifact').that.equals(expectedArtifactPath)
-          );
+          return expect(module.packageModules()).to.be.fulfilled;
         });
       });
 
@@ -253,28 +258,14 @@ describe('packageModules', () => {
         fsMock._streamMock.on.withArgs('close').yields();
         fsMock._statMock.isDirectory.returns(false);
 
-        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()).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)
-            ])
-          );
+          return expect(module.packageModules()).to.be.fulfilled.then(() => BbPromise.all([]));
         }).then(() =>
           BbPromise.each([ '1.17.0', '1.16.0-alpha', '1.15.3' ], version => {
             getVersionStub.returns(version);
-            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),
-                expect(func1).to.have.a.nested.property('package.disable').that.is.true,
-                expect(func2).to.have.a.nested.property('package.disable').that.is.true
-              ])
-            );
+            return expect(module.packageModules()).to.be.fulfilled.then(() => BbPromise.all([]));
           })
         );
       });
@@ -320,6 +311,54 @@ describe('packageModules', () => {
         module.compileStats = stats;
         return expect(module.packageModules()).to.be.rejectedWith('Packaging: No files found');
       });
+
+      it('should reject if no files are found because all files are excluded using regex', () => {
+        module.configuration = new Configuration({
+          webpack: {
+            excludeRegex: /.*/
+          }
+        });
+
+        // Test data
+        const stats = {
+          stats: [
+            {
+              compilation: {
+                compiler: {
+                  outputPath: '/my/Service/Path/.webpack/service'
+                }
+              }
+            }
+          ]
+        };
+        const files = [ 'README.md', 'src/handler1.js', 'src/handler1.js.map', 'src/handler2.js', 'src/handler2.js.map' ];
+        const allFunctions = [ 'func1', 'func2' ];
+        const func1 = {
+          handler: 'src/handler1',
+          events: []
+        };
+        const func2 = {
+          handler: 'src/handler2',
+          events: []
+        };
+        // Serverless behavior
+        sandbox.stub(serverless.config, 'servicePath').value('/my/Service/Path');
+        getVersionStub.returns('1.18.0');
+        getServiceObjectStub.returns({
+          name: 'test-service'
+        });
+        getAllFunctionsStub.returns(allFunctions);
+        getFunctionStub.withArgs('func1').returns(func1);
+        getFunctionStub.withArgs('func2').returns(func2);
+        // Mock behavior
+        globMock.sync.returns(files);
+        fsMock._streamMock.on.withArgs('open').yields();
+        fsMock._streamMock.on.withArgs('close').yields();
+        fsMock._statMock.isDirectory.returns(false);
+
+        module.compileStats = stats;
+        return expect(module.packageModules()).to.be.rejectedWith('Packaging: No files found');
+      });
     });
 
     describe('with individual packaging', () => {
@@ -388,14 +427,232 @@ describe('packageModules', () => {
         fsMock._statMock.isDirectory.returns(false);
 
         module.compileStats = stats;
-        return expect(module.packageModules()).to.be.fulfilled.then(() =>
+
+        return expect(module.packageModules()).to.be.fulfilled;
+      });
+    });
+  });
+
+  describe('copyExistingArtifacts()', () => {
+    const allFunctions = [ 'func1', 'func2' ];
+    const func1 = {
+      handler: 'src/handler1',
+      events: []
+    };
+    const func2 = {
+      handler: 'src/handler2',
+      events: []
+    };
+
+    const entryFunctions = [
+      {
+        handlerFile: 'src/handler1.js',
+        funcName: 'func1',
+        func: func1
+      },
+      {
+        handlerFile: 'src/handler2.js',
+        funcName: 'func2',
+        func: func2
+      }
+    ];
+
+    describe('with service packaging', () => {
+      afterEach(() => {
+        fsMock.copyFileSync.resetHistory();
+      });
+
+      beforeEach(() => {
+        _.set(module, 'entryFunctions', entryFunctions);
+        _.set(serverless.service.package, 'individually', false);
+        getVersionStub.returns('1.18.0');
+        getServiceObjectStub.returns({
+          name: 'test-service'
+        });
+        getAllFunctionsStub.returns(allFunctions);
+        getFunctionStub.withArgs('func1').returns(func1);
+        getFunctionStub.withArgs('func2').returns(func2);
+      });
+
+      it('copies the artifact', () => {
+        const expectedArtifactSource = path.join('.webpack', 'test-service.zip');
+        const expectedArtifactDestination = path.join('.serverless', 'test-service.zip');
+
+        return expect(module.copyExistingArtifacts()).to.be.fulfilled.then(() =>
           BbPromise.all([
+            // Should copy the artifact into .serverless
+            expect(fsMock.copyFileSync).callCount(1),
+            expect(fsMock.copyFileSync).to.be.calledWith(expectedArtifactSource, expectedArtifactDestination),
+
+            // Should set package artifact for each function to the single artifact
+            expect(func1)
+              .to.have.a.nested.property('package.artifact')
+              .that.equals(expectedArtifactDestination),
+            expect(func2)
+              .to.have.a.nested.property('package.artifact')
+              .that.equals(expectedArtifactDestination)
+          ])
+        );
+      });
+
+      it('should set the function artifact depending on the serverless version', () => {
+        // Test data
+        const stats = {
+          stats: [
+            {
+              compilation: {
+                compiler: {
+                  outputPath: '/my/Service/Path/.webpack/service'
+                }
+              }
+            }
+          ]
+        };
+        const files = [ 'README.md', 'src/handler1.js', 'src/handler1.js.map', 'src/handler2.js', 'src/handler2.js.map' ];
+        const allFunctions = [ 'func1', 'func2' ];
+        const func1 = {
+          handler: 'src/handler1',
+          events: []
+        };
+        const func2 = {
+          handler: 'src/handler2',
+          events: []
+        };
+        // Serverless behavior
+        sandbox.stub(serverless.config, 'servicePath').value('/my/Service/Path');
+        getServiceObjectStub.returns({
+          name: 'test-service'
+        });
+        getAllFunctionsStub.returns(allFunctions);
+        getFunctionStub.withArgs('func1').returns(func1);
+        getFunctionStub.withArgs('func2').returns(func2);
+        // Mock behavior
+        globMock.sync.returns(files);
+        fsMock._streamMock.on.withArgs('open').yields();
+        fsMock._streamMock.on.withArgs('close').yields();
+        fsMock._statMock.isDirectory.returns(false);
+
+        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.copyExistingArtifacts()).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)
+            ])
+          );
+        }).then(() =>
+          BbPromise.each([ '1.17.0', '1.16.0-alpha', '1.15.3' ], version => {
+            getVersionStub.returns(version);
+            return expect(module.copyExistingArtifacts()).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),
+                expect(func1).to.have.a.nested.property('package.disable').that.is.true,
+                expect(func2).to.have.a.nested.property('package.disable').that.is.true
+              ])
+            );
+          })
+        );
+      });
+
+      describe('with the Google provider', () => {
+        let oldProviderName;
+
+        beforeEach(() => {
+          oldProviderName = serverless.service.provider.name;
+          // Imitate Google provider
+          serverless.service.provider.name = 'google';
+        });
+
+        afterEach(() => {
+          if (oldProviderName) {
+            serverless.service.provider.name = oldProviderName;
+          } else {
+            _.unset(serverless.service.provider, 'name');
+          }
+        });
+
+        it('should set the service artifact path', () => {
+          // Test data
+          const allFunctions = [ 'func1', 'func2' ];
+          const func1 = {
+            handler: 'handler1',
+            events: []
+          };
+          const func2 = {
+            handler: 'handler2',
+            events: []
+          };
+          sandbox.stub(serverless.config, 'servicePath').value('/my/Service/Path');
+          getVersionStub.returns('1.18.0');
+          getServiceObjectStub.returns({
+            name: 'test-service'
+          });
+          getAllFunctionsStub.returns(allFunctions);
+          getFunctionStub.withArgs('func1').returns(func1);
+          getFunctionStub.withArgs('func2').returns(func2);
+          // Mock behavior
+          // fsMock._streamMock.on.withArgs('open').yields();
+          // fsMock._streamMock.on.withArgs('close').yields();
+          // fsMock._statMock.isDirectory.returns(false);
+
+          const expectedArtifactPath = path.join('.serverless', 'test-service.zip');
+
+          return expect(module.copyExistingArtifacts()).to.be.fulfilled.then(() =>
+            expect(serverless.service)
+              .to.have.a.nested.property('package.artifact')
+              .that.equals(expectedArtifactPath)
+          );
+        });
+      });
+    });
+
+    describe('with individual packaging', () => {
+      afterEach(() => {
+        fsMock.copyFileSync.resetHistory();
+      });
+
+      beforeEach(() => {
+        _.set(module, 'entryFunctions', entryFunctions);
+        _.set(serverless.service.package, 'individually', true);
+        getVersionStub.returns('1.18.0');
+        getServiceObjectStub.returns({
+          name: 'test-service'
+        });
+        getAllFunctionsStub.returns(allFunctions);
+        getFunctionStub.withArgs('func1').returns(func1);
+        getFunctionStub.withArgs('func2').returns(func2);
+      });
+
+      it('copies each artifact', () => {
+        const expectedFunc1Destination = path.join('.serverless', 'func1.zip');
+        const expectedFunc2Destination = path.join('.serverless', 'func2.zip');
+
+        return expect(module.copyExistingArtifacts()).to.be.fulfilled.then(() =>
+          BbPromise.all([
+            // Should copy an artifact per function into .serverless
+            expect(fsMock.copyFileSync).callCount(2),
+            expect(fsMock.copyFileSync).to.be.calledWith(path.join('.webpack', 'func1.zip'), expectedFunc1Destination),
+            expect(fsMock.copyFileSync).to.be.calledWith(path.join('.webpack', 'func2.zip'), expectedFunc2Destination),
+
+            // Should set package artifact locations
             expect(func1)
               .to.have.a.nested.property('package.artifact')
-              .that.equals(path.join('.serverless', 'func1.zip')),
+              .that.equals(expectedFunc1Destination),
             expect(func2)
               .to.have.a.nested.property('package.artifact')
-              .that.equals(path.join('.serverless', 'func2.zip'))
+              .that.equals(expectedFunc2Destination)
           ])
         );
       });
diff --git a/tests/validate.test.js b/tests/validate.test.js
index 12c2ae8..24c513a 100644
--- a/tests/validate.test.js
+++ b/tests/validate.test.js
@@ -960,6 +960,24 @@ describe('validate', () => {
   });
 
   describe('with skipped builds', () => {
+    it('should set `skipComile` to true if `options.build` is false', () => {
+      const testConfig = {
+        entry: 'test',
+        output: {}
+      };
+      const testServicePath = 'testpath';
+      module.serverless.config.servicePath = testServicePath;
+      _.set(module.serverless.service, 'custom.webpack.config', testConfig);
+
+      module.options.build = false;
+
+      fsExtraMock.pathExistsSync.returns(true);
+      return module.validate().then(() => {
+        expect(module.skipCompile).to.be.true;
+        return null;
+      });
+    });
+
     it('should keep output directory', () => {
       const testConfig = {
         entry: 'test',
@@ -968,7 +986,7 @@ describe('validate', () => {
       const testServicePath = 'testpath';
       module.serverless.config.servicePath = testServicePath;
       _.set(module.serverless.service, 'custom.webpack.config', testConfig);
-      module.skipCompile = true;
+      module.options.build = false;
       fsExtraMock.pathExistsSync.returns(true);
       return module.validate().then(() => {
         expect(module.keepOutputDirectory).to.be.true;
@@ -984,7 +1002,7 @@ describe('validate', () => {
       const testServicePath = 'testpath';
       module.serverless.config.servicePath = testServicePath;
       _.set(module.serverless.service, 'custom.webpack.config', testConfig);
-      module.skipCompile = true;
+      module.options.build = false;
       fsExtraMock.pathExistsSync.returns(false);
       return expect(module.validate()).to.be.rejectedWith(/No compiled output found/);
     });
diff --git a/tests/webpack.mock.js b/tests/webpack.mock.js
index abf911e..5bf7194 100644
--- a/tests/webpack.mock.js
+++ b/tests/webpack.mock.js
@@ -6,12 +6,16 @@ const StatsMock = () => ({
   compilation: {
     errors: [],
     compiler: {
-      outputPath: 'statsMock-outputPath'
-    }
+      outputPath: 'statsMock-outputPath',
+    },
+  },
+  toString: sinon.stub().returns('testStats'),
+  hasErrors() {
+    return Boolean(this.compilation.errors.length);
   },
-  toString: sinon.stub().returns('testStats')
 });
 
+
 const CompilerMock = (sandbox, statsMock) => ({
   run: sandbox.stub().yields(null, statsMock),
   watch: sandbox.stub().yields(null, statsMock),
@@ -19,7 +23,7 @@ const CompilerMock = (sandbox, statsMock) => ({
     beforeCompile: {
       tapPromise: sandbox.stub()
     }
-  }
+  },
 });
 
 const webpackMock = sandbox => {