Version

Improve this page

Creating a plugin

So, you want to write a plugin? Great! It’s people like you that will help the ember-cli-deploy plugin ecosystem flourish.

So, let’s get started.

The anatomy of a plugin

ember-cli-deploy plugins are nothing more than standard ember-cli addons with 3 small ember-cli-deploy specific traits:

  1. they contain the ember-cli-deploy-plugin keyword in their package.json to identify them as plugins
  2. they are named ember-cli-deploy-*
  3. they return an object that implements one or more of the ember-cli-deploy pipeline hooks

Let’s have a look at each of these things in a bit more detail.

Create an addon

An ember-cli-deploy plugin is just a standard ember-cli addon. Create it as follows:

ember addon ember-cli-deploy-funky-plugin

Identify the addon as a plugin

In order for ember-cli-deploy to know your addon is a plugin, we need to identify it as such by updating the package.json like so:

// package.json

"keywords": [
  "ember-addon",
  "ember-cli-deploy-plugin"
]

Implement one or more pipeline hooks

In order for a plugin to be useful it must implement one or more of the ember-cli-deploy pipeline hooks.

To do this you must implement a function called createDeployPlugin in your index.js file. This function must return an object that contains:

  1. a name property which is what your plugin will be referred to by in the config/deploy.js and;
  2. one or more implemented pipeline hooks

Let’s look at an example:

module.exports = {
  name: 'ember-cli-deploy-funky-plugin',

  createDeployPlugin: function(options) {
    return {
      name: options.name,

      didBuild: function(context) {
        //do something amazing here once the project has been built
      },

      upload: function(context) {
        //do something here to actually deploy your app somewhere
      },

      didDeploy: function(context) {
        //do something here like notify your team on slack
      }
    };
  }
};

That’s seriously about as difficult as it gets. However, read on for some more advanced info to get the most out of your ember-cli-deploy plugin.

Ordering Plugins

To specify that your plugin should run before or after a particular plugin or set of plugins, specify the runBefore or runAfter properties:

var BasePlugin = require('ember-cli-deploy-plugin');

module.exports = {
  name: 'ember-cli-deploy-funky-plugin',

  createDeployPlugin: function(options) {
    var DeployPlugin = BasePlugin.extend({
      name: options.name,

      runBefore: ['foo', 'bar'],
      runAfter: 'baz',

      didDeploy: function(context) {
        //do something here like notify your team on slack
      }
    });

    return new DeployPlugin();
  }
};

An example use case of this is where we want to upload the project assets with the s3 plugin before uploading the index.html with the redis plugin. This way we can be certain that the assets exist before the bootstrap index.html, that references them, can be loaded by clients.

The Base Deploy Plugin

There are some common tasks that the majority of plugins need to do like validate configuration and log messages out to the terminal. So we have created a base plugin that you can extend to get this functionality for free.

Extending the base plugin

To extend the base plugin, first you need to install it:

npm install ember-cli-deploy-plugin --save

Then you need to extend it like this:

var BasePlugin = require('ember-cli-deploy-plugin');

module.exports = {
  name: 'ember-cli-deploy-funky-plugin',

  createDeployPlugin: function(options) {
    var DeployPlugin = BasePlugin.extend({
      name: options.name,

      didBuild: function(context) {
        //do something amazing here once the project has been built
      },

      upload: function(context) {
        //do something here to actually deploy your app somewhere
      },

      didDeploy: function(context) {
        //do something here like notify your team on slack
      }
    });

    return new DeployPlugin();
  }
};

Validating plugin config

ember-cli-deploy provides a pipeline hook called configure for the purpose of validating and setting up state that will be needed by the plugin hooks executed later on in the pipeline. This hook is the perfect place to validate that the plugin has all the required config it needs to perform it’s pipeline tasks.

As this validation is such a common thing, the base deploy plugin will implement the configure hook by default and validate the configuration for you. In order for this to happen, you must implement one or both of defaultConfig and requiredConfig.

defaultConfig

The defaultConfig property allows you to specify default values for config properties that are not defined in config/deploy.js, like this:

var BasePlugin = require('ember-cli-deploy-plugin');

module.exports = {
  name: 'ember-cli-deploy-funky-plugin',

  createDeployPlugin: function(options) {
    var DeployPlugin = BasePlugin.extend({
      name: options.name,

      defaultConfig: {
        // default filePattern if it isn't
        // defined in config/deploy.js
        filePattern: '**/*.{js,css,png}'
      },

      upload: function(context) { }
    });

    return new DeployPlugin();
  }
};

You can also have the defaultConfig options be a function that takes in the deployment context as the first argument. This allows the config value to be decided at runtime based on properties that have been added to the deployment context by other plugins that have run before it.

var BasePlugin = require('ember-cli-deploy-plugin');

module.exports = {
  name: 'ember-cli-deploy-funky-plugin',

  createDeployPlugin: function(options) {
    var DeployPlugin = BasePlugin.extend({
      name: options.name,

      defaultConfig: {
        gzippedFiles: function(context) {
          // if gzippedFiles has been added
          // to the context by another plugin we can use it
          return context.gzippedFiles || [];
        }
      },

      upload: function(context) { }
    });

    return new DeployPlugin();
  }
};

requiredConfig

The requiredConfig property allows you to specify config properties that must be provided in order for the plugin to function correctly in the deployment pipeline. If any required config value is not provided the pipeline will be aborted and an error message will be displayed informing you of which config property is missing.

You can specify the required configuration properties like this:

var BasePlugin = require('ember-cli-deploy-plugin');

module.exports = {
  name: 'ember-cli-deploy-funky-plugin',

  createDeployPlugin: function(options) {
    var DeployPlugin = BasePlugin.extend({
      name: options.name,

      requiredConfig: ['accessKeyId', 'secretAccessKey'],

      upload: function(context) { }
    });

    return new DeployPlugin();
  }
};

Logging messages to the terminal

Due to the custom pipeline output that ember-cli-deploy displays, the base plugin provides a function to log messages into the pipeline output.

To log a message to the terminal use the log function as follows:

var BasePlugin = require('ember-cli-deploy-plugin');

module.exports = {
  name: 'ember-cli-deploy-funky-plugin',

  createDeployPlugin: function(options) {
    var DeployPlugin = BasePlugin.extend({
      name: options.name,

      upload: function(context) {
        this.log('Uploading assets');
      }
    });

    return new DeployPlugin();
  }
};

Log messages will be displayed using the log-info-color config option (default: ‘blue’)

If you need to log an error or warning message using a different color, simply pass the color in as an option to the log function like this:

var BasePlugin = require('ember-cli-deploy-plugin');

module.exports = {
  name: 'ember-cli-deploy-funky-plugin',

  createDeployPlugin: function(options) {
    var DeployPlugin = BasePlugin.extend({
      name: options.name,

      upload: function(context) {
        this.log('Oops. Something went wrong', { color: 'red' });
      }
    });

    return new DeployPlugin();
  }
};

If you want your message to be only visible when the user passes the --verbose option, simply pass verbose: true to the log function`:

var BasePlugin = require('ember-cli-deploy-plugin');

module.exports = {
  name: 'ember-cli-deploy-funky-plugin',

  createDeployPlugin: function(options) {
    var DeployPlugin = BasePlugin.extend({
      name: options.name,

      upload: function(context) {
        this.log('Uploading all the things', { verbose: true });
      }
    });

    return new DeployPlugin();
  }
};

Accessing config properties

When you want to access config properties from inside your pipeline hooks, the base plugin provides the readConfig function to do so. It is this function that allows you to have config values that are calculated at run time based on data in the deployment context.

A basic example of using this function is with a static config value, like so:

var BasePlugin = require('ember-cli-deploy-plugin');

module.exports = {
  name: 'ember-cli-deploy-funky-plugin',

  createDeployPlugin: function(options) {
    var DeployPlugin = BasePlugin.extend({
      name: options.name,

      defaultConfig: {
        manifestPath: '/manifest.txt'
      },

      upload: function(context) {
        // will return the static value of '/manifest.txt'
        this.readConfig('manifestPath')
      }
    });

    return new DeployPlugin();
  }
};

However, it gets much more interesting when the config value is a function:

var BasePlugin = require('ember-cli-deploy-plugin');

module.exports = {
  name: 'ember-cli-deploy-funky-plugin',

  createDeployPlugin: function(options) {
    var DeployPlugin = BasePlugin.extend({
      name: options.name,

      defaultConfig: {
        manifestPath: function(context) {
          return context.manifestPath;
        }
      },

      upload: function(context) {
        // will return the value of manifestPath,
        // that has been added to the context by another plugin, at runtime
        this.readConfig('manifestPath');
      }
    });

    return new DeployPlugin();
  }
};

In this example, it is assumed that some other plugin hook that has been run previously to this upload hook has added the manifestPath property to the deployment context. At the point that readConfig is called, the config function is exectuted passing in the current deployment context, returning the current value of manifestPath.

Adding data to the deployment context object

The deployment context is an object that is passed to each pipeline hook as it is executed. It allows plugins to access data from plugin hooks that have run before it and to pass data to plugin hooks that will run after it.

To add something to the deployment context, simply return an object from your pipeline hook. This object will be merged into the current deployment context which will then be passed to every pipeline hook thereafter.

So, imagine the deployment context looks like this:

{ distFiles: ['index.html', 'assets/app.js' ] }

When you return an object from your pipeline hook like this:

var BasePlugin = require('ember-cli-deploy-plugin');

module.exports = {
  name: 'ember-cli-deploy-funky-plugin',

  createDeployPlugin: function(options) {
    var DeployPlugin = BasePlugin.extend({
      name: options.name,

      upload: function(context) {
        return {
          uploadedAt: '2015-10-14T22:29:46.313Z'
        };
      }
    });

    return new DeployPlugin();
  }
};

Then once the pipeline hook has run, the deployment context will look like this:

{
  distFiles: ['index.html', 'assets/app.js' ],
  uploadedAt: '2015-10-14T22:29:46.313Z'
}

And every pipeline hook run thereafter will be able to access the uploadedAt property.

Testing

Because plugins are effectively node code rather than ember code, they aren’t tested like regular ember addons. You’ll want to install mocha

npm install mocha --save-dev

and write your tests in tests/unit/index-nodetest.js, using the following as a template. Note the config.pluginClient mock, which you should replace with a check that the correct params are being sent to the external api.

var chai  = require('chai');
var chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);

var assert = chai.assert;

var stubProject = {
  name: function(){
    return 'my-project';
  }
};

describe('my new plugin', function() {
  var subject, mockUi;

  beforeEach(function() {
    subject = require('../../index');
    mockUi = {
      verbose: true,
      messages: [],
      write: function() { },
      writeLine: function(message) {
        this.messages.push(message);
      }
    };
  });

  it('has a name', function() {
    var result = subject.createDeployPlugin({
      name: 'test-plugin'
    });

    assert.equal(result.name, 'test-plugin');
  });

  describe('hook',function() {
    var plugin;
    var context;

    it('calls the hook', function() {
      plugin = subject.createDeployPlugin({name:'my plugin' });
      context = {
        ui: mockUi,
        project: stubProject,
        config: { "my-plugin": {
            pluginClient: function(context) {
              return {
                upload: function(context) {
                  return Promise.resolve();
                }
              };
            }
          }
        }
      };
      return assert.isFulfilled(plugin.upload(context))
    });
  });
});

You should then update your package.json like this, and run your tests with npm test.

// package.json

"scripts": {
    "build": "ember build",
    "start": "ember server",
    "test": "./node_modules/mocha/bin/mocha tests/unit/index-nodetest.js"
  },