Repository: serverless-heaven/serverless-webpack (js)

Issue:
Packaging bug when using nodejs based custom runtime 
# This is a Bug

## Description

I am trying to have a custom runtime for one of my lambda function. However when I specify `runtime: provided` webpack is not packaging my function as expected. Instead is packaging the whole project including all the other functions.
If my runtime is not provided (just standard nodejs) then the packaging is correct.
Without webpack plugin it works fine as well.

I have a repro case here: https://github.com/MatteoGioioso/serverless-webpack-bug

## Additional Data

* ***Serverless-Webpack Version you're using***: 5.3.4
* ***Webpack version you're using***: 4.29.6
* ***Serverless Framework Version you're using***: 
```
Framework Core: 1.77.1
Plugin: 3.6.18
SDK: 2.3.1
Components: 2.33.2
```


## Webpack configuration

```

const path = require('path');
const nodeExternals = require('webpack-node-externals');
const slsw = require('serverless-webpack');

module.exports = {
  entry: slsw.lib.entries,
  target: 'node',
  devtool: 'source-map',

  externals: [nodeExternals()],
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
        include: __dirname,
        exclude: /node_modules/,
      },
    ],
  },
  output: {
    libraryTarget: 'commonjs2',
    path: path.join(__dirname, '.webpack'),
    filename: '[name].js',
    sourceMapFilename: '[file].map',
  },
};

```


### Serverless.yaml simplified

```
service: sls-webpack-bug

provider:
  name: aws
  runtime: nodejs12.x

custom:
  webpack:
    webpackConfig: ./webpack.config.js

package:
  individually: true

plugins:
  - serverless-webpack

functions:
  exampleFunction:
    memorySize: 1024
    timeout: 30
    handler: services/myService/handler.default
    runtime: provided

  exampleFunction2:
    memorySize: 1024
    timeout: 30
    handler: services/anotherService/myHandler.default

```

If I have time I will make a repro case, but I think that should be it for the time being.

PS: **The custom runtime is nodejs in this case**

Thanks

Pull request:
Feature/allow custom runtime
<!--
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:

Closes #664 

Allow custom runtime if Nodejs based.

## How did you implement it:

Those are the proposed changes:

We add an additional property to allow a custom runtime only if the user sets this to `true`

```
  exampleFunction:
    memorySize: 1024
    timeout: 30
    handler: services/myService/handler.default
    runtime: provided
    allowCustomRuntime: true
```

And then add another check:

```
if (runtime === 'provided' && loadedFunc.allowCustomRuntime) {
        const entry = getEntryForFunction.call(this, functions[index], loadedFunc);
        _.merge(entries, entry);
}
```

## How can we verify it:

You can check this repository https://github.com/MatteoGioioso/serverless-webpack-bug

#### Without this feature
- Input: specify `runtime: provided` and use the latest version of serverless-webpack
- Output: serverless-webpack will skip anything that is not a nodejs runtime: https://github.com/serverless-heaven/serverless-webpack/blob/69dfc3f502ee3a080863d7c1e1a94b996f532c5e/lib/validate.js#L104
Therefore it will package the entire project. You can inspect the `.serverless` folder after the packaging has completed


#### With this feature
- Input: specify `runtime: provided` and  `allowCustomRuntime: true`  with the version of this branch
- Output: this time it will allow a custom runtime, but only if the user explicitly allow it: https://github.com/MatteoGioioso/serverless-webpack/blob/b9941bb9f3d4e9772113e6621087e1ac607a77a2/lib/validate.js#L111 
The function will be packaged like normally. You can inspect the result in the `.serverless` folder after the packaging has completed

I have added a test as well to check this condition and updated the documentation.


```
=============================== Coverage summary ===============================
Statements   : 96.34% ( 736/764 )
Branches     : 86.49% ( 320/370 )
Functions    : 95.31% ( 183/192 )
Lines        : 96.42% ( 728/755 )

```

## 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/README.md b/README.md
index 8ba9657..3cb9c37 100644
--- a/README.md
+++ b/README.md
@@ -469,6 +469,21 @@ custom:
 This can be useful, in case you want to upload the source maps to your Error
 reporting system, or just have it available for some post processing.
 
+#### Nodejs custom runtime
+
+If you are using a nodejs custom runtime you can add the property `allowCustomRuntime: true`.
+
+```yaml
+exampleFunction:
+  handler: path/to/your/handler.default
+  runtime: provided
+  allowCustomRuntime: true
+```
+
+⚠️ **Note: this will only work if your custom runtime and function are written in JavaScript.
+Make sure you know what you are doing when this option is set to `true`**
+
+
 #### Examples
 
 You can find an example setups in the [`examples`][link-examples] folder.
diff --git a/lib/validate.js b/lib/validate.js
index a994606..80c0eb7 100644
--- a/lib/validate.js
+++ b/lib/validate.js
@@ -101,11 +101,18 @@ module.exports = {
       _.forEach(functions, (func, index) => {
         const loadedFunc = this.serverless.service.getFunction(func);
         const runtime = loadedFunc.runtime || this.serverless.service.provider.runtime || 'nodejs';
+
         if (runtime.match(/node/)) {
           // runtimes can be 'nodejsX.Y' (AWS, Azure) or 'google-nodejs' (Google Cloud)
           const entry = getEntryForFunction.call(this, functions[index], loadedFunc);
           _.merge(entries, entry);
         }
+
+        if (runtime === 'provided' && loadedFunc.allowCustomRuntime) {
+          // allow custom runtime if the user has specified it
+          const entry = getEntryForFunction.call(this, functions[index], loadedFunc);
+          _.merge(entries, entry);
+        }
       });
     }
 


Test patch:
diff --git a/tests/validate.test.js b/tests/validate.test.js
index 49bb84c..12c2ae8 100644
--- a/tests/validate.test.js
+++ b/tests/validate.test.js
@@ -545,6 +545,85 @@ describe('validate', () => {
         });
       });
 
+      it('should allow custom runtime', () => {
+        const testOutPath = 'test';
+        const testFunctionsConfig = {
+          func1: {
+            handler: 'module1.func1handler',
+            artifact: 'artifact-func1.zip',
+            events: [
+              {
+                http: {
+                  method: 'get',
+                  path: 'func1path'
+                }
+              }
+            ],
+            runtime: 'node10.x'
+          },
+          func2: {
+            handler: 'module2.func2handler',
+            artifact: 'artifact-func2.zip',
+            events: [
+              {
+                http: {
+                  method: 'POST',
+                  path: 'func2path'
+                }
+              },
+              {
+                nonhttp: 'non-http'
+              }
+            ],
+            runtime: 'provided',
+            allowCustomRuntime: true
+          },
+          func3: {
+            handler: 'module3.func2handler',
+            artifact: 'artifact-func3.zip',
+            events: [
+              {
+                http: {
+                  method: 'POST',
+                  path: 'func3path'
+                }
+              },
+              {
+                nonhttp: 'non-http'
+              }
+            ],
+            runtime: 'provided'
+          }
+        };
+
+        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')]);
+        return expect(module.validate()).to.be.fulfilled.then(() => {
+          const lib = require('../lib/index');
+          const expectedLibEntries = {
+            module1: './module1.js',
+            module2: './module2.js'
+          };
+
+          expect(lib.entries).to.deep.equal(expectedLibEntries);
+          expect(globSyncStub).to.have.callCount(2);
+          expect(serverless.cli.log).to.not.have.been.called;
+          return null;
+        });
+      });
+
       describe('google provider', () => {
         beforeEach(() => {
           _.set(module.serverless, 'service.provider.name', 'google');