@rmtc/plugin v1.1.1
@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:
- Node.js 18+
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