Skip to content

Writing Plugins

bonvoy's plugin system is built on tapable. A plugin is a class that taps into lifecycle hooks.

Minimal Plugin

typescript
import type { BonvoyPlugin } from '@bonvoy/core';
import type { Bonvoy } from '@bonvoy/core';

export default class MyPlugin implements BonvoyPlugin {
  name = 'my-plugin';

  apply(bonvoy: Bonvoy): void {
    bonvoy.hooks.afterRelease.tap(this.name, (context) => {
      console.log(`Released ${context.changedPackages.length} packages!`);
    });
  }
}

With Configuration

typescript
import type { BonvoyPlugin, ReleaseContext } from '@bonvoy/core';
import type { Bonvoy } from '@bonvoy/core';

interface SlackConfig {
  webhookUrl: string;
  channel?: string;
}

export default class SlackPlugin implements BonvoyPlugin {
  name = 'slack';
  private config: SlackConfig;

  constructor(config: SlackConfig) {
    this.config = config;
  }

  apply(bonvoy: Bonvoy): void {
    bonvoy.hooks.afterRelease.tapPromise(this.name, async (context: ReleaseContext) => {
      if (context.isDryRun) return;

      const packages = context.changedPackages
        .map(p => `${p.name}@${context.versions[p.name]}`)
        .join(', ');

      await fetch(this.config.webhookUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          text: `🚢 Released: ${packages}`,
        }),
      });
    });
  }
}

Users configure it:

javascript
// bonvoy.config.js
export default {
  plugins: [
    ['./my-slack-plugin.js', { webhookUrl: 'https://...' }],
  ],
};

Hook Types

Sync Hooks

Use tap for synchronous operations:

typescript
bonvoy.hooks.validateRepo.tap(this.name, (context) => {
  if (!context.config.baseBranch) {
    throw new Error('baseBranch is required');
  }
});

Async Hooks

Use tapPromise for async operations:

typescript
bonvoy.hooks.publish.tapPromise(this.name, async (context) => {
  await publishToRegistry(context.packages);
});

Waterfall Hooks

Some hooks pass a value through the chain. Return the modified value:

typescript
bonvoy.hooks.modifyConfig.tap(this.name, (config) => {
  return { ...config, tagFormat: 'v{version}' };
});

bonvoy.hooks.getVersion.tap(this.name, (context) => {
  return 'minor';  // force minor bump
});

Available Hooks

See the Hooks Reference for the complete list.

Context Objects

Each hook receives a context object. The context grows as the release progresses:

PhaseContextExtra fields
ValidationContextconfig, packages, commits, isDryRun
VersionVersionContext+ versions, bumps
ChangelogChangelogContext+ changelogs
PublishPublishContext+ publishedPackages, preid
ReleaseReleaseContext+ releases
PRPRContextbranchName, baseBranch, title, body

Testing Plugins

typescript
import { Bonvoy } from '@bonvoy/core';
import { describe, expect, it } from 'vitest';
import MyPlugin from './my-plugin.js';

describe('MyPlugin', () => {
  it('should tap into afterRelease', () => {
    const bonvoy = new Bonvoy();
    const plugin = new MyPlugin();
    plugin.apply(bonvoy);

    // Verify the hook was tapped
    expect(bonvoy.hooks.afterRelease.taps).toHaveLength(1);
  });
});

Publishing Your Plugin

Name your package bonvoy-plugin-<name> or @scope/bonvoy-plugin-<name> so it's discoverable on npm.

Released under the MIT License.