Repository: serverless/serverless (JavaScript) — Bug Fix

Issue:
Nested internal entry point commands are shown in the help screen
# This is a Bug Report

## Description

Commands of type "entrypoint" can be nested. The help screen (invoke "serverless") hides
entry points that are declared at the top level, but still shows entrypoints that are nested below
visible commands.

Sample:
**package:function** is an internal entry point that can be used by plugins to spawn
the function packaging process or can be hooked to extend it. It should not be shown
to the user though as it is declared as "entrypoint" but nested under the publicly
available "package" command.

The bug will show up after #3496 has been merged (the new entrypoint is introduced there).

## Solution

The main serverless app (Serverless.js) uses (and is the only consumer of) **pluginManager.getCommands()** which does only a top level filtering right now. The filter has to be extended to also omit nested entrypoint command hierarchies.


Discussion:

Resolving patch:
diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js
index 5bae71ec90f..8e7d7b2acc0 100644
--- a/lib/classes/PluginManager.js
+++ b/lib/classes/PluginManager.js
@@ -173,9 +173,26 @@ class PluginManager {
   }
 
   getCommands() {
-    // Return filtered list of visible commands
-    const cmds = _.omitBy(this.commands, ['type', 'entrypoint']);
-    return cmds;
+    const result = {};
+
+    // Iterate through the commands and stop at entrypoints to include only public
+    // command throughout the hierarchy.
+    const stack = [{ commands: this.commands, target: result }];
+    while (!_.isEmpty(stack)) {
+      const currentCommands = stack.pop();
+      const commands = currentCommands.commands;
+      const target = currentCommands.target;
+      _.forOwn(commands, (command, name) => {
+        if (command.type !== 'entrypoint') {
+          _.set(target, name, _.omit(command, 'commands'));
+          if (_.some(command.commands, childCommand => childCommand.type !== 'entrypoint')) {
+            target[name].commands = {};
+            stack.push({ commands: command.commands, target: target[name].commands });
+          }
+        }
+      });
+    }
+    return result;
   }
 
   /**


Test patch:
diff --git a/lib/classes/PluginManager.test.js b/lib/classes/PluginManager.test.js
index 7ee6e6fa283..3ea06eefd0a 100644
--- a/lib/classes/PluginManager.test.js
+++ b/lib/classes/PluginManager.test.js
@@ -240,6 +240,13 @@ describe('PluginManager', () => {
                 'event2',
               ],
             },
+            spawnep: {
+              type: 'entrypoint',
+              lifecycleEvents: [
+                'event1',
+                'event2',
+              ],
+            },
           },
         },
 
@@ -1070,6 +1077,21 @@ describe('PluginManager', () => {
     });
   });
 
+  describe('#getCommands()', () => {
+    it('should hide entrypoints on any level and only return commands', () => {
+      pluginManager.addPlugin(EntrypointPluginMock);
+
+      const commands = pluginManager.getCommands();
+      expect(commands).to.have.a.property('mycmd');
+      expect(commands).to.have.a.deep.property('mycmd.commands.mysubcmd');
+      expect(commands).to.have.a.deep.property('mycmd.commands.spawncmd');
+      // Check for omitted entrypoints
+      expect(commands).to.not.have.a.property('myep');
+      expect(commands).to.not.have.a.deep.property('myep.commands.mysubep');
+      expect(commands).to.not.have.a.deep.property('mycmd.commands.spawnep');
+    });
+  });
+
   describe('#spawn()', () => {
     it('should throw an error when the given command is not available', () => {
       pluginManager.addPlugin(EntrypointPluginMock);