|
| 1 | +--- |
| 2 | +title: Authoring plugins |
| 3 | +--- |
| 4 | + |
| 5 | +import Tabs from '@theme/Tabs'; |
| 6 | +import TabItem from '@theme/TabItem'; |
| 7 | + |
| 8 | +Heft is designed to be extensible. If the [official plugins](../plugins/package_index.md) don't cover your |
| 9 | +needs, you can create your own Heft plugin package. This page explains how to create a task plugin, which |
| 10 | +is the most common kind of plugin. |
| 11 | + |
| 12 | +> For quick prototyping, you may want to start with the [Run script plugin](../plugins/run-script.md) instead. |
| 13 | +> That approach lets you run an arbitrary script as a Heft task without creating a full plugin package. However, |
| 14 | +> for production use, a proper plugin package is recommended because it provides TypeScript type safety, proper |
| 15 | +> ESLint validation, and a better developer experience. |
| 16 | +
|
| 17 | +## Plugin concepts |
| 18 | + |
| 19 | +Before you begin, make sure you're familiar with the key concepts from the |
| 20 | +[Heft architecture](../intro/architecture.md) page: |
| 21 | + |
| 22 | +- A **task plugin** implements `IHeftTaskPlugin` and provides its behavior via the task session hooks |
| 23 | +- A **lifecycle plugin** implements `IHeftLifecyclePlugin` and provides general functionality not specific to any task |
| 24 | +- A **plugin manifest** (`heft-plugin.json`) describes available plugins, their options, and CLI parameters |
| 25 | +- Plugin packages use the NPM naming pattern `heft-____-plugin` or `heft-____-plugins` |
| 26 | + |
| 27 | +## Step 1: Create the package |
| 28 | + |
| 29 | +Create a new NPM package for your plugin. The key requirements are: |
| 30 | + |
| 31 | +- Add `@rushstack/heft` as a `peerDependency` (not a regular dependency) |
| 32 | +- Export a `heft-plugin.json` manifest file from the package root |
| 33 | + |
| 34 | +**package.json** |
| 35 | + |
| 36 | +```json |
| 37 | +{ |
| 38 | + "name": "heft-my-plugin", |
| 39 | + "version": "1.0.0", |
| 40 | + "description": "A Heft plugin for ...", |
| 41 | + "main": "./lib-commonjs/index.js", |
| 42 | + "peerDependencies": { |
| 43 | + "@rushstack/heft": "^1.2.2" |
| 44 | + }, |
| 45 | + "devDependencies": { |
| 46 | + "@rushstack/heft": "^1.2.2" |
| 47 | + }, |
| 48 | + "exports": { |
| 49 | + "./lib/*": { |
| 50 | + "types": "./lib-dts/*.d.ts", |
| 51 | + "node": "./lib-commonjs/*.js", |
| 52 | + "import": "./lib-esm/*.js", |
| 53 | + "require": "./lib-commonjs/*.js" |
| 54 | + }, |
| 55 | + "./heft-plugin.json": "./heft-plugin.json", |
| 56 | + "./package.json": "./package.json" |
| 57 | + } |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +> **Important:** It is essential that `"./heft-plugin.json": "./heft-plugin.json"` is included in |
| 62 | +> the `"exports"` field so that Heft can locate the plugin manifest. |
| 63 | +
|
| 64 | +## Step 2: Create the plugin manifest |
| 65 | + |
| 66 | +Create a **heft-plugin.json** file in your package root. This manifest tells Heft about the plugins |
| 67 | +provided by your package. The file must conform to the |
| 68 | +[heft-plugin.schema.json](https://developer.microsoft.com/json-schemas/heft/v0/heft-plugin.schema.json) schema. |
| 69 | + |
| 70 | +**heft-plugin.json** |
| 71 | + |
| 72 | +```json |
| 73 | +{ |
| 74 | + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-plugin.schema.json", |
| 75 | + |
| 76 | + "taskPlugins": [ |
| 77 | + { |
| 78 | + "pluginName": "my-plugin", |
| 79 | + "entryPoint": "./lib-commonjs/MyPlugin" |
| 80 | + } |
| 81 | + ] |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +### Manifest fields |
| 86 | + |
| 87 | +The plugin manifest supports these fields for each plugin: |
| 88 | + |
| 89 | +| Field | Required | Description | |
| 90 | +| :--------------- | :------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
| 91 | +| `pluginName` | yes | A unique kebab-case name for the plugin (e.g. `"my-plugin"`) | |
| 92 | +| `entryPoint` | yes | Path to the compiled JS module that exports the plugin class (resolved relative to the package folder) | |
| 93 | +| `optionsSchema` | no | Path to a JSON Schema file that validates `options` provided in **heft.json** | |
| 94 | +| `parameterScope` | no | A scope prefix for CLI parameters (defaults to the plugin name). If multiple plugins define a `--verbose` parameter, they can be disambiguated as `--my-scope:verbose`. | |
| 95 | +| `parameters` | no | An array of CLI parameter definitions (see [Defining CLI parameters](#defining-cli-parameters) below) | |
| 96 | + |
| 97 | +## Step 3: Implement the plugin class |
| 98 | + |
| 99 | +Create a TypeScript file that exports a class implementing `IHeftTaskPlugin`. The class must have an |
| 100 | +`apply()` method that receives the task session and Heft configuration. |
| 101 | + |
| 102 | +**src/MyPlugin.ts** |
| 103 | + |
| 104 | +```ts |
| 105 | +import type { |
| 106 | + HeftConfiguration, |
| 107 | + IHeftTaskSession, |
| 108 | + IHeftTaskPlugin, |
| 109 | + IHeftTaskRunHookOptions |
| 110 | +} from '@rushstack/heft'; |
| 111 | + |
| 112 | +const PLUGIN_NAME: 'my-plugin' = 'my-plugin'; |
| 113 | + |
| 114 | +export default class MyPlugin implements IHeftTaskPlugin { |
| 115 | + public apply(taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration): void { |
| 116 | + taskSession.hooks.run.tapPromise(PLUGIN_NAME, async (runOptions: IHeftTaskRunHookOptions) => { |
| 117 | + const { logger } = taskSession; |
| 118 | + |
| 119 | + logger.terminal.writeLine('My plugin is running!'); |
| 120 | + |
| 121 | + // Your plugin logic here... |
| 122 | + }); |
| 123 | + } |
| 124 | +} |
| 125 | +``` |
| 126 | + |
| 127 | +The `apply()` method is called once when the plugin is loaded. Inside it, you "tap" into one or more |
| 128 | +hooks to register your callbacks. |
| 129 | + |
| 130 | +### Available task hooks |
| 131 | + |
| 132 | +The `taskSession.hooks` object provides these hooks: |
| 133 | + |
| 134 | +| Hook | Description | |
| 135 | +| :----------------------- | :------------------------------------------------------------------------------------------------------- | |
| 136 | +| `run` | Called when the task executes during a normal build. Use `run.tapPromise(name, callback)` to register. | |
| 137 | +| `runIncremental` | Called instead of `run` during watch mode, if provided. Receives additional APIs for incremental builds. | |
| 138 | +| `registerFileOperations` | Called once before the first `run` or `runIncremental` to register dynamic file copy/delete operations. | |
| 139 | + |
| 140 | +### Using the task session |
| 141 | + |
| 142 | +The `IHeftTaskSession` provides access to several useful properties: |
| 143 | + |
| 144 | +| Property | Description | |
| 145 | +| :------------------ | :------------------------------------------------------------- | |
| 146 | +| `taskName` | The name of the task as defined in **heft.json** | |
| 147 | +| `hooks` | The hooks available for the plugin to tap (described above) | |
| 148 | +| `parameters` | CLI parameters (including custom ones defined in the manifest) | |
| 149 | +| `parsedCommandLine` | The command line used to invoke Heft | |
| 150 | +| `tempFolderPath` | A unique temp folder for the task, cleaned with `--clean` | |
| 151 | +| `logger` | A scoped logger prefixed with `[<phaseName>:<taskName>]` | |
| 152 | + |
| 153 | +### Watch mode support |
| 154 | + |
| 155 | +To support Heft's watch mode, tap the `runIncremental` hook. This hook provides additional APIs |
| 156 | +for efficient incremental builds: |
| 157 | + |
| 158 | +```ts |
| 159 | +taskSession.hooks.runIncremental.tapPromise( |
| 160 | + PLUGIN_NAME, |
| 161 | + async (options: IHeftTaskRunIncrementalHookOptions) => { |
| 162 | + // Watch for changes to specific files |
| 163 | + const changedFiles = await options.watchGlobAsync('src/**/*.json'); |
| 164 | + |
| 165 | + // Process only changed files |
| 166 | + for (const [filePath, changeInfo] of changedFiles) { |
| 167 | + logger.terminal.writeLine(`Processing changed file: ${filePath}`); |
| 168 | + } |
| 169 | + } |
| 170 | +); |
| 171 | +``` |
| 172 | + |
| 173 | +## Step 4: Use the plugin |
| 174 | + |
| 175 | +Once your plugin is published (or linked locally), consumers can load it in their **heft.json** config file: |
| 176 | + |
| 177 | +**config/heft.json** |
| 178 | + |
| 179 | +```json |
| 180 | +{ |
| 181 | + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", |
| 182 | + |
| 183 | + "phasesByName": { |
| 184 | + "build": { |
| 185 | + "tasksByName": { |
| 186 | + "my-task": { |
| 187 | + "taskPlugin": { |
| 188 | + "pluginPackage": "heft-my-plugin", |
| 189 | + "pluginName": "my-plugin" |
| 190 | + } |
| 191 | + } |
| 192 | + } |
| 193 | + } |
| 194 | + } |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +If the package only provides a single task plugin, the `"pluginName"` can be omitted. |
| 199 | + |
| 200 | +## Accepting plugin options |
| 201 | + |
| 202 | +Plugins can accept user-defined options via **heft.json**. To enable this: |
| 203 | + |
| 204 | +1. Create a JSON Schema file for your options: |
| 205 | + |
| 206 | + **src/schemas/my-plugin-options.schema.json** |
| 207 | + |
| 208 | + ```json |
| 209 | + { |
| 210 | + "$schema": "http://json-schema.org/draft-04/schema#", |
| 211 | + "type": "object", |
| 212 | + "additionalProperties": false, |
| 213 | + "properties": { |
| 214 | + "outputFolder": { |
| 215 | + "type": "string", |
| 216 | + "description": "The output folder for generated files." |
| 217 | + } |
| 218 | + }, |
| 219 | + "required": ["outputFolder"] |
| 220 | + } |
| 221 | + ``` |
| 222 | + |
| 223 | +2. Reference the schema from your plugin manifest: |
| 224 | + |
| 225 | + **heft-plugin.json** |
| 226 | + |
| 227 | + ```json |
| 228 | + { |
| 229 | + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-plugin.schema.json", |
| 230 | + "taskPlugins": [ |
| 231 | + { |
| 232 | + "pluginName": "my-plugin", |
| 233 | + "entryPoint": "./lib-commonjs/MyPlugin", |
| 234 | + "optionsSchema": "./lib-commonjs/schemas/my-plugin-options.schema.json" |
| 235 | + } |
| 236 | + ] |
| 237 | + } |
| 238 | + ``` |
| 239 | + |
| 240 | +3. Consumers specify the options in **heft.json**: |
| 241 | + |
| 242 | + ```json |
| 243 | + { |
| 244 | + "tasksByName": { |
| 245 | + "my-task": { |
| 246 | + "taskPlugin": { |
| 247 | + "pluginPackage": "heft-my-plugin", |
| 248 | + "options": { |
| 249 | + "outputFolder": "dist/generated" |
| 250 | + } |
| 251 | + } |
| 252 | + } |
| 253 | + } |
| 254 | + } |
| 255 | + ``` |
| 256 | + |
| 257 | +The options are validated against your schema at load time, providing clear error messages for |
| 258 | +invalid configurations. |
| 259 | + |
| 260 | +## Defining CLI parameters |
| 261 | + |
| 262 | +Plugins can define CLI parameters by adding a `parameters` array to the plugin manifest. |
| 263 | +These parameters are automatically added to Heft's command line when the plugin is loaded. |
| 264 | + |
| 265 | +**heft-plugin.json** |
| 266 | + |
| 267 | +```json |
| 268 | +{ |
| 269 | + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-plugin.schema.json", |
| 270 | + "taskPlugins": [ |
| 271 | + { |
| 272 | + "pluginName": "my-plugin", |
| 273 | + "entryPoint": "./lib-commonjs/MyPlugin", |
| 274 | + "parameterScope": "my-plugin", |
| 275 | + "parameters": [ |
| 276 | + { |
| 277 | + "longName": "--output-format", |
| 278 | + "parameterKind": "choice", |
| 279 | + "description": "Specifies the output format.", |
| 280 | + "alternatives": [ |
| 281 | + { "name": "json", "description": "Output as JSON" }, |
| 282 | + { "name": "yaml", "description": "Output as YAML" } |
| 283 | + ], |
| 284 | + "defaultValue": "json" |
| 285 | + }, |
| 286 | + { |
| 287 | + "longName": "--verbose", |
| 288 | + "parameterKind": "flag", |
| 289 | + "description": "Enable verbose logging." |
| 290 | + } |
| 291 | + ] |
| 292 | + } |
| 293 | + ] |
| 294 | +} |
| 295 | +``` |
| 296 | + |
| 297 | +The supported parameter kinds are: `flag`, `string`, `stringList`, `integer`, `integerList`, |
| 298 | +`choice`, and `choiceList`. |
| 299 | + |
| 300 | +To read the parameter values in your plugin, use `taskSession.parameters`: |
| 301 | + |
| 302 | +```ts |
| 303 | +public apply(taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration): void { |
| 304 | + taskSession.hooks.run.tapPromise(PLUGIN_NAME, async () => { |
| 305 | + const verbose = taskSession.parameters.getFlagParameter('--verbose'); |
| 306 | + if (verbose.value) { |
| 307 | + taskSession.logger.terminal.writeLine('Verbose mode enabled'); |
| 308 | + } |
| 309 | + }); |
| 310 | +} |
| 311 | +``` |
| 312 | + |
| 313 | +## Interacting with other plugins |
| 314 | + |
| 315 | +A plugin can request access to another plugin within the same phase using the |
| 316 | +`requestAccessToPluginByName()` API. This enables plugins to share data via custom accessor hooks. |
| 317 | + |
| 318 | +```ts |
| 319 | +taskSession.requestAccessToPluginByName( |
| 320 | + '@rushstack/heft-typescript-plugin', // the package containing the plugin |
| 321 | + 'typescript-plugin', // the plugin name |
| 322 | + (accessor) => { |
| 323 | + // Access hooks or data exposed by the TypeScript plugin |
| 324 | + } |
| 325 | +); |
| 326 | +``` |
| 327 | + |
| 328 | +## Reference examples |
| 329 | + |
| 330 | +The best way to learn plugin development is to study the official plugin implementations: |
| 331 | + |
| 332 | +- [heft-dev-cert-plugin](https://github.com/microsoft/rushstack/tree/main/heft-plugins/heft-dev-cert-plugin) — A simple task plugin example |
| 333 | +- [heft-jest-plugin](https://github.com/microsoft/rushstack/tree/main/heft-plugins/heft-jest-plugin) — A complex plugin with many CLI parameters |
| 334 | +- [heft-typescript-plugin](https://github.com/microsoft/rushstack/tree/main/heft-plugins/heft-typescript-plugin) — The TypeScript compiler plugin |
| 335 | +- [heft-rspack-plugin](https://github.com/microsoft/rushstack/tree/main/heft-plugins/heft-rspack-plugin) — A plugin with watch mode support |
| 336 | + |
| 337 | +## See also |
| 338 | + |
| 339 | +- [Heft architecture](../intro/architecture.md) — Key concepts and terminology |
| 340 | +- [heft.json](../configs/heft_json.md) — Config file reference for loading plugins |
| 341 | +- [Run script plugin](../plugins/run-script.md) — A simpler alternative for quick prototyping |
| 342 | +- [Plugin package index](../plugins/package_index.md) — List of official plugins |
| 343 | +- [API Reference](https://api.rushstack.io/pages/heft/) — Complete Heft API documentation |
0 commit comments