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.
import type { Plugin } from "vantris";definePlugin
A typed identity helper that gives you autocompletion and checking while authoring. It returns its argument unchanged.
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:
export function myPlugin(options: MyOptions = {}): Plugin {
return definePlugin({ name: "my-plugin", /* … uses options … */ });
}name
- Type:
string— required
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
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.
config(config, env) {
if (env.command === "build") {
return { define: { __PROD__: true } };
}
}ConfigEnv:
interface ConfigEnv {
mode: string; // e.g. "development", "production"
command: "serve" | "build"; // the command in flight
}configResolved
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.
let root = "";
export default definePlugin({
name: "x",
configResolved(config) {
root = config.paths.root;
},
});configureServer
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.
configureServer(server) {
server.hmr.send("my:ready", { at: Date.now() });
}transformIndexHtml
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.
transformIndexHtml(html) {
return html.replace("</head>", `<meta name="built" content="${Date.now()}"></head>`);
}Module pipeline hooks
These run with a this PluginContext.
buildStart
buildStart?(this: PluginContext): void | Promise<void>;- Runs: build, once before the module pipeline starts.
Initialise state, emit initial assets, or add watch files.
resolveId
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.
resolveId(source) {
if (source === "virtual:config") return "\0virtual:config";
return null;
}ResolveIdResult:
interface ResolveIdResult {
id: string; // resolved id (absolute path or virtual id)
external?: boolean; // leave the import in place instead of bundling/serving
}load
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.
load(id) {
if (id === "\0virtual:config") {
return `export default ${JSON.stringify({ version: "1.0.0" })};`;
}
return null;
}transform
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.
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
buildEnd?(this: PluginContext, error?: Error): void | Promise<void>;- Runs: build, once when the pipeline finishes (or errors —
erroris set).
Flush state, emit final assets, or clean up.
HMR hook
handleHotUpdate
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.
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
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
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[]>;
}