Skip to content

Plugin API

The complete reference for the Plugin interface — every hook, its signature, when it runs, and what it returns. A plugin is a plain object with a name and any subset of these hooks. Every hook is optional.

ts
import type { Plugin } from "vantris";

definePlugin

A typed identity helper that gives you autocompletion and checking while authoring. It returns its argument unchanged.

ts
import { definePlugin } from "vantris";

export default definePlugin({
  name: "my-plugin",
  transform(code, id) {
    return null;
  },
});

You can also export a function returning a plugin, which is useful for options:

ts
export function myPlugin(options: MyOptions = {}): Plugin {
  return definePlugin({ name: "my-plugin", /* … uses options … */ });
}

name

  • Type: stringrequired

Unique, human-readable name. Shown in logs and errors, and used to attribute warnings/errors raised through the plugin context.

enforce

  • Type: "pre" | "post"
  • Default: unset (normal)

Ordering bucket relative to other plugins. pre plugins run first, then unenforced plugins, then post. See Plugins → Ordering.

Configuration hooks

config

ts
config?(
  config: Config,
  env: ConfigEnv,
): Config | null | void | Promise<Config | null | void>;
  • Runs: dev + build, once, before defaults are applied.

Modify the resolved-but-not-yet-finalised user config, or return a partial config to deep-merge into it. env tells you which command is running.

ts
config(config, env) {
  if (env.command === "build") {
    return { define: { __PROD__: true } };
  }
}

ConfigEnv:

ts
interface ConfigEnv {
  mode: string;                  // e.g. "development", "production"
  command: "serve" | "build";    // the command in flight
}

configResolved

ts
configResolved?(config: ResolvedConfig): void | Promise<void>;
  • Runs: dev + build.

Read the fully resolved config (defaults applied, paths absolute). Capture values your plugin needs later. Read-only — don't mutate here.

ts
let root = "";
export default definePlugin({
  name: "x",
  configResolved(config) {
    root = config.paths.root;
  },
});

configureServer

ts
configureServer?(server: DevServerHandle): void | Promise<void>;
  • Runs: dev only, once, after the server starts.

Access the running dev server — its URL and the HMR engine for custom events.

ts
configureServer(server) {
  server.hmr.send("my:ready", { at: Date.now() });
}

transformIndexHtml

ts
transformIndexHtml?(html: string): string | null | void | Promise<string | null | void>;
  • Runs: dev + build. In dev, on the served page before the HMR client is injected; in the build, on each emitted page.

Rewrite the HTML entry document. Return the new HTML, or null/void to leave it unchanged.

ts
transformIndexHtml(html) {
  return html.replace("</head>", `<meta name="built" content="${Date.now()}"></head>`);
}

Module pipeline hooks

These run with a this PluginContext.

buildStart

ts
buildStart?(this: PluginContext): void | Promise<void>;
  • Runs: build, once before the module pipeline starts.

Initialise state, emit initial assets, or add watch files.

resolveId

ts
resolveId?(
  this: PluginContext,
  source: string,
  importer: string | undefined,
): string | ResolveIdResult | null | void | Promise<>;
  • Runs: dev + build.

Resolve an import specifier to a module id. The first plugin returning a non-null value wins. Return an id string, or a ResolveIdResult. A virtual id (e.g. prefixed with \0) is then served from this plugin's load.

ts
resolveId(source) {
  if (source === "virtual:config") return "\0virtual:config";
  return null;
}

ResolveIdResult:

ts
interface ResolveIdResult {
  id: string;         // resolved id (absolute path or virtual id)
  external?: boolean; // leave the import in place instead of bundling/serving
}

load

ts
load?(
  this: PluginContext,
  id: string,
): string | TransformResult | null | void | Promise<>;
  • Runs: dev + build.

Provide a module's source instead of reading it from disk — for virtual modules and codegen. First non-null wins. Return the source string or a TransformResult.

ts
load(id) {
  if (id === "\0virtual:config") {
    return `export default ${JSON.stringify({ version: "1.0.0" })};`;
  }
  return null;
}

transform

ts
transform?(
  this: PluginContext,
  code: string,
  id: string,
): string | TransformResult | null | void | Promise<>;
  • Runs: dev + build.

Transform a module's code. Plugins run in order, each receiving the previous one's output. code is the module source and id its absolute path (or a virtual id). Return the new code (with an optional source map) or null to skip.

ts
transform(code, id) {
  if (!id.endsWith(".ts")) return null;
  return { code: `${code}\n/* transformed */`, map: null };
}

transform runs on the module source in both dev and build, so ordering is consistent and source maps stay accurate.

buildEnd

ts
buildEnd?(this: PluginContext, error?: Error): void | Promise<void>;
  • Runs: build, once when the pipeline finishes (or errors — error is set).

Flush state, emit final assets, or clean up.

HMR hook

handleHotUpdate

ts
handleHotUpdate?(
  ctx: HotUpdateContext,
): void | ModuleNode[] | Promise<void | ModuleNode[]>;
  • Runs: dev only.

Customise how a file change becomes HMR updates: narrow the affected modules, push custom events, or fully handle the change (return []). This is the hook a framework plugin (React Fast Refresh) uses.

ts
handleHotUpdate(ctx) {
  if (ctx.file.endsWith(".data.json")) {
    ctx.server.send("data:changed", { file: ctx.file });
    return []; // handled — no default update/reload
  }
}

The HotUpdateContext gives you file, the affected modules, and a server handle for custom events. See the HMR API for the module-graph types.

Shared result types

TransformResult

ts
interface TransformResult {
  code: string;
  map?: string | object | null; // source map (object or JSON string)
}

Returned by load and transform. Returning a bare string is shorthand for { code, map: null }.

The full interface

ts
interface Plugin {
  name: string;
  enforce?: "pre" | "post";

  config?(config: Config, env: ConfigEnv): Config | null | void | Promise<Config | null | void>;
  configResolved?(config: ResolvedConfig): void | Promise<void>;
  configureServer?(server: DevServerHandle): void | Promise<void>;
  transformIndexHtml?(html: string): string | null | void | Promise<string | null | void>;

  buildStart?(this: PluginContext): void | Promise<void>;
  resolveId?(this: PluginContext, source: string, importer: string | undefined): string | ResolveIdResult | null | void | Promise<…>;
  load?(this: PluginContext, id: string): string | TransformResult | null | void | Promise<…>;
  transform?(this: PluginContext, code: string, id: string): string | TransformResult | null | void | Promise<…>;
  buildEnd?(this: PluginContext, error?: Error): void | Promise<void>;

  handleHotUpdate?(ctx: HotUpdateContext): void | ModuleNode[] | Promise<void | ModuleNode[]>;
}

Released under the MIT License.