Repository: serverless/serverless (JavaScript)
Date: 2018-04-08 20:47:20+00:00
Task type: Feature

Issue:
Configure local plugins folder
# This is a Feature Proposal

## Description

Right now, when creating small local plugins, the only current option is to create a folder named `.serverless_plugins` to store it.

It would be nice to be able to configure the local plugins folder. This is good for projects with a well defined folder structure, that must then have an enforced folder that breaks the mould. That is specially true for _dot_ prefixed folders, which are usually used for working environment metadata, and commonly _gitignored_.

Behaviour would be exactly the same as today, it would only change the folder from `.serverless_plugins` to the configured one.

```yaml
service:
  name: my-serverless-stuff

# we could have a simple new property...
localPluginsFolder: './sls/plugins'
plugins:
  - plugin-one
  - plugin-two
...
```

```yaml
service: my-serverless-stuff  # fallback!

# or we could have an enhanced plugin object, with fallback into the "import", 
# just as "service" does with "name" 
plugins:
  localFolder: './sls/plugins'
  import:
    - plugin-one
    - plugin-two
...
```
## Additional Data

* https://serverless.com/framework/docs/providers/aws/guide/plugins/
Configure local plugins folder
# This is a Feature Proposal

## Description

Right now, when creating small local plugins, the only current option is to create a folder named `.serverless_plugins` to store it.

It would be nice to be able to configure the local plugins folder. This is good for projects with a well defined folder structure, that must then have an enforced folder that breaks the mould. That is specially true for _dot_ prefixed folders, which are usually used for working environment metadata, and commonly _gitignored_.

Behaviour would be exactly the same as today, it would only change the folder from `.serverless_plugins` to the configured one.

```yaml
service:
  name: my-serverless-stuff

# we could have a simple new property...
localPluginsFolder: './sls/plugins'
plugins:
  - plugin-one
  - plugin-two
...
```

```yaml
service: my-serverless-stuff  # fallback!

# or we could have an enhanced plugin object, with fallback into the "import", 
# just as "service" does with "name" 
plugins:
  localFolder: './sls/plugins'
  import:
    - plugin-one
    - plugin-two
...
```
## Additional Data

* https://serverless.com/framework/docs/providers/aws/guide/plugins/


Issue discussion:
Hi @nolde , thanks for the feature request 👍 . This is indeed a valuable improvement.

I would prefer something like the second alternative with a fallback to the old declaration instead of adding a new root key and make sure that plugins that manage the list do not break. Maybe we can use a different name than `import` that more precisely describes the plugin list node, e.g.

```yaml
plugins:
  localPath: './sls/plugins',
  modules:
    - plugin1
    - plugin2
```

What do you think? Of course the `sls plugin` command has to be adapted too - and the dot-folder should still be the default if `localPath` is omitted.
Looks pretty good in my opinion!

The names can be fine-tuned to anything that you guys feel are the correct ones; the important part is the functionality. If you want my opinion, I reckon anything around the idea of `use`, `list`, `import`, `modules` should be fine; and `localPath` is a lot better as well.

Yes, current behaviour should still be the default one, when no `localPath` is defined.

Thanks for taking the time to look into this.


Hi @nolde @HyperBrain I want to clarify the behavior of `localPath` setting. As I infer from the declaration both relative and absolute paths should be supported. For relative paths the root location would be the `servicePath` it's equal to `process.cwd()` if applicable.
Thanks!
Hi @nolde , thanks for the feature request 👍 . This is indeed a valuable improvement.

I would prefer something like the second alternative with a fallback to the old declaration instead of adding a new root key and make sure that plugins that manage the list do not break. Maybe we can use a different name than `import` that more precisely describes the plugin list node, e.g.

```yaml
plugins:
  localPath: './sls/plugins',
  modules:
    - plugin1
    - plugin2
```

What do you think? Of course the `sls plugin` command has to be adapted too - and the dot-folder should still be the default if `localPath` is omitted.
Looks pretty good in my opinion!

The names can be fine-tuned to anything that you guys feel are the correct ones; the important part is the functionality. If you want my opinion, I reckon anything around the idea of `use`, `list`, `import`, `modules` should be fine; and `localPath` is a lot better as well.

Yes, current behaviour should still be the default one, when no `localPath` is defined.

Thanks for taking the time to look into this.


Hi @nolde @HyperBrain I want to clarify the behavior of `localPath` setting. As I infer from the declaration both relative and absolute paths should be supported. For relative paths the root location would be the `servicePath` it's equal to `process.cwd()` if applicable.
Thanks!

Solution patch:
diff --git a/docs/providers/aws/guide/plugins.md b/docs/providers/aws/guide/plugins.md
index f7a57a76a72..3cea0015727 100644
--- a/docs/providers/aws/guide/plugins.md
+++ b/docs/providers/aws/guide/plugins.md
@@ -33,9 +33,25 @@ We need to tell Serverless that we want to use the plugin inside our service. We
 plugins:
   - custom-serverless-plugin
 ```
+The `plugins` section supports two formats:
 
-Plugins might want to add extra information which should be accessible to Serverless. The `custom` section in the `serverless.yml` file is the place where you can add necessary
-configurations for your plugins (the plugins author / documentation will tell you if you need to add anything there):
+Array object:
+```yml
+plugins:
+  - plugin1
+  - plugin2
+```
+
+Enhanced plugins object:
+```yml
+plugins:
+  localPath: './custom_serverless_plugins'
+  modules:
+    - plugin1
+    - plugin2
+```
+
+Plugins might want to add extra information which should be accessible to Serverless. The `custom` section in the `serverless.yml` file is the place where you can add necessary configurations for your plugins (the plugins author / documentation will tell you if you need to add anything there):
 
 ```yml
 plugins:
@@ -47,9 +63,24 @@ custom:
 
 ## Service local plugin
 
-If you are working on a plugin or have a plugin that is just designed for one project you can add them to the `.serverless_plugins` directory at the root of your service, and in the `plugins` array in `serverless.yml`.
+If you are working on a plugin or have a plugin that is just designed for one project they can be loaded from the local folder. Local plugins can be added in the `plugins` array in `serverless.yml`.
+
+By default local plugins can be added to the `.serverless_plugins` directory at the root of your service, and in the `plugins` array in `serverless.yml`.
+```yml
+plugins:
+  - custom-serverless-plugin
+```
+
+Local plugins folder can be changed by enhancing `plugins` object:
+```yml
+plugins:
+  localPath: './custom_serverless_plugins'
+  modules:
+    - custom-serverless-plugin
+```
+The `custom-serverless-plugin` will be loaded from the `custom_serverless_plugins` directory at the root of your service. If the `localPath` is not provided or empty `.serverless_plugins` directory will be taken as the `localPath`.
 
-The plugin will be loaded based on being named `custom-serverless-plugin.js` or `custom-serverless-plugin\index.js` in the root of `.serverless_plugins` folder.
+The plugin will be loaded based on being named `custom-serverless-plugin.js` or `custom-serverless-plugin\index.js` in the root of `localPath` folder (`.serverless_plugins` by default).
 
 ### Load Order
 
diff --git a/docs/providers/azure/guide/plugins.md b/docs/providers/azure/guide/plugins.md
index b80c6df001c..5263fa3b97a 100644
--- a/docs/providers/azure/guide/plugins.md
+++ b/docs/providers/azure/guide/plugins.md
@@ -42,11 +42,25 @@ do this by adding the name of the Plugin to the `plugins` section in the
 plugins:
   - custom-serverless-plugin
 ```
+The `plugins` section supports two formats:
 
-Plugins might want to add extra information which should be accessible to
-Serverless. The `custom` section in the `serverless.yml` file is the place where
-you can add necessary configurations for your plugins (the plugins
-author/documentation will tell you if you need to add anything there):
+Array object:
+```yml
+plugins:
+  - plugin1
+  - plugin2
+```
+
+Enhanced plugins object:
+```yml
+plugins:
+  localPath: './custom_serverless_plugins'
+  modules:
+    - plugin1
+    - plugin2
+```
+
+Plugins might want to add extra information which should be accessible to Serverless. The `custom` section in the `serverless.yml` file is the place where you can add necessary configurations for your plugins (the plugins author / documentation will tell you if you need to add anything there):
 
 ```yml
 plugins:
@@ -58,18 +72,28 @@ custom:
 
 ## Service local plugin
 
-If you are working on a plugin or have a plugin that is just designed for one
-project you can add them to the `.serverless_plugins` directory at the root of
-your service, and in the `plugins` array in `serverless.yml`.
+If you are working on a plugin or have a plugin that is just designed for one project they can be loaded from the local folder. Local plugins can be added in the `plugins` array in `serverless.yml`.
+
+By default local plugins can be added to the `.serverless_plugins` directory at the root of your service, and in the `plugins` array in `serverless.yml`.
+```yml
+plugins:
+  - custom-serverless-plugin
+```
+
+Local plugins folder can be changed by enhancing `plugins` object:
+```yml
+plugins:
+  localPath: './custom_serverless_plugins'
+  modules:
+    - custom-serverless-plugin
+```
+The `custom-serverless-plugin` will be loaded from the `custom_serverless_plugins` directory at the root of your service. If the `localPath` is not provided or empty `.serverless_plugins` directory will be taken as the `localPath`.
 
-The plugin will be loaded based on being named `custom-serverless-plugin.js` or
-`custom-serverless-plugin\index.js` in the root of `.serverless_plugins` folder.
+The plugin will be loaded based on being named `custom-serverless-plugin.js` or `custom-serverless-plugin\index.js` in the root of `localPath` folder (`.serverless_plugins` by default).
 
 ### Load Order
 
-Keep in mind that the order you define your plugins matters. When Serverless
-loads all the core plugins and then the custom plugins in the order you've
-defined them.
+Keep in mind that the order you define your plugins matters. When Serverless loads all the core plugins and then the custom plugins in the order you've defined them.
 
 ```yml
 # serverless.yml
diff --git a/docs/providers/google/guide/plugins.md b/docs/providers/google/guide/plugins.md
index aaf032b125e..78c8a6e24ed 100644
--- a/docs/providers/google/guide/plugins.md
+++ b/docs/providers/google/guide/plugins.md
@@ -33,6 +33,23 @@ We need to tell Serverless that we want to use the plugin inside our service. We
 plugins:
   - custom-serverless-plugin
 ```
+The `plugins` section supports two formats:
+
+Array object:
+```yml
+plugins:
+  - plugin1
+  - plugin2
+```
+
+Enhanced plugins object:
+```yml
+plugins:
+  localPath: './custom_serverless_plugins'
+  modules:
+    - plugin1
+    - plugin2
+```
 
 Plugins might want to add extra information which should be accessible to Serverless. The `custom` section in the `serverless.yml` file is the place where you can add necessary configurations for your plugins (the plugins author / documentation will tell you if you need to add anything there):
 
@@ -46,9 +63,24 @@ custom:
 
 ## Service local plugin
 
-If you are working on a plugin or have a plugin that is just designed for one project you can add them to the `.serverless_plugins` directory at the root of your service, and in the `plugins` array in `serverless.yml`.
+If you are working on a plugin or have a plugin that is just designed for one project they can be loaded from the local folder. Local plugins can be added in the `plugins` array in `serverless.yml`.
+
+By default local plugins can be added to the `.serverless_plugins` directory at the root of your service, and in the `plugins` array in `serverless.yml`.
+```yml
+plugins:
+  - custom-serverless-plugin
+```
+
+Local plugins folder can be changed by enhancing `plugins` object:
+```yml
+plugins:
+  localPath: './custom_serverless_plugins'
+  modules:
+    - custom-serverless-plugin
+```
+The `custom-serverless-plugin` will be loaded from the `custom_serverless_plugins` directory at the root of your service. If the `localPath` is not provided or empty `.serverless_plugins` directory will be taken as the `localPath`.
 
-The plugin will be loaded based on being named `custom-serverless-plugin.js` or `custom-serverless-plugin\index.js` in the root of `.serverless_plugins` folder.
+The plugin will be loaded based on being named `custom-serverless-plugin.js` or `custom-serverless-plugin\index.js` in the root of `localPath` folder (`.serverless_plugins` by default).
 
 ### Load Order
 
diff --git a/docs/providers/openwhisk/guide/plugins.md b/docs/providers/openwhisk/guide/plugins.md
index f233ccbe977..c0d27405fc0 100644
--- a/docs/providers/openwhisk/guide/plugins.md
+++ b/docs/providers/openwhisk/guide/plugins.md
@@ -33,9 +33,25 @@ We need to tell Serverless that we want to use the plugin inside our service. We
 plugins:
   - custom-serverless-plugin
 ```
+The `plugins` section supports two formats:
 
-Plugins might want to add extra information which should be accessible to Serverless. The `custom` section in the `serverless.yml` file is the place where you can add necessary
-configurations for your plugins (the plugins author / documentation will tell you if you need to add anything there):
+Array object:
+```yml
+plugins:
+  - plugin1
+  - plugin2
+```
+
+Enhanced plugins object:
+```yml
+plugins:
+  localPath: './custom_serverless_plugins'
+  modules:
+    - plugin1
+    - plugin2
+```
+
+Plugins might want to add extra information which should be accessible to Serverless. The `custom` section in the `serverless.yml` file is the place where you can add necessary configurations for your plugins (the plugins author / documentation will tell you if you need to add anything there):
 
 ```yml
 plugins:
@@ -47,9 +63,24 @@ custom:
 
 ## Service local plugin
 
-If you are working on a plugin or have a plugin that is just designed for one project you can add them to the `.serverless_plugins` directory at the root of your service, and in the `plugins` array in `serverless.yml`.
+If you are working on a plugin or have a plugin that is just designed for one project they can be loaded from the local folder. Local plugins can be added in the `plugins` array in `serverless.yml`.
+
+By default local plugins can be added to the `.serverless_plugins` directory at the root of your service, and in the `plugins` array in `serverless.yml`.
+```yml
+plugins:
+  - custom-serverless-plugin
+```
+
+Local plugins folder can be changed by enhancing `plugins` object:
+```yml
+plugins:
+  localPath: './custom_serverless_plugins'
+  modules:
+    - custom-serverless-plugin
+```
+The `custom-serverless-plugin` will be loaded from the `custom_serverless_plugins` directory at the root of your service. If the `localPath` is not provided or empty `.serverless_plugins` directory will be taken as the `localPath`.
 
-The plugin will be loaded based on being named `custom-serverless-plugin.js` or `custom-serverless-plugin\index.js` in the root of `.serverless_plugins` folder.
+The plugin will be loaded based on being named `custom-serverless-plugin.js` or `custom-serverless-plugin\index.js` in the root of `localPath` folder (`.serverless_plugins` by default).
 
 ### Load Order
 
diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js
index 1325ec9a91d..2e10cf0b080 100644
--- a/lib/classes/PluginManager.js
+++ b/lib/classes/PluginManager.js
@@ -54,9 +54,9 @@ class PluginManager {
     let pluginProvider = null;
     // check if plugin is provider agnostic
     if (pluginInstance.provider) {
-      if (typeof pluginInstance.provider === 'string') {
+      if (_.isString(pluginInstance.provider)) {
         pluginProvider = pluginInstance.provider;
-      } else if (typeof pluginInstance.provider === 'object') {
+      } else if (_.isObject(pluginInstance.provider)) {
         pluginProvider = pluginInstance.provider.constructor.getProviderName();
       }
     }
@@ -130,16 +130,34 @@ class PluginManager {
   }
 
   loadServicePlugins(servicePlugs) {
-    const servicePlugins = Array.isArray(servicePlugs) ? servicePlugs : [];
-
     // eslint-disable-next-line no-underscore-dangle
     module.paths = Module._nodeModulePaths(process.cwd());
 
+    const pluginsObject = this.parsePluginsObject(servicePlugs);
     // we want to load plugins installed locally in the service
-    if (this.serverless && this.serverless.config && this.serverless.config.servicePath) {
-      module.paths.unshift(path.join(this.serverless.config.servicePath, '.serverless_plugins'));
+    if (pluginsObject.localPath) {
+      module.paths.unshift(pluginsObject.localPath);
+    }
+    this.loadPlugins(pluginsObject.modules);
+  }
+
+  parsePluginsObject(servicePlugs) {
+    let localPath = (this.serverless && this.serverless.config &&
+      this.serverless.config.servicePath) &&
+      path.join(this.serverless.config.servicePath, '.serverless_plugins');
+    let modules = [];
+
+    if (_.isArray(servicePlugs)) {
+      modules = servicePlugs;
+    } else if (servicePlugs) {
+      localPath = servicePlugs.localPath &&
+        _.isString(servicePlugs.localPath) ? servicePlugs.localPath : localPath;
+      if (_.isArray(servicePlugs.modules)) {
+        modules = servicePlugs.modules;
+      }
     }
-    this.loadPlugins(servicePlugins);
+
+    return { modules, localPath };
   }
 
   createCommandAlias(alias, command) {
@@ -430,7 +448,7 @@ class PluginManager {
 
       if (_.isPlainObject(value.customValidation) &&
         value.customValidation.regularExpression instanceof RegExp &&
-        typeof value.customValidation.errorMessage === 'string' &&
+        _.isString(value.customValidation.errorMessage) &&
         !value.customValidation.regularExpression.test(this.cliOptions[key])) {
         throw new this.serverless.classes.Error(value.customValidation.errorMessage);
       }
diff --git a/lib/plugins/package/lib/packageService.js b/lib/plugins/package/lib/packageService.js
index 67a73208aca..b7008b88cde 100644
--- a/lib/plugins/package/lib/packageService.js
+++ b/lib/plugins/package/lib/packageService.js
@@ -26,9 +26,13 @@ module.exports = {
 
   getExcludes(exclude) {
     const packageExcludes = this.serverless.service.package.exclude || [];
+    // add local service plugins Path
+    const pluginsLocalPath = this.serverless.pluginManager
+      .parsePluginsObject(this.serverless.service.plugins).localPath;
 
+    const localPathExcludes = pluginsLocalPath ? [pluginsLocalPath] : [];
     // add defaults for exclude
-    return _.union(this.defaultExcludes, packageExcludes, exclude);
+    return _.union(this.defaultExcludes, localPathExcludes, packageExcludes, exclude);
   },
 
   packageService() {
diff --git a/lib/plugins/plugin/install/install.js b/lib/plugins/plugin/install/install.js
index c688128bba8..65116941149 100644
--- a/lib/plugins/plugin/install/install.js
+++ b/lib/plugins/plugin/install/install.js
@@ -117,19 +117,40 @@ class PluginInstall {
         return BbPromise.resolve();
       }
 
+      const checkIsArrayPluginsObject = (pluginsObject) =>
+        _.isNil(pluginsObject) || _.isArray(pluginsObject);
+      // pluginsObject type determined based on the value loaded during the serverless init.
       if (_.last(_.split(serverlessFilePath, '.')) === 'json') {
         return fse.readJsonAsync(serverlessFilePath).then(serverlessFileObj => {
           const newServerlessFileObj = serverlessFileObj;
-          if (newServerlessFileObj.plugins) {
-            newServerlessFileObj.plugins.push(this.options.pluginName);
+          const isArrayPluginsObject = checkIsArrayPluginsObject(newServerlessFileObj.plugins);
+          // null modules property is not supported
+          let plugins = isArrayPluginsObject ? newServerlessFileObj.plugins || [] :
+            newServerlessFileObj.plugins.modules;
+
+          if (_.isNil(plugins)) {
+            throw new Error('plugins modules property must be present');
+          }
+
+          plugins.push(this.options.pluginName);
+          plugins = _.sortedUniq(plugins);
+
+          if (isArrayPluginsObject) {
+            newServerlessFileObj.plugins = plugins;
           } else {
-            newServerlessFileObj.plugins = [this.options.pluginName];
+            newServerlessFileObj.plugins.modules = plugins;
           }
-          newServerlessFileObj.plugins = _.sortedUniq(newServerlessFileObj.plugins);
+
           return fse.writeJsonAsync(serverlessFilePath, newServerlessFileObj);
         });
       }
-      return yamlAstParser.addNewArrayItem(serverlessFilePath, 'plugins', this.options.pluginName);
+
+      return this.serverless.yamlParser
+        .parse(serverlessFilePath)
+        .then((serverlessFileObj) =>
+          yamlAstParser.addNewArrayItem(serverlessFilePath,
+            checkIsArrayPluginsObject(serverlessFileObj.plugins) ?
+              'plugins' : 'plugins.modules', this.options.pluginName));
     });
   }
 
diff --git a/lib/plugins/plugin/uninstall/uninstall.js b/lib/plugins/plugin/uninstall/uninstall.js
index 05580a98ab8..4d241d2e99f 100644
--- a/lib/plugins/plugin/uninstall/uninstall.js
+++ b/lib/plugins/plugin/uninstall/uninstall.js
@@ -88,17 +88,32 @@ class PluginUninstall {
 
       if (_.last(_.split(serverlessFilePath, '.')) === 'json') {
         return fse.readJsonAsync(serverlessFilePath).then(serverlessFileObj => {
-          if (serverlessFileObj.plugins) {
-            _.pull(serverlessFileObj.plugins, this.options.pluginName);
-            if (_.isEmpty(serverlessFileObj.plugins)) {
-              _.unset(serverlessFileObj, 'plugins');
+          const isArrayPluginsObject = _.isArray(serverlessFileObj.plugins);
+          const plugins = isArrayPluginsObject ?
+            serverlessFileObj.plugins :
+            serverlessFileObj.plugins && serverlessFileObj.plugins.modules;
+
+          if (plugins) {
+            _.pull(plugins, this.options.pluginName);
+            if (_.isEmpty(plugins)) {
+              if (isArrayPluginsObject) {
+                _.unset(serverlessFileObj, 'plugins');
+              } else {
+                _.unset(serverlessFileObj.plugins, 'modules');
+              }
             }
+            return fse.writeJsonAsync(serverlessFilePath, serverlessFileObj);
           }
-          return fse.writeJsonAsync(serverlessFilePath, serverlessFileObj);
+          return BbPromise.resolve();
         });
       }
-      return yamlAstParser.removeExistingArrayItem(
-        serverlessFilePath, 'plugins', this.options.pluginName);
+
+      return this.serverless.yamlParser
+      .parse(serverlessFilePath)
+      .then((serverlessFileObj) =>
+        yamlAstParser.removeExistingArrayItem(
+          serverlessFilePath, _.isArray(serverlessFileObj.plugins) ?
+            'plugins' : 'plugins.modules', this.options.pluginName));
     });
   }
 
diff --git a/lib/utils/yamlAstParser.js b/lib/utils/yamlAstParser.js
index 7d91f55f986..324b543dc44 100644
--- a/lib/utils/yamlAstParser.js
+++ b/lib/utils/yamlAstParser.js
@@ -58,60 +58,113 @@ const parseAST = (ymlAstContent, astObject) => {
   return newAstObject;
 };
 
+const constructPlainObject = (ymlAstContent, branchObject) => {
+  const newbranchObject = branchObject || {};
+  if (ymlAstContent.mappings && _.isArray(ymlAstContent.mappings)) {
+    _.forEach(ymlAstContent.mappings, (v) => {
+      if (!v.value) {
+        // no need to log twice, parseAST will log errors
+        return;
+      }
+
+      if (v.key.kind === 0 && v.value.kind === 0) {
+        newbranchObject[v.key.value] = v.value.value;
+      } else if (v.key.kind === 0 && v.value.kind === 2) {
+        newbranchObject[v.key.value] = constructPlainObject(v.value, {});
+      } else if (v.key.kind === 0 && v.value.kind === 3) {
+        const plainArray = [];
+        _.forEach(v.value.items, (c) => {
+          plainArray.push(c.value);
+        });
+        newbranchObject[v.key.value] = plainArray;
+      }
+    });
+  }
+
+  return newbranchObject;
+};
+
 const addNewArrayItem = (ymlFile, pathInYml, newValue) =>
 fs.readFileAsync(ymlFile, 'utf8').then(yamlContent => {
-  const astObject = parseAST(yaml.load(yamlContent));
+  const rawAstObject = yaml.load(yamlContent);
+  const astObject = parseAST(rawAstObject);
+  const plainObject = constructPlainObject(rawAstObject);
+  const pathInYmlArray = pathInYml.split('.');
 
-  if (astObject[pathInYml] && astObject[pathInYml].items) {
-    let newArray = [];
-    _.forEach(astObject[pathInYml].items, (v) => {
-      newArray.push(v.value);
-    });
-    newArray = _.union(newArray, [newValue]);
+  let currentNode = plainObject;
+  for (let i = 0; i < pathInYmlArray.length - 1; i++) {
+    const propertyName = pathInYmlArray[i];
+    const property = currentNode[propertyName];
+    if (_.isUndefined(property) || _.isObject(property)) {
+      currentNode[propertyName] = property || {};
+      currentNode = currentNode[propertyName];
+    } else {
+      throw new Error(`${property} can only be undefined or an object!`);
+    }
+  }
 
-    const pathInYmlArrray = pathInYml.split('.');
-    let newObject = JSON.stringify(newArray);
-    _.forEach(pathInYmlArrray.reverse(), (v) => {
-      newObject = `{"${v}":${newObject}}`;
-    });
+  const arrayPropertyName = _.last(pathInYmlArray);
+  let arrayProperty = currentNode[arrayPropertyName];
+  if (_.isUndefined(arrayProperty) || _.isArray(arrayProperty)) {
+    arrayProperty = arrayProperty || [];
+  } else {
+    throw new Error(`${arrayProperty} can only be undefined or an array!`);
+  }
+  currentNode[arrayPropertyName] = _.union(arrayProperty, [newValue]);
 
-    const beginning = yamlContent.substring(0, astObject[_.head(_.split(pathInYml, '.'))]
-      .parent.key.startPosition);
-    const end = yamlContent.substring(astObject[pathInYml].endPosition, yamlContent.length);
-    return fs.writeFileAsync(ymlFile, `${beginning}${yaml.dump(JSON.parse(newObject))}${end}`);
+  const branchToReplaceName = _.head(pathInYmlArray);
+  const newObject = {};
+  newObject[branchToReplaceName] = plainObject[branchToReplaceName];
+  const newText = yaml.dump(newObject);
+  if (astObject[branchToReplaceName]) {
+    const beginning = yamlContent
+      .substring(0, astObject[branchToReplaceName].parent.key.startPosition);
+    const end = yamlContent
+      .substring(astObject[branchToReplaceName].endPosition, yamlContent.length);
+    return fs.writeFileAsync(ymlFile, `${beginning}${newText}${end}`);
   }
-  const pathInYmlArrray = pathInYml.split('.');
-  let newObject = JSON.stringify([newValue]);
-  _.forEach(pathInYmlArrray.reverse(), (v) => {
-    newObject = `{"${v}":${newObject}}`;
-  });
-  const beginning = yamlContent.substring(0, yamlContent.length);
-  return fs.writeFileAsync(ymlFile, `${beginning}${os.EOL}${yaml.dump(JSON.parse(newObject))}`);
+  return fs.writeFileAsync(ymlFile, `${yamlContent}${os.EOL}${newText}`);
 });
 
 const removeExistingArrayItem = (ymlFile, pathInYml, removeValue) =>
 fs.readFileAsync(ymlFile, 'utf8').then(yamlContent => {
-  const astObject = parseAST(yaml.load(yamlContent));
+  const rawAstObject = yaml.load(yamlContent);
+  const astObject = parseAST(rawAstObject);
 
   if (astObject[pathInYml] && astObject[pathInYml].items) {
-    const newArray = [];
-    let newText = '';
-    _.forEach(astObject[pathInYml].items, (v) => {
-      newArray.push(v.value);
-    });
-    _.pull(newArray, removeValue);
-
-    if (!_.isEmpty(newArray)) {
-      const pathInYmlArrray = pathInYml.split('.');
-      let newObject = JSON.stringify(newArray);
-      _.forEach(pathInYmlArrray.reverse(), (v) => {
-        newObject = `{"${v}":${newObject}}`;
-      });
-      newText = yaml.dump(JSON.parse(newObject));
+    const plainObject = constructPlainObject(rawAstObject);
+    const pathInYmlArray = pathInYml.split('.');
+
+    let currentNode = plainObject;
+    const pathInObjectTree = [];
+    for (let i = 0; i < pathInYmlArray.length - 1; i++) {
+      pathInObjectTree.push(currentNode);
+      currentNode = currentNode[pathInYmlArray[i]];
+    }
+    const arrayPropertyName = _.last(pathInYmlArray);
+    const arrayProperty = currentNode[arrayPropertyName];
+    _.pull(arrayProperty, removeValue);
+
+    if (_.isEmpty(arrayProperty)) {
+      _.unset(currentNode, arrayPropertyName);
+      pathInObjectTree.push(currentNode);
+      for (let i = pathInObjectTree.length - 1; i > 0; i--) {
+        if (_.keys(pathInObjectTree[i]).length > 0) {
+          break;
+        }
+        _.unset(pathInObjectTree[i - 1], pathInYmlArray[i - 1]);
+      }
     }
 
-    const beginning = yamlContent.substring(0, astObject[_.head(_.split(pathInYml, '.'))]
-      .parent.key.startPosition);
+    const headObjectPath = _.head(pathInYmlArray);
+    let newText = '';
+
+    if (plainObject[headObjectPath]) {
+      const newObject = {};
+      newObject[headObjectPath] = plainObject[headObjectPath];
+      newText = yaml.dump(newObject);
+    }
+    const beginning = yamlContent.substring(0, astObject[headObjectPath].parent.key.startPosition);
     const end = yamlContent.substring(astObject[pathInYml].endPosition, yamlContent.length);
     return fs.writeFileAsync(ymlFile, `${beginning}${newText}${end}`);
   }


Test patch:
diff --git a/lib/classes/PluginManager.test.js b/lib/classes/PluginManager.test.js
index 437a515ffd0..7a19f227d28 100644
--- a/lib/classes/PluginManager.test.js
+++ b/lib/classes/PluginManager.test.js
@@ -779,6 +779,64 @@ describe('PluginManager', () => {
     });
   });
 
+  describe('#parsePluginsObject()', () => {
+    const parsePluginsObjectAndVerifyResult = (servicePlugins, expectedResult) => {
+      const result = pluginManager.parsePluginsObject(servicePlugins);
+      expect(result).to.deep.equal(expectedResult);
+    };
+
+    it('should parse array object', () => {
+      const servicePlugins = ['ServicePluginMock1', 'ServicePluginMock2'];
+
+      parsePluginsObjectAndVerifyResult(servicePlugins, {
+        modules: servicePlugins,
+        localPath: path.join(serverless.config.servicePath, '.serverless_plugins'),
+      });
+    });
+
+    it('should parse plugins object', () => {
+      const servicePlugins = {
+        modules: ['ServicePluginMock1', 'ServicePluginMock2'],
+        localPath: './myplugins',
+      };
+
+      parsePluginsObjectAndVerifyResult(servicePlugins, {
+        modules: servicePlugins.modules,
+        localPath: servicePlugins.localPath,
+      });
+    });
+
+    it('should parse plugins object if format is not correct', () => {
+      const servicePlugins = {};
+
+      parsePluginsObjectAndVerifyResult(servicePlugins, {
+        modules: [],
+        localPath: path.join(serverless.config.servicePath, '.serverless_plugins'),
+      });
+    });
+
+    it('should parse plugins object if modules property is not an array', () => {
+      const servicePlugins = { modules: {} };
+
+      parsePluginsObjectAndVerifyResult(servicePlugins, {
+        modules: [],
+        localPath: path.join(serverless.config.servicePath, '.serverless_plugins'),
+      });
+    });
+
+    it('should parse plugins object if localPath is not correct', () => {
+      const servicePlugins = {
+        modules: ['ServicePluginMock1', 'ServicePluginMock2'],
+        localPath: {},
+      };
+
+      parsePluginsObjectAndVerifyResult(servicePlugins, {
+        modules: servicePlugins.modules,
+        localPath: path.join(serverless.config.servicePath, '.serverless_plugins'),
+      });
+    });
+  });
+
   describe('command aliases', () => {
     describe('#getAliasCommandTarget', () => {
       it('should return an alias target', () => {
@@ -1511,6 +1569,66 @@ describe('PluginManager', () => {
     });
   });
 
+  describe('Plugin / Load local plugins', () => {
+    const cwd = process.cwd();
+    let serviceDir;
+    let tmpDir;
+    beforeEach(function () { // eslint-disable-line prefer-arrow-callback
+      tmpDir = testUtils.getTmpDirPath();
+      serviceDir = path.join(tmpDir, 'service');
+      fse.mkdirsSync(serviceDir);
+      process.chdir(serviceDir);
+      pluginManager.serverless.config.servicePath = serviceDir;
+    });
+
+    it('should load plugins from .serverless_plugins', () => {
+      const localPluginDir = path.join(serviceDir, '.serverless_plugins', 'local-plugin');
+      testUtils.installPlugin(localPluginDir, SynchronousPluginMock);
+
+      pluginManager.loadServicePlugins(['local-plugin']);
+      expect(pluginManager.plugins).to.satisfy(plugins =>
+        plugins.some(plugin => plugin.constructor.name === 'SynchronousPluginMock'));
+    });
+
+    it('should load plugins from custom folder', () => {
+      const localPluginDir = path.join(serviceDir, 'serverless-plugins-custom', 'local-plugin');
+      testUtils.installPlugin(localPluginDir, SynchronousPluginMock);
+
+      pluginManager.loadServicePlugins({
+        localPath: path.join(serviceDir, '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'));
+    });
+
+    it('should load plugins from custom folder outside of serviceDir', () => {
+      serviceDir = path.join(tmpDir, 'serverless-plugins-custom');
+      const localPluginDir = path.join(serviceDir, 'local-plugin');
+      testUtils.installPlugin(localPluginDir, SynchronousPluginMock);
+
+      pluginManager.loadServicePlugins({
+        localPath: serviceDir,
+        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(function () { // eslint-disable-line prefer-arrow-callback
+      process.chdir(cwd);
+      try {
+        fse.removeSync(tmpDir);
+      } catch (e) {
+        // Couldn't delete temporary file
+      }
+    });
+  });
+
   describe('Plugin / CLI integration', function () {
     this.timeout(0);
 
@@ -1559,6 +1677,11 @@ describe('PluginManager', () => {
 
     afterEach(function () { // eslint-disable-line prefer-arrow-callback
       process.chdir(cwd);
+      try {
+        fse.removeSync(serviceDir);
+      } catch (e) {
+        // Couldn't delete temporary file
+      }
     });
   });
 });
diff --git a/lib/plugins/package/lib/packageService.test.js b/lib/plugins/package/lib/packageService.test.js
index a625e0f3099..4420a1dcb59 100644
--- a/lib/plugins/package/lib/packageService.test.js
+++ b/lib/plugins/package/lib/packageService.test.js
@@ -1,5 +1,6 @@
 'use strict';
 
+const _ = require('lodash');
 const BbPromise = require('bluebird');
 const path = require('path');
 const chai = require('chai');
@@ -71,41 +72,61 @@ describe('#packageService()', () => {
       expect(exclude).to.deep.equal(packagePlugin.defaultExcludes);
     });
 
-    it('should merge defaults with excludes', () => {
+    it('should exclude plugins localPath defaults', () => {
+      const localPath = './myplugins';
+      serverless.service.plugins = { localPath };
+
+      const exclude = packagePlugin.getExcludes();
+      expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes, [localPath]));
+    });
+
+    it('should not exclude plugins localPath if it is empty', () => {
+      const localPath = '';
+      serverless.service.plugins = { localPath };
+
+      const exclude = packagePlugin.getExcludes();
+      expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes));
+    });
+
+    it('should not exclude plugins localPath if it is not a string', () => {
+      const localPath = {};
+      serverless.service.plugins = { localPath };
+
+      const exclude = packagePlugin.getExcludes();
+      expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes));
+    });
+
+    it('should merge defaults with plugin localPath and excludes', () => {
+      const localPath = './myplugins';
+      serverless.service.plugins = { localPath };
+
       const packageExcludes = [
         'dir', 'file.js',
       ];
-
       serverless.service.package.exclude = packageExcludes;
 
+
       const exclude = packagePlugin.getExcludes();
-      expect(exclude).to.deep.equal([
-        '.git/**', '.gitignore', '.DS_Store',
-        'npm-debug.log', 'serverless.yml',
-        'serverless.yaml', 'serverless.json', 'serverless.js',
-        '.serverless/**', '.serverless_plugins/**',
-        'dir', 'file.js',
-      ]);
+      expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes,
+        [localPath], packageExcludes));
     });
 
-    it('should merge defaults with package and func excludes', () => {
-      const funcExcludes = [
-        'lib', 'other.js',
-      ];
+    it('should merge defaults with plugin localPath package and func excludes', () => {
+      const localPath = './myplugins';
+      serverless.service.plugins = { localPath };
+
       const packageExcludes = [
         'dir', 'file.js',
       ];
-
       serverless.service.package.exclude = packageExcludes;
 
+      const funcExcludes = [
+        'lib', 'other.js',
+      ];
+
       const exclude = packagePlugin.getExcludes(funcExcludes);
-      expect(exclude).to.deep.equal([
-        '.git/**', '.gitignore', '.DS_Store',
-        'npm-debug.log', 'serverless.yml',
-        'serverless.yaml', 'serverless.json', 'serverless.js',
-        '.serverless/**', '.serverless_plugins/**',
-        'dir', 'file.js', 'lib', 'other.js',
-      ]);
+      expect(exclude).to.deep.equal(_.union(packagePlugin.defaultExcludes,
+        [localPath], packageExcludes, funcExcludes));
     });
   });
 
diff --git a/lib/plugins/plugin/install/install.test.js b/lib/plugins/plugin/install/install.test.js
index ee0602db4de..7bdf1d17b3d 100644
--- a/lib/plugins/plugin/install/install.test.js
+++ b/lib/plugins/plugin/install/install.test.js
@@ -14,13 +14,13 @@ const CLI = require('../../../classes/CLI');
 const testUtils = require('../../../../tests/utils');
 chai.use(require('chai-as-promised'));
 const expect = require('chai').expect;
+const _ = require('lodash');
 
 describe('PluginInstall', () => {
   let pluginInstall;
   let serverless;
   let consoleLogStub;
   let serverlessErrorStub;
-
   const plugins = [
     {
       name: 'serverless-plugin-1',
@@ -300,8 +300,10 @@ describe('PluginInstall', () => {
       pluginInstall.options.pluginName = 'serverless-plugin-1';
 
       return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
-        expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8').plugins)
-          .to.deep.equal(['serverless-plugin-1']);
+        expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8'))
+          .to.deep.equal(_.assign({}, serverlessYml, {
+            plugins: ['serverless-plugin-1'],
+          }));
       });
     });
 
@@ -318,8 +320,10 @@ describe('PluginInstall', () => {
       pluginInstall.options.pluginName = 'serverless-plugin-1';
 
       return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
-        expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8').plugins)
-          .to.deep.equal(['serverless-existing-plugin', 'serverless-plugin-1']);
+        expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8'))
+          .to.deep.equal(_.assign({}, serverlessYml, {
+            plugins: ['serverless-existing-plugin', 'serverless-plugin-1'],
+          }));
       });
     });
 
@@ -333,8 +337,8 @@ describe('PluginInstall', () => {
         .writeFileSync(serverlessYamlFilePath, YAML.dump(serverlessYml));
       pluginInstall.options.pluginName = 'serverless-plugin-1';
       return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
-        expect(serverless.utils.readFileSync(serverlessYamlFilePath, 'utf8').plugins)
-          .to.deep.equal(['serverless-plugin-1']);
+        expect(serverless.utils.readFileSync(serverlessYamlFilePath, 'utf8'))
+          .to.deep.equal(_.assign({}, serverlessYml, { plugins: ['serverless-plugin-1'] }));
       });
     });
 
@@ -348,16 +352,19 @@ describe('PluginInstall', () => {
         .writeFileSync(serverlessJsonFilePath, serverlessJson);
       pluginInstall.options.pluginName = 'serverless-plugin-1';
       return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
-        expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8').plugins)
-          .to.deep.equal(['serverless-plugin-1']);
+        expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8'))
+          .to.deep.equal(_.assign({}, serverlessJson, { plugins: ['serverless-plugin-1'] }));
       })
-      .then(() => {
-        pluginInstall.options.pluginName = 'serverless-plugin-2';
-        return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
-          expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8').plugins)
-            .to.deep.equal(['serverless-plugin-1', 'serverless-plugin-2']);
+        .then(() => {
+          pluginInstall.options.pluginName = 'serverless-plugin-2';
+          return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
+            expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8'))
+              .to.deep.equal(_.assign({}, serverlessJson,
+                {
+                  plugins: ['serverless-plugin-1', 'serverless-plugin-2'],
+                }));
+          });
         });
-      });
     });
 
     it('should not modify serverless .js file', () => {
@@ -377,6 +384,70 @@ describe('PluginInstall', () => {
           .to.be.deep.equal([]);
       });
     });
+
+    describe('if plugins object is not array', () => {
+      it('should add the plugin to the service file', () => {
+        // serverless.yml
+        const serverlessYml = {
+          service: 'plugin-service',
+          provider: 'aws',
+          plugins: {
+            localPath: 'test',
+            modules: [],
+          },
+        };
+        serverless.utils
+          .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml));
+
+        pluginInstall.options.pluginName = 'serverless-plugin-1';
+
+        return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
+          expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8'))
+            .to.deep.equal(_.assign({}, serverlessYml, {
+              plugins: {
+                localPath: 'test',
+                modules: [pluginInstall.options.pluginName],
+              },
+            }));
+        });
+      });
+
+      it('should add the plugin to serverless file path for a .json file', () => {
+        const serverlessJsonFilePath = path.join(servicePath, 'serverless.json');
+        const serverlessJson = {
+          service: 'plugin-service',
+          provider: 'aws',
+          plugins: {
+            localPath: 'test',
+            modules: [],
+          },
+        };
+        serverless.utils
+          .writeFileSync(serverlessJsonFilePath, serverlessJson);
+        pluginInstall.options.pluginName = 'serverless-plugin-1';
+        return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
+          expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8'))
+            .to.deep.equal(_.assign({}, serverlessJson, {
+              plugins: {
+                localPath: 'test',
+                modules: [pluginInstall.options.pluginName],
+              },
+            }));
+        })
+          .then(() => {
+            pluginInstall.options.pluginName = 'serverless-plugin-2';
+            return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
+              expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8'))
+                .to.deep.equal(_.assign({}, serverlessJson, {
+                  plugins: {
+                    localPath: 'test',
+                    modules: ['serverless-plugin-1', 'serverless-plugin-2'],
+                  },
+                }));
+            });
+          });
+      });
+    });
   });
 
   describe('#installPeerDependencies()', () => {
diff --git a/lib/plugins/plugin/uninstall/uninstall.test.js b/lib/plugins/plugin/uninstall/uninstall.test.js
index a119fbbc0ea..8f75cd91ce7 100644
--- a/lib/plugins/plugin/uninstall/uninstall.test.js
+++ b/lib/plugins/plugin/uninstall/uninstall.test.js
@@ -85,9 +85,9 @@ describe('PluginUninstall', () => {
 
     it('should run promise chain in order for "plugin:uninstall:uninstall" hook',
       () => expect(pluginUninstall.hooks['plugin:uninstall:uninstall']())
-      .to.be.fulfilled.then(() => {
-        expect(uninstallStub.calledOnce).to.equal(true);
-      })
+        .to.be.fulfilled.then(() => {
+          expect(uninstallStub.calledOnce).to.equal(true);
+        })
     );
   });
 
@@ -205,7 +205,7 @@ describe('PluginUninstall', () => {
       fse.ensureDirSync(servicePath);
       packageJsonFilePath = path.join(servicePath, 'package.json');
       npmUninstallStub = sinon.stub(childProcess, 'execAsync')
-      .returns(BbPromise.resolve());
+        .returns(BbPromise.resolve());
       // save the cwd so that we can restore it later
       savedCwd = process.cwd();
       process.chdir(servicePath);
@@ -328,16 +328,16 @@ describe('PluginUninstall', () => {
           .to.deep.equal(['serverless-plugin-2']);
         pluginUninstall.options.pluginName = 'serverless-plugin-2';
       })
-      .then(() => expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(
-      () => {
-        expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8'))
-          .to.not.have.property('plugins');
-      }))
-      .then(() => expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(
-      () => {
-        expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8'))
-          .to.not.have.property('plugins');
-      }));
+        .then(() => expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(
+          () => {
+            expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8'))
+              .to.not.have.property('plugins');
+          }))
+        .then(() => expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(
+          () => {
+            expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8'))
+              .to.not.have.property('plugins');
+          }));
     });
 
     it('should not modify serverless .js file', () => {
@@ -364,6 +364,103 @@ describe('PluginUninstall', () => {
         });
       });
     });
+
+    describe('if plugins object is not array', () => {
+      it('should only remove the given plugin from the service', () => {
+        // serverless.yml
+        const serverlessYml = {
+          service: 'plugin-service',
+          provider: 'aws',
+          plugins: {
+            localPath: 'test',
+            modules: [
+              'serverless-existing-plugin',
+              'serverless-plugin-1',
+            ],
+          },
+        };
+        serverless.utils
+          .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml));
+
+        pluginUninstall.options.pluginName = 'serverless-plugin-1';
+
+        return expect(pluginUninstall.removePluginFromServerlessFile())
+          .to.be.fulfilled.then(() => {
+            expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8').plugins)
+              .to.deep.equal({
+                localPath: 'test',
+                modules: ['serverless-existing-plugin'],
+              });
+          });
+      });
+
+      it('should remove the plugin from the service if it is the only one', () => {
+        // serverless.yml
+        const serverlessYml = {
+          service: 'plugin-service',
+          provider: 'aws',
+          plugins: {
+            localPath: 'test',
+            modules: [
+              'serverless-plugin-1',
+            ],
+          },
+        };
+        serverless.utils
+          .writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml));
+
+        pluginUninstall.options.pluginName = 'serverless-plugin-1';
+
+        return expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => {
+          expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8').plugins)
+            .to.deep.equal({
+              localPath: 'test',
+            });
+        });
+      });
+
+      it('should remove the plugin from serverless file path for a .json file', () => {
+        const serverlessJsonFilePath = path.join(servicePath, 'serverless.json');
+        const serverlessJson = {
+          service: 'plugin-service',
+          provider: 'aws',
+          plugins: {
+            localPath: 'test',
+            modules: [
+              'serverless-plugin-1',
+              'serverless-plugin-2',
+            ],
+          },
+        };
+        serverless.utils
+          .writeFileSync(serverlessJsonFilePath, serverlessJson);
+        pluginUninstall.options.pluginName = 'serverless-plugin-1';
+        return expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(() => {
+          expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8').plugins)
+            .to.deep.equal({
+              localPath: 'test',
+              modules: [
+                'serverless-plugin-2',
+              ],
+            });
+          pluginUninstall.options.pluginName = 'serverless-plugin-2';
+        })
+          .then(() => expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(
+            () => {
+              expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8').plugins)
+                .to.deep.equal({
+                  localPath: 'test',
+                });
+            }))
+          .then(() => expect(pluginUninstall.removePluginFromServerlessFile()).to.be.fulfilled.then(
+            () => {
+              expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8').plugins)
+                .to.deep.equal({
+                  localPath: 'test',
+                });
+            }));
+      });
+    });
   });
 
   describe('#uninstallPeerDependencies()', () => {
diff --git a/lib/utils/yamlAstParser.test.js b/lib/utils/yamlAstParser.test.js
index 9e04b4a0ec9..3dbbb23612f 100644
--- a/lib/utils/yamlAstParser.test.js
+++ b/lib/utils/yamlAstParser.test.js
@@ -6,7 +6,10 @@ const testUtils = require('../../tests/utils');
 const writeFileSync = require('./fs/writeFileSync');
 const readFileSync = require('./fs/readFileSync');
 const yamlAstParser = require('./yamlAstParser');
-chai.use(require('chai-as-promised'));
+const _ = require('lodash');
+const chaiAsPromised = require('chai-as-promised');
+
+chai.use(chaiAsPromised);
 const expect = require('chai').expect;
 
 describe('#yamlAstParser', () => {
@@ -17,177 +20,273 @@ describe('#yamlAstParser', () => {
   });
 
   describe('#addNewArrayItem()', () => {
-    it('should add a top level object and item into the yaml file', () => {
+    const addNewArrayItemAndVerifyResult = (yamlContent, pathInYaml, newItem, expectedResult) => {
       const yamlFilePath = path.join(tmpDirPath, 'test.yaml');
-
-      writeFileSync(yamlFilePath, 'service: test-service');
-      return expect(yamlAstParser.addNewArrayItem(yamlFilePath, 'toplevel', 'foo'))
+      writeFileSync(yamlFilePath, yamlContent);
+      return expect(yamlAstParser.addNewArrayItem(yamlFilePath, pathInYaml, newItem))
         .to.be.fulfilled.then(() => {
           const yaml = readFileSync(yamlFilePath, 'utf8');
-          expect(yaml).to.have.property('toplevel');
-          expect(yaml.toplevel).to.be.deep.equal(['foo']);
+          expect(yaml).to.be.deep.equal(expectedResult);
         });
+    };
+
+    it('should add a top level object and item into the yaml file', () => {
+      const yamlContent = { service: 'test-service' };
+      const expectedResult = _.assign({}, yamlContent, {
+        toplevel: ['foo'],
+      });
+      return addNewArrayItemAndVerifyResult(yamlContent, 'toplevel', 'foo', expectedResult);
     });
 
     it('should add an item under the existing object which you specify', () => {
-      const yamlFilePath = path.join(tmpDirPath, 'test.yaml');
-
-      writeFileSync(yamlFilePath, { toplevel: ['foo'] });
-      return expect(yamlAstParser.addNewArrayItem(yamlFilePath, 'toplevel', 'bar'))
-        .to.be.fulfilled.then(() => {
-          const yaml = readFileSync(yamlFilePath, 'utf8');
-          expect(yaml).to.have.property('toplevel');
-          expect(yaml.toplevel).to.be.deep.equal(['foo', 'bar']);
-        });
+      const yamlContent = { toplevel: ['foo'] };
+      const expectedResult = { toplevel: ['foo', 'bar'] };
+      return addNewArrayItemAndVerifyResult(yamlContent, 'toplevel', 'bar', expectedResult);
     });
 
     it('should add a multiple level object and item into the yaml file', () => {
-      const yamlFilePath = path.join(tmpDirPath, 'test.yaml');
-
-      writeFileSync(yamlFilePath, 'service: test-service');
-      return expect(yamlAstParser.addNewArrayItem(yamlFilePath, 'toplevel.second.third', 'foo'))
-        .to.be.fulfilled.then(() => {
-          const yaml = readFileSync(yamlFilePath, 'utf8');
-          expect(yaml).to.have.property('toplevel');
-          expect(yaml.toplevel).to.be.deep.equal({
-            second: {
-              third: ['foo'],
-            },
-          });
-        });
+      const yamlContent = { service: 'test-service' };
+      const expectedResult = _.assign({}, yamlContent, {
+        toplevel: {
+          second: {
+            third: ['foo'],
+          },
+        },
+      });
+      return addNewArrayItemAndVerifyResult(yamlContent,
+        'toplevel.second.third', 'foo', expectedResult);
     });
 
     it('should add an item under the existing multiple level object which you specify', () => {
-      const yamlFilePath = path.join(tmpDirPath, 'test.yaml');
-
-      writeFileSync(yamlFilePath, {
+      const yamlContent = {
         toplevel: {
           second: {
             third: ['foo'],
           },
         },
-      });
-      return expect(yamlAstParser.addNewArrayItem(yamlFilePath, 'toplevel.second.third', 'bar'))
-        .to.be.fulfilled.then(() => {
-          const yaml = readFileSync(yamlFilePath, 'utf8');
-          expect(yaml).to.have.property('toplevel');
-          expect(yaml.toplevel).to.be.deep.equal({
-            second: {
-              third: ['foo', 'bar'],
-            },
-          });
-        });
+      };
+      const expectedResult = {
+        toplevel: {
+          second: {
+            third: ['foo', 'bar'],
+          },
+        },
+      };
+      return addNewArrayItemAndVerifyResult(yamlContent,
+        'toplevel.second.third', 'bar', expectedResult);
     });
 
-    it('should do nothing when adding the existing item', () => {
-      const yamlFilePath = path.join(tmpDirPath, 'test.yaml');
+    it('should add an item under partially existing multiple level object', () => {
+      const yamlContent = {
+        toplevel: {
+          first: 'foo',
+          second: {
+          },
+        },
+      };
+      const expectedResult = {
+        toplevel: {
+          first: 'foo',
+          second: {
+            third: ['bar'],
+          },
+        },
+      };
+      return addNewArrayItemAndVerifyResult(yamlContent,
+        'toplevel.second.third', 'bar', expectedResult);
+    });
 
-      writeFileSync(yamlFilePath, { toplevel: ['foo'] });
-      return expect(yamlAstParser.addNewArrayItem(yamlFilePath, 'toplevel', 'foo'))
-        .to.be.fulfilled.then(() => {
-          const yaml = readFileSync(yamlFilePath, 'utf8');
-          expect(yaml).to.have.property('toplevel');
-          expect(yaml.toplevel).to.be.deep.equal(['foo']);
-        });
+    it('should add an item in the middle branch', () => {
+      const yamlContent = {
+        initiallevel: 'bar',
+        toplevel: {
+          first: 'foo',
+        },
+        bottomlevel: 'bar',
+      };
+      const expectedResult = {
+        initiallevel: 'bar',
+        toplevel: {
+          first: 'foo',
+          second: ['bar'],
+        },
+        bottomlevel: 'bar',
+      };
+      return addNewArrayItemAndVerifyResult(yamlContent,
+        'toplevel.second', 'bar', expectedResult);
     });
 
-    it('should survive with invalid yaml', () => {
-      const yamlFilePath = path.join(tmpDirPath, 'test.yaml');
+    it('should add an item with multiple top level entries', () => {
+      const yamlContent = {
+        toplevel: {
+          first: 'foo',
+          second: {
+          },
+        },
+        nexttoplevel: {
+          first: 'bar',
+        },
+      };
+      const expectedResult = {
+        toplevel: {
+          first: 'foo',
+          second: {
+            third: ['bar'],
+          },
+        },
+        nexttoplevel: {
+          first: 'bar',
+        },
+      };
+      return addNewArrayItemAndVerifyResult(yamlContent,
+        'toplevel.second.third', 'bar', expectedResult);
+    });
 
-      writeFileSync(yamlFilePath, 'service:');
-      return expect(yamlAstParser.addNewArrayItem(yamlFilePath, 'toplevel', 'foo'))
-        .to.be.fulfilled.then(() => {
-          const yaml = readFileSync(yamlFilePath, 'utf8');
-          expect(yaml).to.have.property('toplevel');
-          expect(yaml.toplevel).to.be.deep.equal(['foo']);
-        });
+    it('should do nothing when adding the existing item', () => {
+      const yamlContent = { toplevel: ['foo'] };
+      const expectedResult = { toplevel: ['foo'] };
+      return addNewArrayItemAndVerifyResult(yamlContent, 'toplevel', 'foo', expectedResult);
+    });
+
+    it('should survive with invalid yaml', () => {
+      const yamlContent = 'service:';
+      const expectedResult = { service: null, toplevel: ['foo'] };
+      return addNewArrayItemAndVerifyResult(yamlContent, 'toplevel', 'foo', expectedResult);
     });
   });
 
   describe('#removeExistingArrayItem()', () => {
-    it('should remove the existing top level object and item from the yaml file', () => {
-      const yamlFilePath = path.join(tmpDirPath, 'test.yaml');
+    const removeExistingArrayItemAndVerifyResult =
+      (yamlContent, pathInYaml, removeItem, expectedResult) => {
+        const yamlFilePath = path.join(tmpDirPath, 'test.yaml');
+        writeFileSync(yamlFilePath, yamlContent);
+        return expect(yamlAstParser.removeExistingArrayItem(yamlFilePath, pathInYaml, removeItem))
+          .to.be.fulfilled.then(() => {
+            const yaml = readFileSync(yamlFilePath, 'utf8');
+            expect(yaml).to.be.deep.equal(expectedResult);
+          });
+      };
 
-      writeFileSync(yamlFilePath, {
-        serveice: 'test-service',
+    it('should remove the existing top level object and item from the yaml file', () => {
+      const yamlContent = {
+        service: 'test-service',
         toplevel: ['foo'],
-      });
-      return expect(yamlAstParser.removeExistingArrayItem(yamlFilePath, 'toplevel', 'foo'))
-        .to.be.fulfilled.then(() => {
-          const yaml = readFileSync(yamlFilePath, 'utf8');
-          expect(yaml).to.have.not.property('toplevel');
-        });
+      };
+      const expectedResult = { service: 'test-service' };
+      return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel',
+        'foo', expectedResult);
     });
 
     it('should remove the existing item under the object which you specify', () => {
-      const yamlFilePath = path.join(tmpDirPath, 'test.yaml');
-
-      writeFileSync(yamlFilePath, {
-        serveice: 'test-service',
+      const yamlContent = {
+        service: 'test-service',
         toplevel: ['foo', 'bar'],
-      });
-      return expect(yamlAstParser.removeExistingArrayItem(yamlFilePath, 'toplevel', 'bar'))
-        .to.be.fulfilled.then(() => {
-          const yaml = readFileSync(yamlFilePath, 'utf8');
-          expect(yaml).to.have.property('toplevel');
-          expect(yaml.toplevel).to.be.deep.equal(['foo']);
-        });
+      };
+      const expectedResult = {
+        service: 'test-service',
+        toplevel: ['foo'],
+      };
+      return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel',
+        'bar', expectedResult);
     });
 
     it('should remove the multiple level object and item from the yaml file', () => {
-      const yamlFilePath = path.join(tmpDirPath, 'test.yaml');
-
-      writeFileSync(yamlFilePath, {
-        serveice: 'test-service',
+      const yamlContent = {
+        service: 'test-service',
         toplevel: {
           second: {
             third: ['foo'],
           },
         },
-      });
-      return expect(yamlAstParser.removeExistingArrayItem(
-        yamlFilePath, 'toplevel.second.third', 'foo')).to.be.fulfilled.then(() => {
-          const yaml = readFileSync(yamlFilePath, 'utf8');
-          expect(yaml).to.have.not.property('toplevel');
-        });
+      };
+      const expectedResult = { service: 'test-service' };
+      return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel.second.third',
+        'foo', expectedResult);
     });
 
     it('should remove the existing item under the multiple level object which you specify', () => {
-      const yamlFilePath = path.join(tmpDirPath, 'test.yaml');
+      const yamlContent = {
+        service: 'test-service',
+        toplevel: {
+          second: {
+            third: ['foo', 'bar'],
+          },
+        },
+      };
+      const expectedResult = {
+        service: 'test-service',
+        toplevel: {
+          second: {
+            third: ['foo'],
+          },
+        },
+      };
+      return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel.second.third',
+        'bar', expectedResult);
+    });
 
-      writeFileSync(yamlFilePath, {
-        serveice: 'test-service',
+    it('should remove multilevel object from the middle branch', () => {
+      const yamlContent = {
+        service: 'test-service',
+        toplevel: {
+          second: {
+            third: ['foo'],
+          },
+        },
+        end: 'end',
+      };
+      const expectedResult = {
+        service: 'test-service',
+        end: 'end',
+      };
+      return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel.second.third',
+        'foo', expectedResult);
+    });
+
+    it('should remove item from multilevel object from the middle branch', () => {
+      const yamlContent = {
+        service: 'test-service',
         toplevel: {
           second: {
             third: ['foo', 'bar'],
           },
         },
-      });
-      return expect(yamlAstParser.removeExistingArrayItem(
-        yamlFilePath, 'toplevel.second.third', 'bar')).to.be.fulfilled.then(() => {
-          const yaml = readFileSync(yamlFilePath, 'utf8');
-          expect(yaml).to.have.property('toplevel');
-          expect(yaml.toplevel).to.be.deep.equal({
-            second: {
-              third: ['foo'],
-            },
-          });
-        });
+        end: 'end',
+      };
+      const expectedResult = {
+        service: 'test-service',
+        toplevel: {
+          second: {
+            third: ['bar'],
+          },
+        },
+        end: 'end',
+      };
+      return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel.second.third',
+        'foo', expectedResult);
     });
 
     it('should do nothing when you can not find the object which you specify', () => {
-      const yamlFilePath = path.join(tmpDirPath, 'test.yaml');
-
-      writeFileSync(yamlFilePath, {
+      const yamlContent = {
         serveice: 'test-service',
         toplevel: ['foo', 'bar'],
-      });
-      return expect(yamlAstParser.removeExistingArrayItem(yamlFilePath, 'toplevel', 'foo2'))
-      .to.be.fulfilled.then(() => {
-        const yaml = readFileSync(yamlFilePath, 'utf8');
-        expect(yaml).to.have.property('toplevel');
-        expect(yaml.toplevel).to.be.deep.equal(['foo', 'bar']);
-      });
+      };
+
+      return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel',
+        'foo2', yamlContent);
+    });
+
+    it('should remove when with inline declaration of the array', () => {
+      const yamlContent =
+        'toplevel:\n' +
+        '  second: ["foo2", "bar"]';
+      const expectedResult = {
+        toplevel: {
+          second: ['foo2'],
+        },
+      };
+      return removeExistingArrayItemAndVerifyResult(yamlContent, 'toplevel.second',
+        'bar', expectedResult);
     });
   });
 });