Repository: serverless-heaven/serverless-webpack
Pull request #605 (2020-07-21 15:29:44)

Release/5.3.3
<!--
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:

Releasing 5.3.3

<!--
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:

<!--
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:

<!--
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



Patch:
diff --git a/.appveyor.yml b/.appveyor.yml
index 06d76b1..4b80523 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -1,10 +1,11 @@
 environment:
   matrix:
-    - NODEJS_VERSION: "10"
+    - nodejs_version: "10"
+    - nodejs_version: "12"
 
 install:
   # Get the version of Node.js
-  - ps: Install-Product node $Env:NODEJS_VERSION
+  - ps: Install-Product node $env:nodejs_version
   # install modules
   - npm install
   - node --version
diff --git a/.eslintrc b/.eslintrc.yml
similarity index 100%
rename from .eslintrc
rename to .eslintrc.yml
diff --git a/.travis.yml b/.travis.yml
index 198631a..9ce7663 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,12 +1,12 @@
+os: linux
+dist: xenial
 language: node_js
 
-matrix:
+jobs:
   include:
     - node_js: '10'
     - node_js: '12'
 
-sudo: false
-
 install:
   - travis_retry npm install
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..4cc3a83
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,162 @@
+# Change Log
+
+* 5.3.3
+  * default webpackConfig.node should be false #502
+  * Fix yarn list --json stdOut parsing. fixes #388 #516
+  * Updated release notes #585
+  * Updated Node versions in CI #587
+  * Remove optional peer dependencies #542
+  * Check for node runtimes first #579
+  * Bumps lodash from 4.17.15 to 4.17.19 #602
+
+* 5.3.2
+  * Fix eslint prettier #518
+  * Add TypeScript definition #520
+  * Project dependencies updated #524
+  * fix typescript example lockfile #526
+  * Typescript example babel loader #527
+  * Bump tar from 2.2.1 to 2.2.2 in /examples/babel #544
+  * Bump acorn from 6.3.0 to 6.4.1 in /examples/typescript #562
+  * Bump eslint-utils from 1.4.0 to 1.4.3 #567
+  * Bump handlebars from 4.1.2 to 4.7.6 #568
+  * Addressed npm security vulnerabilities #569
+  * Bump https-proxy-agent from 2.2.2 to 2.2.4 #572
+  * Bump https-proxy-agent from 2.2.2 to 2.2.4 in /examples/typescript #573
+  * Bump extend from 3.0.1 to 3.0.2 in /examples/babel #574
+  * Bump stringstream from 0.0.5 to 0.0.6 in /examples/babel #575
+  * Bump tough-cookie from 2.3.2 to 2.3.4 in /examples/babel #576
+  * Bump sshpk from 1.13.1 to 1.16.1 in /examples/babel #577
+  * Bump debug from 2.6.8 to 2.6.9 in /examples/babel #578
+
+* 5.3.1
+  * Fixed bug that prevented to use handlers using import [#505][link-505]
+  * Do not print empty lines in webpack stats [#499][link-499]
+  * Added git hooks to improved code quality and developer experience [#496][link-496]
+
+* 5.3.0
+  * Restore compatibility with TypeScript [#449][link-449] [#465][link-465]
+  * Allow glob for excludeFiles [#471][link-471]
+  * Support Webpack 5 [#472][link-472]
+  * Use colored output depending on tty [#480][link-480]
+  * Allow to keep webpack folder [#453][link-453] [#467][link-467]
+  * Add ability to exclude files from handler lookup [#433][link-433]
+  * Documentation fixes [#429][link-429]
+
+* 5.2.0
+  * Show info message in verbose mode if aws-sdk has been excluded automatically [#393][link-393]
+  * Added support for asynchronous webpack configuration [#412][link-412]
+  * Better error message if handlers are not found [#418][link-418]
+
+* 5.1.5
+  * Re-publish of 5.1.4 without yarn.lock
+
+* 5.1.4
+  * Fix support for Yarn resolutions definitions [#379][link-379]
+  * Better debugging for "Unable to import module ..." errors: Detect runtime dependencies that are only declared as devDependencies [#384][link-384]
+  * Documentation updates [#382][link-382]
+
+* 5.1.3
+  * Fixed issue with Yarn and file references as dependencies [#370][link-370]
+
+* 5.1.2
+  * Fixed issue that leads to `Unexpected end of JSON` in projects with lots of dependencies [#309][link-309][#373][link-373]
+  * Update webpack-4 example with VSCode debugging configuration [#365][link-365]
+
+* 5.1.1
+  * Fixed local invoke watch mode not executing changed files [#349][link-349]
+  * Added Webpack 4 example [#355][link-355]
+  * Documentation updates [#354][link-354]
+
+* 5.1.0
+  * Support Yarn [#286][link-286]
+  * Allow local invoke to use existing compiled output [#341][link-341] [#275][link-275]
+  * Support custom packager scripts [#343][link-343] [#342][link-342]
+
+* 5.0.0
+  * Support Webpack 4 [#331][link-331] [#328][link-328]
+  * BREAKING: Drop support for Webpack 2
+  * Allow to check for local invocation in the webpack configuration [#232][link-232]
+  * New centralized configuration with fallback to the old one [#336][link-336]
+  * Improved unit tests and actual coverage calculation [#337][link-337]
+
+* 4.4.0
+  * Support serverless-step-functions-offline [#313][link-313]
+  * Fixed webpack documentation links [#326][link-326]
+  * Abstracted packager interface [#329][link-329]
+
+* 4.3.0
+  * Add new `webpack:compile:watch:compile` event [#315][link-315]
+  * Added note to README about using yarn [#316][link-316]
+  * Made babel dynamic example the default babel example [#253][link-253]
+  * Documentation fixes [#317][link-317] [#321][link-321]
+
+* 4.2.0
+  * Support local file references in package.json [#263][link-263]
+  * Updated used tools (dev dependencies)
+
+* 4.1.0
+  * Prohibit manual entry configuration with individual packaging [#272][link-272]
+  * Fixed bug with stats in webpack config for individual packaging [#278][link-278]
+  * Fixed bug with startup order in combination with serverless-offline [#279][link-279]
+  * Default target to "node" if not set [#276][link-276]
+  * Support `serverless run` including watch mode [#269][link-269]
+
+* 4.0.0
+  * BREAKING: Expose lifecycle events for plugin authors [#254][link-254]
+  * Fixed deprecated hook warning [#126][link-126]
+  * Support forceExclude option for external modules [#247][link-247]
+  * Support stats output configuration in webpack config [#260][link-260]
+  * Google: Only integrate package.json but not node modules into artifact [#264][link-264]
+  * Documentation fixes and updates [#265][link-265]
+  * Updated examples [#250][link-250]
+
+* 3.1.2
+  * Fix issue where dependencies with dots in their names would not be installed [#251][link-251]
+
+* 3.1.1
+  * Fix issue where locked dependencies (package-lock.json) were ignored [#245][link-245]
+
+* 3.1.0
+  * Allow filesystem polling in watch mode (`--webpack-use-polling`) [#215][link-215]
+  * Allow forced include of not referenced modules [#217][link-217]
+  * Automatically include peer dependencies of used modules [#223][link-223]
+  * Show explicit message if the provided webpack config can not be loaded [#234][link-234]
+  * Improve examples [#227][link-227]
+  * Update 3rd party provider compatibility table [#221][link-221]
+  * Added automatic Travis and Coveralls builds to increase stability
+
+* 3.0.0
+  * Integrate with `serverless invoke local` [#151][link-151]
+  * Support watch mode with `serverless invoke local --watch`
+  * Stabilized and improved the bundling of node modules [#116][link-116], [#117][link-117]
+  * Improved interoperability with Serverless and 3rd party plugins [#173][link-173]
+  * Support individual packaging of the functions in a service [#120][link-120]
+  * Allow setting stdio max buffers for NPM operations [#185][link-185]
+  * Support bundling of node modules via node-externals whitelist [#186][link-186]
+  * Removed the `webpack serve` command in favor of [`serverless-offline`][link-serverless-offline] [#152][link-152]
+  * Updated examples [#179][link-179]
+  * Added missing unit tests to improve code stability
+  * Fixed unit tests to run on Windows [#145][link-145]
+
+* 2.2.2
+  * Reverted breaking change introduced in default output config [#202][link-202]
+
+* 2.2.1
+  * Restore functionality for Google provider [#193][link-193]
+
+* 2.2.0
+  * Allow full dynamic configurations [#158][link-158]
+  * Fix a bug that prevented the entries lib export to work with TypeScript [#165][link-165]
+
+* 2.1.0
+  * Added support for webpack configuration in TypeScript format [#129][link-129]
+  * Fixed bug with serverless-offline exec [#154][link-154]
+  * Added unit tests for cleanup. Updated test framework [#11][link-11]
+  * Support single function deploy and packaging [#107][link-107]
+  * Fixed path exception bug with individual packaging and SLS 1.18 [#159][link-159]
+
+* 2.0.0
+  * Support arbitrary Webpack versions as peer dependency [#83][link-83]
+  * Support `serverless offline start` invocation [#131][link-131]
+  * Documentation updates [#88][link-88], [#132][link-132], [#140][link-140], [#141][link-141], [#144][link-144]
+  * Print Webpack stats on recompile [#127][link-127]
diff --git a/README.md b/README.md
index 155b143..899c983 100644
--- a/README.md
+++ b/README.md
@@ -756,138 +756,7 @@ me to take it over and continue working on the project. That helped to revive it
 
 ## Release Notes
 
-* 5.3.1
-  * Fixed bug that prevented to use handlers using import [#505][link-505]
-  * Do not print empty lines in webpack stats [#499][link-499]
-  * Added git hooks to improved code quality and developer experience [#496][link-496]
-
-* 5.3.0
-  * Restore compatibility with TypeScript [#449][link-449] [#465][link-465]
-  * Allow glob for excludeFiles [#471][link-471]
-  * Support Webpack 5 [#472][link-472]
-  * Use colored output depending on tty [#480][link-480]
-  * Allow to keep webpack folder [#453][link-453] [#467][link-467]
-  * Add ability to exclude files from handler lookup [#433][link-433]
-  * Documentation fixes [#429][link-429]
-
-* 5.2.0
-  * Show info message in verbose mode if aws-sdk has been excluded automatically [#393][link-393]
-  * Added support for asynchronous webpack configuration [#412][link-412]
-  * Better error message if handlers are not found [#418][link-418]
-
-* 5.1.5
-  * Re-publish of 5.1.4 without yarn.lock
-
-* 5.1.4
-  * Fix support for Yarn resolutions definitions [#379][link-379]
-  * Better debugging for "Unable to import module ..." errors: Detect runtime dependencies that are only declared as devDependencies [#384][link-384]
-  * Documentation updates [#382][link-382]
-
-* 5.1.3
-  * Fixed issue with Yarn and file references as dependencies [#370][link-370]
-
-* 5.1.2
-  * Fixed issue that leads to `Unexpected end of JSON` in projects with lots of dependencies [#309][link-309][#373][link-373]
-  * Update webpack-4 example with VSCode debugging configuration [#365][link-365]
-
-* 5.1.1
-  * Fixed local invoke watch mode not executing changed files [#349][link-349]
-  * Added Webpack 4 example [#355][link-355]
-  * Documentation updates [#354][link-354]
-
-* 5.1.0
-  * Support Yarn [#286][link-286]
-  * Allow local invoke to use existing compiled output [#341][link-341] [#275][link-275]
-  * Support custom packager scripts [#343][link-343] [#342][link-342]
-
-* 5.0.0
-  * Support Webpack 4 [#331][link-331] [#328][link-328]
-  * BREAKING: Drop support for Webpack 2
-  * Allow to check for local invocation in the webpack configuration [#232][link-232]
-  * New centralized configuration with fallback to the old one [#336][link-336]
-  * Improved unit tests and actual coverage calculation [#337][link-337]
-
-* 4.4.0
-  * Support serverless-step-functions-offline [#313][link-313]
-  * Fixed webpack documentation links [#326][link-326]
-  * Abstracted packager interface [#329][link-329]
-
-* 4.3.0
-  * Add new `webpack:compile:watch:compile` event [#315][link-315]
-  * Added note to README about using yarn [#316][link-316]
-  * Made babel dynamic example the default babel example [#253][link-253]
-  * Documentation fixes [#317][link-317] [#321][link-321]
-
-* 4.2.0
-  * Support local file references in package.json [#263][link-263]
-  * Updated used tools (dev dependencies)
-
-* 4.1.0
-  * Prohibit manual entry configuration with individual packaging [#272][link-272]
-  * Fixed bug with stats in webpack config for individual packaging [#278][link-278]
-  * Fixed bug with startup order in combination with serverless-offline [#279][link-279]
-  * Default target to "node" if not set [#276][link-276]
-  * Support `serverless run` including watch mode [#269][link-269]
-
-* 4.0.0
-  * BREAKING: Expose lifecycle events for plugin authors [#254][link-254]
-  * Fixed deprecated hook warning [#126][link-126]
-  * Support forceExclude option for external modules [#247][link-247]
-  * Support stats output configuration in webpack config [#260][link-260]
-  * Google: Only integrate package.json but not node modules into artifact [#264][link-264]
-  * Documentation fixes and updates [#265][link-265]
-  * Updated examples [#250][link-250]
-
-* 3.1.2
-  * Fix issue where dependencies with dots in their names would not be installed [#251][link-251]
-
-* 3.1.1
-  * Fix issue where locked dependencies (package-lock.json) were ignored [#245][link-245]
-
-* 3.1.0
-  * Allow filesystem polling in watch mode (`--webpack-use-polling`) [#215][link-215]
-  * Allow forced include of not referenced modules [#217][link-217]
-  * Automatically include peer dependencies of used modules [#223][link-223]
-  * Show explicit message if the provided webpack config can not be loaded [#234][link-234]
-  * Improve examples [#227][link-227]
-  * Update 3rd party provider compatibility table [#221][link-221]
-  * Added automatic Travis and Coveralls builds to increase stability
-
-* 3.0.0
-  * Integrate with `serverless invoke local` [#151][link-151]
-  * Support watch mode with `serverless invoke local --watch`
-  * Stabilized and improved the bundling of node modules [#116][link-116], [#117][link-117]
-  * Improved interoperability with Serverless and 3rd party plugins [#173][link-173]
-  * Support individual packaging of the functions in a service [#120][link-120]
-  * Allow setting stdio max buffers for NPM operations [#185][link-185]
-  * Support bundling of node modules via node-externals whitelist [#186][link-186]
-  * Removed the `webpack serve` command in favor of [`serverless-offline`][link-serverless-offline] [#152][link-152]
-  * Updated examples [#179][link-179]
-  * Added missing unit tests to improve code stability
-  * Fixed unit tests to run on Windows [#145][link-145]
-
-* 2.2.2
-  * Reverted breaking change introduced in default output config [#202][link-202]
-
-* 2.2.1
-  * Restore functionality for Google provider [#193][link-193]
-
-* 2.2.0
-  * Allow full dynamic configurations [#158][link-158]
-  * Fix a bug that prevented the entries lib export to work with TypeScript [#165][link-165]
-
-* 2.1.0
-  * Added support for webpack configuration in TypeScript format [#129][link-129]
-  * Fixed bug with serverless-offline exec [#154][link-154]
-  * Added unit tests for cleanup. Updated test framework [#11][link-11]
-  * Support single function deploy and packaging [#107][link-107]
-  * Fixed path exception bug with individual packaging and SLS 1.18 [#159][link-159]
-
-* 2.0.0
-  * Support arbitrary Webpack versions as peer dependency [#83][link-83]
-  * Support `serverless offline start` invocation [#131][link-131]
-  * Documentation updates [#88][link-88], [#132][link-132], [#140][link-140], [#141][link-141], [#144][link-144]
-  * Print Webpack stats on recompile [#127][link-127]
+See [CHANGELOG.md](./CHANGELOG.md)
 
 [ico-serverless]: http://public.serverless.com/badges/v3.svg
 [ico-license]: https://img.shields.io/github/license/serverless-heaven/serverless-webpack.svg
diff --git a/lib/packExternalModules.js b/lib/packExternalModules.js
index 3e787a8..ac7b091 100644
--- a/lib/packExternalModules.js
+++ b/lib/packExternalModules.js
@@ -94,14 +94,32 @@ function getProdModules(externalModules, packagePath, dependencyGraph, forceExcl
         const peerDependencies = require(modulePackagePath).peerDependencies;
         if (!_.isEmpty(peerDependencies)) {
           this.options.verbose && this.serverless.cli.log(`Adding explicit peers for dependency ${module.external}`);
-          const peerModules = getProdModules.call(
-            this,
-            _.map(peerDependencies, (value, key) => ({ external: key })),
-            packagePath,
-            dependencyGraph,
-            forceExcludes
-          );
-          Array.prototype.push.apply(prodModules, peerModules);
+
+          const peerDependenciesMeta = require(modulePackagePath).peerDependenciesMeta;
+
+          if (!_.isEmpty(peerDependenciesMeta)) {
+            _.forEach(peerDependencies, (value, key) => {
+              if (peerDependenciesMeta[key] && peerDependenciesMeta[key].optional === true) {
+                this.options.verbose &&
+                  this.serverless.cli.log(
+                    `Skipping peers dependency ${key} for dependency ${module.external} because it's optional`
+                  );
+
+                _.unset(peerDependencies, key);
+              }
+            });
+          }
+
+          if (!_.isEmpty(peerDependencies)) {
+            const peerModules = getProdModules.call(
+              this,
+              _.map(peerDependencies, (value, key) => ({ external: key })),
+              packagePath,
+              dependencyGraph,
+              forceExcludes
+            );
+            Array.prototype.push.apply(prodModules, peerModules);
+          }
         }
       } catch (e) {
         this.serverless.cli.log(`WARNING: Could not check for peer dependencies of ${module.external}`);
@@ -424,3 +442,4 @@ module.exports = {
     });
   }
 };
+
diff --git a/lib/packagers/yarn.js b/lib/packagers/yarn.js
index 4b3721b..d1dcd74 100644
--- a/lib/packagers/yarn.js
+++ b/lib/packagers/yarn.js
@@ -62,7 +62,13 @@ class Yarn {
         return BbPromise.reject(err);
       })
       .then(processOutput => processOutput.stdout)
-      .then(depJson => BbPromise.try(() => JSON.parse(depJson)))
+      .then(stdout =>
+        BbPromise.try(() => {
+          const lines = Utils.splitLines(stdout);
+          const parsedLines = _.map(lines, Utils.safeJsonParse);
+          return _.find(parsedLines, line => line && line.type === 'tree');
+        })
+      )
       .then(parsedTree => {
         const convertTrees = trees =>
           _.reduce(
diff --git a/lib/utils.js b/lib/utils.js
index 7609265..5bb12ff 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -96,10 +96,24 @@ function spawnProcess(command, args, options) {
   });
 }
 
+function safeJsonParse(str) {
+  try {
+    return JSON.parse(str);
+  } catch (e) {
+    return null;
+  }
+}
+
+function splitLines(str) {
+  return _.split(str, /\r?\n/);
+}
+
 module.exports = {
   guid,
   purgeCache,
   searchAndProcessCache,
   SpawnError,
-  spawnProcess
+  spawnProcess,
+  safeJsonParse,
+  splitLines
 };
diff --git a/lib/validate.js b/lib/validate.js
index b26bce3..fcef521 100644
--- a/lib/validate.js
+++ b/lib/validate.js
@@ -96,8 +96,12 @@ module.exports = {
       _.merge(entries, entry);
     } else {
       _.forEach(functions, (func, index) => {
-        const entry = getEntryForFunction.call(this, functions[index], this.serverless.service.getFunction(func));
-        _.merge(entries, entry);
+        const loadedFunc = this.serverless.service.getFunction(func);
+        const runtime = loadedFunc.runtime || this.serverless.service.provider.runtime;
+        if (runtime.match(/^node/)) {
+          const entry = getEntryForFunction.call(this, functions[index], loadedFunc);
+          _.merge(entries, entry);
+        }
       });
     }
 
@@ -145,6 +149,11 @@ module.exports = {
           filename: '[name].js'
         };
       }
+      
+      // Default node
+      if (!this.webpackConfig.node || _.isEmpty(this.webpackConfig.node)) {
+        this.webpackConfig.node = false;
+      }
 
       // Custom output path
       if (this.options.out) {
diff --git a/package-lock.json b/package-lock.json
index 0e15660..adb55c0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4759,9 +4759,9 @@
       }
     },
     "lodash": {
-      "version": "4.17.15",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+      "version": "4.17.19",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
+      "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
     },
     "lodash.defaults": {
       "version": "4.2.0",
diff --git a/package.json b/package.json
index e2faea3..266672f 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "fs-extra": "^8.1.0",
     "glob": "^7.1.4",
     "is-builtin-module": "^3.0.0",
-    "lodash": "^4.17.15",
+    "lodash": "^4.17.19",
     "semver": "^6.2.0",
     "ts-node": "^8.3.0"
   },


Test patch:
diff --git a/lib/packagers/yarn.test.js b/lib/packagers/yarn.test.js
index 0e58d46..52445ad 100644
--- a/lib/packagers/yarn.test.js
+++ b/lib/packagers/yarn.test.js
@@ -61,14 +61,22 @@ describe('yarn', () => {
     });
 
     it('should transform yarn trees to npm dependencies', () => {
-      const testYarnResult = `{"type":"tree","data":{"type":"list","trees":[
-        {"name":"archiver@2.1.1","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":
-        "minimist@0.0.8","children":[],"hint":null,"color":"bold","depth":0}],
-        "hint":null,"color":null,"depth":0},{"name":"@sls/webpack@1.0.0", 
-        "children":[],"hint":null,"color":"bold","depth":0}]}}`;
+      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":"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",' +
+        '"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":' +
+        '"minimist@0.0.8","children":[],"hint":null,"color":"bold","depth":0}],' +
+        '"hint":null,"color":null,"depth":0},{"name":"@sls/webpack@1.0.0",' +
+        '"children":[],"hint":null,"color":"bold","depth":0}]}}\n';
       const expectedResult = {
         problems: [],
         dependencies: {
diff --git a/lib/utils.test.js b/lib/utils.test.js
index a0a6479..5d75167 100644
--- a/lib/utils.test.js
+++ b/lib/utils.test.js
@@ -118,4 +118,20 @@ describe('Utils', () => {
       return expect(Utils.spawnProcess('cmd', [])).to.be.rejectedWith(Utils.SpawnError);
     });
   });
+
+  describe('safeJsonParse', () => {
+    it('should parse valid JSON', () => {
+      expect(Utils.safeJsonParse('{"foo": "bar"}')).to.deep.equal({ foo: 'bar' });
+    });
+
+    it('should return null for invalid JSON', () => {
+      expect(Utils.safeJsonParse('{"foo":')).to.equal(null);
+    });
+  });
+
+  describe('splitLines', () => {
+    it('should split on new line characters', () => {
+      expect(Utils.splitLines('a\r\nb\nc')).to.deep.equal([ 'a', 'b', 'c' ]);
+    });
+  });
 });
diff --git a/tests/data/rp-package-optional.json b/tests/data/rp-package-optional.json
new file mode 100644
index 0000000..f193d5c
--- /dev/null
+++ b/tests/data/rp-package-optional.json
@@ -0,0 +1,94 @@
+{
+  "_from": "request-promise",
+  "_id": "request-promise@4.2.1",
+  "_inBundle": false,
+  "_integrity": "sha1-fuxWyJMXqCLL/qmbA5zlQ8LhX2c=",
+  "_location": "/request-promise",
+  "_phantomChildren": {},
+  "_requested": {
+    "type": "tag",
+    "registry": true,
+    "raw": "request-promise",
+    "name": "request-promise",
+    "escapedName": "request-promise",
+    "rawSpec": "",
+    "saveSpec": null,
+    "fetchSpec": "latest"
+  },
+  "_requiredBy": [
+    "#USER",
+    "/"
+  ],
+  "_resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.1.tgz",
+  "_shasum": "7eec56c89317a822cbfea99b039ce543c2e15f67",
+  "_spec": "request-promise",
+  "_where": "C:\\Projects\\serverless\\test\\babel-dynamically-entries",
+  "author": {
+    "name": "Nicolai Kamenzky",
+    "url": "https://github.com/analog-nico"
+  },
+  "bugs": {
+    "url": "https://github.com/request/request-promise/issues"
+  },
+  "bundleDependencies": false,
+  "dependencies": {
+    "bluebird": "^3.5.0",
+    "request-promise-core": "1.1.1",
+    "stealthy-require": "^1.1.0",
+    "tough-cookie": ">=2.3.0"
+  },
+  "deprecated": false,
+  "description": "The simplified HTTP request client 'request' with Promise support. Powered by Bluebird.",
+  "devDependencies": {
+    "body-parser": "~1.15.2",
+    "chai": "~3.5.0",
+    "chalk": "~1.1.3",
+    "gulp": "~3.9.1",
+    "gulp-coveralls": "~0.1.4",
+    "gulp-eslint": "~2.1.0",
+    "gulp-istanbul": "~1.0.0",
+    "gulp-mocha": "~2.2.0",
+    "lodash": "~4.13.1",
+    "publish-please": "~2.1.4",
+    "request": "^2.34.0",
+    "rimraf": "~2.5.3",
+    "run-sequence": "~1.2.2"
+  },
+  "engines": {
+    "node": ">=0.10.0"
+  },
+  "homepage": "https://github.com/request/request-promise#readme",
+  "keywords": [
+    "xhr",
+    "http",
+    "https",
+    "promise",
+    "request",
+    "then",
+    "thenable",
+    "bluebird"
+  ],
+  "license": "ISC",
+  "main": "./lib/rp.js",
+  "name": "request-promise",
+  "peerDependencies": {
+    "request": "^2.34",
+    "canvas": "^2.5.0"
+  },
+  "peerDependenciesMeta": {
+    "canvas": {
+      "optional": true
+    }
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/request/request-promise.git"
+  },
+  "scripts": {
+    "prepublish": "publish-please guard",
+    "publish-please": "publish-please",
+    "test": "gulp ci",
+    "test-publish": "gulp ci-no-cov"
+  },
+  "version": "4.2.1"
+}
diff --git a/tests/packExternalModules.test.js b/tests/packExternalModules.test.js
index 2fb9a88..b1abd43 100644
--- a/tests/packExternalModules.test.js
+++ b/tests/packExternalModules.test.js
@@ -1114,107 +1114,222 @@ describe('packExternalModules', () => {
     });
 
     describe('peer dependencies', () => {
-      before(() => {
-        const peerDepPackageJson = require('./data/package-peerdeps.json');
-        mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
-        mockery.registerMock(path.join(process.cwd(), 'package.json'), peerDepPackageJson);
-        // Mock request-promise package.json
-        const rpPackageJson = require('./data/rp-package.json');
-        const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json');
-        mockery.registerMock(rpPackagePath, rpPackageJson);
-      });
+      /**
+       * Both "default" & "optinal" behaviors are mostly equal.
+       * The only difference between each scenario is they don't use the same package.json as mock
+       */
+      describe('default behavior', () => {
+        before(() => {
+          const peerDepPackageJson = require('./data/package-peerdeps.json');
+          mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
+          mockery.registerMock(path.join(process.cwd(), 'package.json'), peerDepPackageJson);
+          // Mock request-promise package.json
+          const rpPackageJson = require('./data/rp-package.json');
+          const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json');
+          mockery.registerMock(rpPackagePath, rpPackageJson);
+        });
 
-      after(() => {
-        mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
-        mockery.registerMock(path.join(process.cwd(), 'package.json'), packageMock);
-        const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json');
-        mockery.deregisterMock(rpPackagePath);
-      });
+        after(() => {
+          mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
+          mockery.registerMock(path.join(process.cwd(), 'package.json'), packageMock);
+          const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json');
+          mockery.deregisterMock(rpPackagePath);
+        });
 
-      it('should install external peer dependencies', () => {
-        const expectedCompositePackageJSON = {
-          name: 'test-service',
-          version: '1.0.0',
-          description: 'Packaged externals for test-service',
-          private: true,
-          scripts: {},
-          dependencies: {
-            bluebird: '^3.5.0',
-            'request-promise': '^4.2.1',
-            request: '^2.82.0'
-          }
-        };
-        const expectedPackageJSON = {
-          name: 'test-service',
-          version: '1.0.0',
-          description: 'Packaged externals for test-service',
-          private: true,
-          scripts: {},
-          dependencies: {
-            bluebird: '^3.5.0',
-            'request-promise': '^4.2.1',
-            request: '^2.82.0'
-          }
-        };
+        it('should install external peer dependencies', () => {
+          const expectedCompositePackageJSON = {
+            name: 'test-service',
+            version: '1.0.0',
+            description: 'Packaged externals for test-service',
+            private: true,
+            scripts: {},
+            dependencies: {
+              bluebird: '^3.5.0',
+              'request-promise': '^4.2.1',
+              request: '^2.82.0'
+            }
+          };
+          const expectedPackageJSON = {
+            name: 'test-service',
+            version: '1.0.0',
+            description: 'Packaged externals for test-service',
+            private: true,
+            scripts: {},
+            dependencies: {
+              bluebird: '^3.5.0',
+              'request-promise': '^4.2.1',
+              request: '^2.82.0'
+            }
+          };
 
-        const dependencyGraph = require('./data/npm-ls-peerdeps.json');
-        const peerDepStats = {
-          stats: [
-            {
-              compilation: {
-                chunks: [
-                  new ChunkMock([
-                    {
-                      identifier: _.constant('"crypto"')
-                    },
-                    {
-                      identifier: _.constant('"uuid/v4"')
-                    },
-                    {
-                      identifier: _.constant('"mockery"')
-                    },
-                    {
-                      identifier: _.constant('"@scoped/vendor/module1"')
-                    },
-                    {
-                      identifier: _.constant('external "bluebird"')
-                    },
-                    {
-                      identifier: _.constant('external "request-promise"')
-                    }
-                  ])
-                ],
-                compiler: {
-                  outputPath: '/my/Service/Path/.webpack/service'
+          const dependencyGraph = require('./data/npm-ls-peerdeps.json');
+          const peerDepStats = {
+            stats: [
+              {
+                compilation: {
+                  chunks: [
+                    new ChunkMock([
+                      {
+                        identifier: _.constant('"crypto"')
+                      },
+                      {
+                        identifier: _.constant('"uuid/v4"')
+                      },
+                      {
+                        identifier: _.constant('"mockery"')
+                      },
+                      {
+                        identifier: _.constant('"@scoped/vendor/module1"')
+                      },
+                      {
+                        identifier: _.constant('external "bluebird"')
+                      },
+                      {
+                        identifier: _.constant('external "request-promise"')
+                      }
+                    ])
+                  ],
+                  compiler: {
+                    outputPath: '/my/Service/Path/.webpack/service'
+                  }
                 }
               }
+            ]
+          };
+
+          module.webpackOutputPath = 'outputPath';
+          fsExtraMock.pathExists.yields(null, false);
+          fsExtraMock.copy.yields();
+          packagerMock.getProdDependencies.returns(BbPromise.resolve(dependencyGraph));
+          packagerMock.install.returns(BbPromise.resolve());
+          packagerMock.prune.returns(BbPromise.resolve());
+          packagerMock.runScripts.returns(BbPromise.resolve());
+          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,
+              expect(writeFileSyncStub.firstCall.args[1]).to.equal(
+                JSON.stringify(expectedCompositePackageJSON, null, 2)
+              ),
+              expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
+              // The modules should have been copied
+              expect(fsExtraMock.copy).to.have.been.calledOnce,
+              // npm ls and npm prune should have been called
+              expect(packagerMock.getProdDependencies).to.have.been.calledOnce,
+              expect(packagerMock.install).to.have.been.calledOnce,
+              expect(packagerMock.prune).to.have.been.calledOnce,
+              expect(packagerMock.runScripts).to.have.been.calledOnce
+            ])
+          );
+        });
+      });
+
+      describe('optional behavior', () => {
+        before(() => {
+          const peerDepPackageJson = require('./data/package-peerdeps.json');
+          mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
+          mockery.registerMock(path.join(process.cwd(), 'package.json'), peerDepPackageJson);
+          // Mock request-promise package.json
+          const rpPackageJson = require('./data/rp-package-optional.json');
+          const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json');
+          mockery.registerMock(rpPackagePath, rpPackageJson);
+        });
+
+        after(() => {
+          mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
+          mockery.registerMock(path.join(process.cwd(), 'package.json'), packageMock);
+          const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json');
+          mockery.deregisterMock(rpPackagePath);
+        });
+
+        it('should skip optional peer dependencies', () => {
+          const expectedCompositePackageJSON = {
+            name: 'test-service',
+            version: '1.0.0',
+            description: 'Packaged externals for test-service',
+            private: true,
+            scripts: {},
+            dependencies: {
+              bluebird: '^3.5.0',
+              'request-promise': '^4.2.1',
+              request: '^2.82.0'
+            }
+          };
+          const expectedPackageJSON = {
+            name: 'test-service',
+            version: '1.0.0',
+            description: 'Packaged externals for test-service',
+            private: true,
+            scripts: {},
+            dependencies: {
+              bluebird: '^3.5.0',
+              'request-promise': '^4.2.1',
+              request: '^2.82.0'
             }
-          ]
-        };
+          };
 
-        module.webpackOutputPath = 'outputPath';
-        fsExtraMock.pathExists.yields(null, false);
-        fsExtraMock.copy.yields();
-        packagerMock.getProdDependencies.returns(BbPromise.resolve(dependencyGraph));
-        packagerMock.install.returns(BbPromise.resolve());
-        packagerMock.prune.returns(BbPromise.resolve());
-        packagerMock.runScripts.returns(BbPromise.resolve());
-        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,
-            expect(writeFileSyncStub.firstCall.args[1]).to.equal(JSON.stringify(expectedCompositePackageJSON, null, 2)),
-            expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
-            // The modules should have been copied
-            expect(fsExtraMock.copy).to.have.been.calledOnce,
-            // npm ls and npm prune should have been called
-            expect(packagerMock.getProdDependencies).to.have.been.calledOnce,
-            expect(packagerMock.install).to.have.been.calledOnce,
-            expect(packagerMock.prune).to.have.been.calledOnce,
-            expect(packagerMock.runScripts).to.have.been.calledOnce
-          ])
-        );
+          const dependencyGraph = require('./data/npm-ls-peerdeps.json');
+          const peerDepStats = {
+            stats: [
+              {
+                compilation: {
+                  chunks: [
+                    new ChunkMock([
+                      {
+                        identifier: _.constant('"crypto"')
+                      },
+                      {
+                        identifier: _.constant('"uuid/v4"')
+                      },
+                      {
+                        identifier: _.constant('"mockery"')
+                      },
+                      {
+                        identifier: _.constant('"@scoped/vendor/module1"')
+                      },
+                      {
+                        identifier: _.constant('external "bluebird"')
+                      },
+                      {
+                        identifier: _.constant('external "request-promise"')
+                      }
+                    ])
+                  ],
+                  compiler: {
+                    outputPath: '/my/Service/Path/.webpack/service'
+                  }
+                }
+              }
+            ]
+          };
+
+          module.webpackOutputPath = 'outputPath';
+          fsExtraMock.pathExists.yields(null, false);
+          fsExtraMock.copy.yields();
+          packagerMock.getProdDependencies.returns(BbPromise.resolve(dependencyGraph));
+          packagerMock.install.returns(BbPromise.resolve());
+          packagerMock.prune.returns(BbPromise.resolve());
+          packagerMock.runScripts.returns(BbPromise.resolve());
+          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,
+              expect(writeFileSyncStub.firstCall.args[1]).to.equal(
+                JSON.stringify(expectedCompositePackageJSON, null, 2)
+              ),
+              expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
+              // The modules should have been copied
+              expect(fsExtraMock.copy).to.have.been.calledOnce,
+              // npm ls and npm prune should have been called
+              expect(packagerMock.getProdDependencies).to.have.been.calledOnce,
+              expect(packagerMock.install).to.have.been.calledOnce,
+              expect(packagerMock.prune).to.have.been.calledOnce,
+              expect(packagerMock.runScripts).to.have.been.calledOnce
+            ])
+          );
+        });
       });
     });
   });
diff --git a/tests/validate.test.js b/tests/validate.test.js
index df62757..fe2563e 100644
--- a/tests/validate.test.js
+++ b/tests/validate.test.js
@@ -212,6 +212,21 @@ describe('validate', () => {
     });
   });
 
+  describe('default node', () => {
+    it('should turn NodeStuffPlugin and NodeSourcePlugin plugins off by default', () => {
+      const testEntry = 'testentry';
+      const testConfig = {
+        entry: testEntry,
+      };
+      const testServicePath = 'testpath';
+      module.serverless.config.servicePath = testServicePath;
+      _.set(module.serverless.service, 'custom.webpack.config', testConfig);
+      return module
+        .validate()
+        .then(() => expect(module.webpackConfig.node).to.eql(false));
+    });
+  });
+
   describe('config file load', () => {
     it('should load a webpack config from file if `custom.webpack` is a string', () => {
       const testConfig = 'testconfig';
@@ -407,7 +422,8 @@ describe('validate', () => {
                 path: 'func1path'
               }
             }
-          ]
+          ],
+          runtime: 'node10.x'
         },
         func2: {
           handler: 'module2.func2handler',
@@ -422,7 +438,8 @@ describe('validate', () => {
             {
               nonhttp: 'non-http'
             }
-          ]
+          ],
+          runtime: 'node10.x'
         },
         func3: {
           handler: 'handlers/func3/module2.func3handler',
@@ -431,7 +448,8 @@ describe('validate', () => {
             {
               nonhttp: 'non-http'
             }
-          ]
+          ],
+          runtime: 'node10.x'
         },
         func4: {
           handler: 'handlers/module2/func3/module2.func3handler',
@@ -440,7 +458,18 @@ describe('validate', () => {
             {
               nonhttp: 'non-http'
             }
-          ]
+          ],
+          runtime: 'node10.x'
+        },
+        func5: {
+          handler: 'com.serverless.Handler',
+          artifact: 'target/hello-dev.jar',
+          events: [
+            {
+              nonhttp: 'non-http'
+            }
+          ],
+          runtime: 'java8'
         }
       };
 
@@ -454,19 +483,24 @@ describe('validate', () => {
                 path: 'func1path'
               }
             }
-          ]
+          ],
+          runtime: 'node10.x'
         }
       };
 
-      it('should expose all functions if `options.function` is not defined', () => {
+      it('should expose all node functions if `options.function` is not defined', () => {
         const testOutPath = 'test';
         const testConfig = {
           entry: 'test',
           context: 'testcontext',
           output: {
             path: testOutPath
+          },
+          getFunction: func => {
+            return testFunctionsConfig[func];
           }
         };
+
         _.set(module.serverless.service, 'custom.webpack.config', testConfig);
         module.serverless.service.functions = testFunctionsConfig;
         globSyncStub.callsFake(filename => [_.replace(filename, '*', 'js')]);
@@ -879,3 +913,4 @@ describe('validate', () => {
     });
   });
 });
+