Repository: serverless/serverless (JavaScript) — Bug Fix

Issue:
Unable to deploy services with custom `plugins.localPath` after upgrading serverless
# Bug Report

## Description

Upgrading to serverless version >= 1.55.0 breaks local serverless plugins

1. What did you do?

    Ran `npm i -g serverless@latest` and was unable to deploy or locally invoke functions which specify a custom `plugins.localPath` (see .yml file below).

1. What happened?

    ```
    $ npm i -g serverless@latest
    ...
    $ sls invoke local -f handler

      Serverless Error ---------------------------------------

      Serverless plugin "deployment-scripts" not found. Make sure it's installed and listed in the "plugins" section of your serverless config file.

      Get Support --------------------------------------------
          Docs:          docs.serverless.com
          Bugs:          github.com/serverless/serverless/issues
          Issues:        forum.serverless.com

      Your Environment Information ---------------------------
          Operating System:          darwin
          Node Version:              10.16.3
          Framework Version:         1.60.0
          Plugin Version:            3.2.6
          SDK Version:               2.2.1
          Components Core Version:   1.1.2
          Components CLI Version:    1.4.0
    ```

1. What should've happened?

     The local invocation should have succeeded, or at least started. Instead serverless never gets past trying to load the plugins.

1. What's the content of your `serverless.yml` file?

    ```
    plugins:
      localPath: '../serverless-plugins'
      modules:
        - deployment-scripts
        - ...
    ```

1. What's the output you get when you use the `SLS_DEBUG=*` environment variable (e.g. `SLS_DEBUG=* serverless deploy`)

    ```
      Serverless Error ---------------------------------------
    
      ServerlessError: Serverless plugin "deployment-scripts" not found. Make sure it's installed and listed in the "plugins" section of your serverless config file.
          at pluginsObject.modules.filter.map.name (/usr/local/lib/node_modules/serverless/lib/classes/PluginManager.js:137:17)
          at Array.map (<anonymous>)
          at PluginManager.resolveServicePlugins (/usr/local/lib/node_modules/serverless/lib/classes/PluginManager.js:123:8)
          at PluginManager.loadAllPlugins (/usr/local/lib/node_modules/serverless/lib/classes/PluginManager.js:110:15)
          at pluginManager.loadConfigFile.then.then (/usr/local/lib/node_modules/serverless/lib/Serverless.js:95:35)
      From previous event:
          at Serverless.init (/usr/local/lib/node_modules/serverless/lib/Serverless.js:93:8)
          at initializeErrorReporter.then (/usr/local/lib/node_modules/serverless/bin/serverless.js:73:8)
          at runCallback (timers.js:705:18)
          at tryOnImmediate (timers.js:676:5)
          at processImmediate (timers.js:658:5)
          at process.topLevelDomainCallback (domain.js:126:23)
      From previous event:
          at Object.<anonymous> (/usr/local/lib/node_modules/serverless/bin/serverless.js:63:4)
          at Module._compile (internal/modules/cjs/loader.js:778:30)
          at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
          at Module.load (internal/modules/cjs/loader.js:653:32)
          at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
          at Function.Module._load (internal/modules/cjs/loader.js:585:3)
          at Function.Module.runMain (internal/modules/cjs/loader.js:831:12)
          at startup (internal/bootstrap/node.js:283:19)
          at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
    
      Get Support --------------------------------------------
         Docs:          docs.serverless.com
         Bugs:          github.com/serverless/serverless/issues
         Issues:        forum.serverless.com
    
      Your Environment Information ---------------------------
         Operating System:          darwin
         Node Version:              10.16.3
         Framework Version:         1.60.0
         Plugin Version:            3.2.6
         SDK Version:               2.2.1
         Components Core Version:   1.1.2
         Components CLI Version:    1.4.0
    ```

Other:

- Deployment and local invocation work when I change the plugins section like this:

     ```
         plugins:
           localPath: '../serverless-plugins'
           modules: {}
    ```

- My serverless version prior to upgrading was ~1.51.0. After discovering the problem with 1.60.0, I've been able to isolate the problem to something that changed between 1.54 and 1.55. I.e., everything works as expected if I revert to serverless 1.54.0 but breaks as above when I move to versions >= 1.55.0.

Similar or dependent issues:

- Searching `is:issue local plugin not found` yields nothing that looks to be related 



Discussion:

Resolving patch:
diff --git a/commitlint.config.js b/commitlint.config.js
index 07422529a3e..09509aa82f4 100644
--- a/commitlint.config.js
+++ b/commitlint.config.js
@@ -8,7 +8,7 @@ module.exports = {
     'footer-max-line-length': [2, 'always', 72],
     'header-max-length': [2, 'always', 72],
     'scope-case': [2, 'always', 'start-case'],
-    'scope-enum': [2, 'always', ['', 'Binary Installer']],
+    'scope-enum': [2, 'always', ['', 'Binary Installer', 'Plugins']],
     'subject-case': [2, 'always', 'sentence-case'],
     'subject-empty': [2, 'never'],
     'subject-full-stop': [2, 'never', '.'],
diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js
index fdcabd80b21..3c61bf07e02 100644
--- a/lib/classes/PluginManager.js
+++ b/lib/classes/PluginManager.js
@@ -15,7 +15,8 @@ const getCommandSuggestion = require('../utils/getCommandSuggestion');
 const requireServicePlugin = (servicePath, pluginPath, localPluginsPath) => {
   if (localPluginsPath && !pluginPath.startsWith('./')) {
     // TODO (BREAKING): Consider removing support for localPluginsPath with next major
-    const absoluteLocalPluginPath = path.join(localPluginsPath, pluginPath);
+    const absoluteLocalPluginPath = path.resolve(localPluginsPath, pluginPath);
+
     try {
       return require(absoluteLocalPluginPath);
     } catch (error) {


Test patch:
diff --git a/lib/classes/PluginManager.test.js b/lib/classes/PluginManager.test.js
index 941a9b6e59b..a676d69f622 100644
--- a/lib/classes/PluginManager.test.js
+++ b/lib/classes/PluginManager.test.js
@@ -2014,6 +2014,22 @@ describe('PluginManager', () => {
       );
     });
 
+    it('should load plugins from custom folder outside of serviceDir', () => {
+      serviceDir = path.join(tmpDir, 'serverless-plugins-custom');
+      const localPluginDir = path.join(serviceDir, 'local-plugin');
+      installPlugin(localPluginDir, SynchronousPluginMock);
+
+      pluginManager.loadAllPlugins({
+        localPath: '../serverless-plugins-custom',
+        modules: ['local-plugin'],
+      });
+      // Had to use contructor.name because the class will be loaded via
+      // require and the reference will not match with SynchronousPluginMock
+      expect(pluginManager.plugins).to.satisfy(plugins =>
+        plugins.some(plugin => plugin.constructor.name === 'SynchronousPluginMock')
+      );
+    });
+
     afterEach(() => {
       // eslint-disable-line prefer-arrow-callback
       process.chdir(cwd);