Skip to content

Latest commit

 

History

History
209 lines (173 loc) · 5.83 KB

plugins.md

File metadata and controls

209 lines (173 loc) · 5.83 KB

Authoring Plugins

A published plugin must be its own installable npm module, following the plugin naming convention. You may also create one-off plugins for an app.

A plugin's main export has the following shape:

module.exports = {
  name: 'awesome', // Required for published plugins. Should match the name in the module pattern '@gasket/{name}-plugin'.
  dependencies: [/* list of plugin names */],
  hooks: {
    hookA(gasket) {

    },
    async hookB(gasket) {

    },
    hookC: {
      timing: {},
      handler(gasket) {

      }
    }
  }
};

The optional dependencies array lists other plugins that must be installed as a prerequisite. The hooks map has a key for every event the plugin handles, with the values being either the function itself or an object specifying timing options and the handler.

Hooks

Each handler function assigned to an event is invoked by the plugin engine when an event occurs. If an event is used for collection of data, each callback should return data. If the handling is asynchronous, the callback should return a Promise.

If multiple plugins hook the same event, they'll run in parallel if async or in arbitrary order if synchronous. If a plugin wants to specify more specific sequencing, the hook should be an object with a timing property and handler function instead of a plain function. For example, this plugin contains an init hook that specifically runs after the metadata plugin:

module.exports = {
  name: 'will-run-after-metadata',
  hooks: {
    init: {
      timing: {
        after: [ 'metadata' ]
      },
      handler: async function initHook(gasket) {
        const { config } = gasket;
        console.log(config.name);
      }
    }
  }
}

In short, a hook should have the following type:

type HandlerFunction = (GasketAPI, ...args: any[]) => any;

type Hook = HandlerFunction | {
  timing: {
    before?: Array<string>,
    after?: Array<string>,
    first?: boolean,
    last?: boolean
  },
  handler: HandlerFunction
}

The before and other timing properties establish ordering with respect to other plugins. You can also use a first or last boolean instead to try to ensure that it runs first or last (if multiple plugins do this, they'll run in parallel).

The handler functions are called with a GasketAPI followed by any arguments passed when the event was invoked. You can see the full definitions for the functions available on a GasketAPI object here.

Testing

Because Gasket plugins are just Objects of functions, it's fairly trivial to test them. For example, let's say we have this plugin which hooks the start lifecycle.

module.exports = {
  name: 'detective',
  hooks: {
    start: async function gatherClues(gasket) {
      const { logger, exec } = gasket;
      const clues = await Promise.all([
        exec('motive'),
        exec('alibi')
      ]);

      logger.info(clues);
    },
    motive: function () {
      return 'That subtle off-white coloring';
    },
    alibi: function () {
      return 'Was returning some video tapes';
    }
  }
}

Here are some basic tests, assuming we're using the mocha test framework.

const plugin = require('/path/to/plugin');
const assume = require('assume');
const { stub }  = require('sinon');

describe('detective plugin', function () {
  it('hooks the correct lifecycles', function() {
    const hooks = plugin.hooks;
    assume(Object.keys(hooks)).equals(['init', 'motive', 'alibi']);
  });

  it('provides a motive lifecycle', function () {
    const name = plugin.hooks.motive();
    assume(name).contains('subtle');
  });

  it('executes the lifecyles for each clue', async function () {
    const execStub = stub();
    const logSub = stub();
    const gasket = {
      exec: execStub,
      logger: {
        info: logStub
      }
    };

    await plugin.hooks.start(gasket);

    assume(execStub.calledTwice).to.be.true();
    assume(execStub.calledWith('motive')).to.be.true();
    assume(execStub.calledWith('alibi')).to.be.true();

    assume(logStub.calledOnce).to.be.true();
  });
});

Documentation

If applications using your plugin are also using the @gasket/plugin-docs you can automatically view and generate docs for your application via the gasket docs command. To best take advantage of this functionality, you should provide a README.md enumerating documentation, as well as metadata hook to best illustrate which lifecycles are invoked.

module.exports = {
  name: 'detective',
  hooks: {
    // other hook implementations
    metadata(gasket, data) {
      return {
        ...data,
        lifecycles: [{
          name: 'motive',
          description: 'A reason for doing something.',
          method: 'exec',
          parent: 'start'
        }, {
          name: 'alibi',
          description: 'A claim or evidence that one was elsewhere during the act.',
          method: 'exec',
          parent: 'start'
        }]
      };
    }
  }
}

Then, upon running gasket docs, developers will automatically find documentation for the detective plugin.

One-off plugins

While it is encouraged to build plugins as separate packages, the ability to create one-off plugins in an app is available. Files in a plugins/ directory at the root of the Gasket project will automatically be loaded by the CLI for the engine. This gives you access to tie into lifecycles, set timings, or even add your own hooks.

These type of app-level plugins allow you to experiment quickly, before deciding what is best for reused across apps. If you find yourself duplicating app-level plugins across apps, be sure to extract it as an npm package which can be versioned, published, and imported to your different apps.