1.1.1 • Published 12 months ago

@rmtc/plugin v1.1.1

Weekly downloads
-
License
MIT
Repository
github
Last release
12 months ago

@rmtc/plugin

A base class to be extended by other @rmtc/toolchain plugins.

!TIP This package forms part of @rmtc/toolchain. You'll find further documentation in the main project README.

!WARNING This project is intended for use in @rowanmanning's projects. It's free to use but I don't offer support for use-cases outside of what I need.

Table of Contents

Requirements

This library requires the following to run:

Usage

This module is intended to be installed as a dependency for @rmtc/toolchain plugins. For a plugin to load it'll need to extend the Plugin class.

Install the module with npm:

npm install @rmtc/plugin

Now you can import the Plugin class and extend it. This is the minimal code for a plugin, but it doesn't do anything:

const { Plugin } = require('@rmtc/plugin');

exports.Plugin = class Example extends Plugin {}

To make changes to the toolchain, you'll need to override certain methods in the class and call others.

Override methods

There are several methods that a subclass of Plugin can override to make changes to the toolchain. We'll cover them here.

These methods alone don't make a plugin, you'll also need the utility methods to complete a plugin.

init method

The init method is called automatically when a plugin is initialised and loaded into the toolchain via a config file. It's better to override this method rather than the constructor because it has a much simpler signature and you can guarantee that the plugin is fully set up.

Consider the init method to be where you define the workflows and steps for your plugin and get everything set up:

exports.Plugin = class Example extends Plugin {
    init() {
        // Setup code goes here
    }
}

The method signature is very simple. It accepts no arguments and returns nothing:

() => void

configure method

The configure method is called before init and is used to change, provide defaults for, or validate the configuration that a user has passed into your plugin.

The method is passed a config object and is expected to return one as well:

type ConfigObject = {[key: string]: any}
(config: ConfigObject) => ConfigObject

Here's an example of defaulting some configuration:

exports.Plugin = class Example extends Plugin {
    configure(config) {
        return Object.assign({
            myConfiguration: 'default value'
        }, config);
    }
}

Here's an example of validating the configuration, also using the ConfigError class from @rmtc/config:

const {ConfigError} = require('@rmtc/config');

exports.Plugin = class Example extends Plugin {
    configure(config) {
        if (typeof config.myConfiguration !== 'string') {
            throw new ConfigError('The myConfiguration option must be a string');
        }

        // Don't forget to return the config still
        return config;
    }
}

Utilities

The rest of the properties and methods on the Plugin class can be used as you see fit in your init and configure methods.

this.config

{[key: string]: any}. A read-only property that contains the plugin configuration found in the project's .rmtc.json5 config file. It will also be the exact value you return from your configure method if you specify one.

this.project

Project. A read-only property that contains a representation of the project the plugin is installed in.

The most common use-case would be to access this.project.directoryPath which can be used to make file system operations relative to the project, e.g. reading a package.json file.

See the @rmtc/project documentation for more information.

this.log

A Logger instance that can be used to consistently output messages to the console. It's best to use this rather than console.log.

this.availableWorkflows

string[]. A read-only list of the workflows that have been defined by other plugins that were set up ahead of this one.

this.defineStep()

Used to define a step. If the step already exists then an error will be thrown, so it's best to name your steps in a way that won't conflict with other plugins. The method signature is:

type StepParams = {environment: "local" | "ci"}
type StepExecutor = (params: StepParams) => Promise<void>
(name: string, executor: StepExecutor) => void

When defining a step, you're giving it a name and a function to run when that step is executed as part of a workflow. This "executor" is an async function that can do anything you like. It's called with some parameters which, along with this.config, allows you to change your plugin's behaviour.

A simple step definition looks like this:

exports.Plugin = class Example extends Plugin {
    init() {
        this.defineStep('myStep', async () => {
            this.log('my step happened');
        });
    }
}

But you can use the fact that a plugin is a class to help organise, defining your own methods to handle each step:

exports.Plugin = class Example extends Plugin {
    init() {
        this.defineStep('myStep', this.myStep.bind(this));
    }
    async myStep() {
        this.log('my step happened');
    }
}

Steps can then be added to workflows (see below), or just left in place for users to add to their own workflows.

this.defineWorkflow()

Used to define a workflow. If the workflow already exists then it is not re-created, so it's safe to call this method multiple times. This is also useful for adding steps to shared generic workflows that other plugins may use (e.g. build, verify, test). The method signature is:

(name: string, defaultSteps?: string[]) => void

You may optionally specify some steps that should be added to this workflow by default. These steps do not override steps added to the workflow by other plugins.

This method is best used in the init method after steps have been defined:

exports.Plugin = class Example extends Plugin {
    init() {
        this.defineStep('myStep', this.myStep.bind(this));
        this.defineWorkflow('test', ['myStep']);
    }
    async myStep() {
        this.log('my step happened');
    }
}

this.exec()

This is a shortcut method for executing Node.js-based commands within the context of the project. It's a wrapper around the built-in child_process API that is more convenient to use in a plugin. The method signature is:

(command: string, args?: string[]) => Promise<void>

If the command that you're calling exits with a non-0 code then an error will be thrown and execution will stop. All stdout from the command will be piped directly into the workflow output.

exports.Plugin = class Example extends Plugin {
    init() {
        this.defineStep('myStep', this.myStep.bind(this));
        this.defineWorkflow('test', ['myStep']);
    }
    async myStep() {
        await this.exec('cowsay', ['my step happened']);
    }
}

It's important to note that all commands are prefixed with npx, the intention being that this works best with Node.js-based command line tools.

this.editJsonFile()

This is a shortcut method for editing the contents of JSON files in the project. The method signature is:

type Transformer = (json: any) => any
(filePath: string, transformer: Transformer) => Promise<void>

The transformer function is called with a copy of the parsed JSON and expects you to return this with any edits you want to make. The edited JSON is then written back to the file if there are any changes. E.g.

exports.Plugin = class Example extends Plugin {
    init() {
        this.defineStep('myStep', this.myStep.bind(this));
        this.defineWorkflow('test', ['myStep']);
    }
    async myStep() {
        await this.editJsonFile('package.json', (packageJson) => {
            if (Array.isArray(packageJson?.keywords)) {
                packageJson.keywords.push('edited');
            }
            return packageJson;
        });
    }
}

Contributing

See the central README for a contribution guide and code of conduct.

License

Licensed under the MIT license. Copyright © 2023, Rowan Manning