Plugins
ExtForge plugins let you hook into every stage of the build: config resolution, manifest generation, entry point transformation, and dev reloads. Built-in presets (presetReact()) are plugins too — they use the same API.
The plugin shape
Section titled “The plugin shape”A v1 plugin is a plain object:
import type { ExtForgePluginV1 } from 'extforge';
const myPlugin = (): ExtForgePluginV1 => ({ name: 'my-plugin', apiVersion: 1, setup(ctx) { // register hooks via ctx.hooks.* },});| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
yes | Unique name; appears in logs and error messages |
apiVersion |
1 |
yes | Discriminates from the legacy shape |
setup |
(ctx: PluginContext) => void | Promise<void> |
yes | Called once after config resolution; register hooks here |
The ctx object passed to setup gives you the resolved config, project paths, a scoped logger, and the hook registration methods.
Hooks summary
Section titled “Hooks summary”All hooks are registered inside setup via ctx.hooks:
| Hook | When it fires | Signature |
|---|---|---|
onConfigResolved |
Once, after config is loaded | (config: ExtForgeConfig) => void |
onManifestTransform |
Once per browser, after manifest assembly | (manifest, browser) => manifest |
onBuildStart |
Once per browser build | ({ browser, dev }) => void |
onBuildEntry |
Once per entry point | (entry) => entry | void |
onBuildEnd |
Once per browser, after all entries bundled | (result) => void |
onCssTransform |
Per stylesheet, after the base CSS processor | (ctx) => string | void |
onDevReload |
Each HMR reload event in dev mode | (event) => void |
See /reference/plugins/api/ for full type signatures.
Plugin ordering
Section titled “Plugin ordering”Plugins run in the order they are resolved:
- Built-in plugins (
presetReact(), etc.) — injected first based onframework - User plugins — in the order declared in
plugins: [...]
For reduce-style hooks (onManifestTransform and onBuildEntry), the output of each plugin is the input to the next. This means the first plugin in the chain sees the base manifest; the last sees the fully transformed one. Order matters if two plugins modify the same field.
Worked example 1: a CSS processor plugin
Section titled “Worked example 1: a CSS processor plugin”onCssTransform runs for every stylesheet ExtForge emits, after the base CSS
strategy (config.css) has run. Each hook receives the previous step’s output
as ctx.code, so plugins can chain. Here’s a plugin that runs every stylesheet
through Lightning CSS for autoprefixing and
minification:
import type { ExtForgePluginV1 } from 'extforge';import { transform } from 'lightningcss';
export function presetLightning(): ExtForgePluginV1 { return { name: 'extforge:preset-lightning', apiVersion: 1, setup({ hooks }) { hooks.onCssTransform((ctx) => { const { code } = transform({ filename: ctx.file, code: Buffer.from(ctx.code), minify: !ctx.dev, }); return code.toString(); }); }, };}Register it in your config:
import { defineConfig } from 'extforge';import { presetLightning } from './plugins/preset-lightning';
export default defineConfig({ plugins: [presetLightning()],});Worked example 2: manifest stamp plugin
Section titled “Worked example 2: manifest stamp plugin”A plugin that adds a custom field to the generated manifest using onManifestTransform:
import type { ExtForgePluginV1 } from 'extforge';
interface StampOptions { buildId: string;}
export function manifestStamp(options: StampOptions): ExtForgePluginV1 { return { name: 'extforge:manifest-stamp', apiVersion: 1, setup({ hooks }) { hooks.onManifestTransform((manifest, browser) => ({ ...manifest, // Chrome allows arbitrary extra fields; other browsers may strip them _buildId: options.buildId, _builtFor: browser, })); }, };}Usage:
export default defineConfig({ plugins: [ manifestStamp({ buildId: process.env.GITHUB_SHA ?? 'local' }), ],});onManifestTransform receives the current manifest object and must return the (possibly modified) manifest. The browser argument lets you apply overrides only for specific targets.
Error handling
Section titled “Error handling”If a plugin throws during setup or any hook, ExtForge wraps the error in EXT_PLUGIN_FAILED and surfaces the plugin name and hook name alongside the original message:
EXT_PLUGIN_FAILED Plugin "extforge:manifest-stamp" failed in onManifestTransform: Cannot read property 'x' of undefinedThe build exits with code 1. Fix the plugin and re-run.
Legacy plugin shape
Section titled “Legacy plugin shape”ExtForge still accepts the pre-v1 shape for backwards compatibility:
const legacyPlugin = { name: 'legacy-compat', // no apiVersion field setup(config) { /* config is ExtForgeConfig */ }, buildStart() { /* called before each build */ }, buildEnd(result) { /* called after each build */ },};The runner detects the missing apiVersion and adapts the legacy plugin automatically. Legacy plugins do not have access to onManifestTransform, onBuildEntry, or onDevReload. Migrate to apiVersion: 1 to use those hooks.
See /reference/plugins/api/ for the complete hook type reference, and /reference/plugins/preset-react/ for the built-in React preset docs.