Featurevisor

Extending Featurevisor

Plugins

A plugin adds a command to the Featurevisor CLI. The built in commands use the same API, so a plugin can read every definition, whatever format or datasource the project uses.

Write a plugin

A plugin is an object with a command, a handler and examples. Plugins suit reports your team keeps asking for. This one lists every rule that is partly rolled out, which a weekly release review could start from:

plugins/rollouts.js
module.exports = {
command: 'rollouts',
description: 'List rules that are partly rolled out',
options: {
below: {
type: 'number',
description: 'Only list rules below this percentage',
},
},
handler: async function ({ datasource, parsed }) {
const below = parsed.below ?? 100
for (const key of await datasource.listFeatures()) {
const feature = await datasource.readFeature(key)
const lists = Array.isArray(feature.rules)
? { '': feature.rules }
: feature.rules
for (const [environment, rules] of Object.entries(lists)) {
for (const rule of rules) {
if (rule.percentage > 0 && rule.percentage < below) {
const where = environment ? ` (${environment})` : ''
console.log(`${key}${where}: ${rule.key} at ${rule.percentage}%`)
}
}
}
}
},
examples: [
{
command: 'rollouts',
description: 'list every partial rollout',
},
{
command: 'rollouts --below=50',
description: 'list rollouts under 50%',
},
],
}

Register it in the configuration:

featurevisor.config.js
module.exports = {
tags: ['web', 'mobile'],
plugins: [require('./plugins/rollouts')],
}

Run it like any other command:

Command
npx featurevisor rollouts
Output
checkout: everyone at 25%

description appears next to the command in npx featurevisor --help. Without it, the first example's description is used.

Options

Declare options and the CLI checks every option before your handler runs, as it does for built in commands. A typo stops the command with a suggestion:

Output
Unknown argument: belo. Did you mean "--below"?

Each option has a type: string, number, boolean or array, the last for an option given more than once. It can also have an alias, a list of choices, a description for the help, demandOption: true when it is required, and hidden: true to leave it out of the help.

A plugin without options accepts any argument, so a typo reaches the handler unchecked. Declare them in any plugin others will use.

Handler

The handler receives:

PropertyContains
rootDirectoryPathThe project's root directory
projectConfigThe resolved configuration, with every default filled in
parsedThe parsed command line arguments, such as { below: 50 }
datasourceReads and writes definitions, described below
cliVersionThe CLI's version

Return false to make the command exit with an error.

Reading and writing definitions

Use datasource rather than the file system. It follows the project's parser, directories and adapter, so the plugin works unchanged in a JSON or TOML project. Every kind of definition has the same methods:

const keys = await datasource.listFeatures()
const exists = await datasource.featureExists('checkout')
const checkout = await datasource.readFeature('checkout')
await datasource.writeFeature('checkout', checkout)
await datasource.deleteFeature('checkout')

Segments, attributes, global variables, schemas, targets and scenarios have the same five, and tests have all but an exists check. There are also methods for the revision and state, built datafiles and Git history. See Datasource.

In a project with sets, choose a set first:

const storefront = datasource.forSet('storefront')
const keys = await storefront.listFeatures()

TypeScript

The Plugin type from @featurevisor/core checks a plugin's shape:

plugins/rollouts.ts
import type { Plugin } from '@featurevisor/core'
const rollouts: Plugin = {
command: 'rollouts',
handler: async function ({ datasource, parsed }) {
// ...
},
examples: [],
}
export default rollouts

Share a plugin

Publish a plugin to npm to use it in several projects. Export a function that takes options and returns the plugin, so each project can configure it:

index.js
module.exports = function rollouts({ below = 100 } = {}) {
return {
command: 'rollouts',
handler: async function ({ datasource, parsed }) {
const threshold = parsed.below ?? below
// ...
},
examples: [],
}
}
featurevisor.config.js
module.exports = {
plugins: [require('featurevisor-plugin-rollouts')({ below: 50 })],
}

Command names must be unique. Registering a command that a built in command or another plugin already has fails at once, so choose a name unlikely to clash with commands Featurevisor may add later.

Previous
Custom parsers