Plugin Hooks Reference
Complete reference guide for all available hooks in Frame-Master plugins.
📋 Quick Reference
- Server Hooks:
serverStart.main,serverStart.dev_main,serverReady,serverStop,createContext - Router Hooks:
router.before_request,router.request,router.after_request,router.html_rewrite - Build Hooks:
build.buildConfig,build.beforeBuild,build.afterBuild - File System Hooks:
fileSystemWatchDir,onFileSystemChange - Configuration:
name,version,priority,requirement - WebSocket:
websocket.onOpen,websocket.onMessage,websocket.onClose - Advanced:
serverConfig,cli,directives,runtimePlugins
🚀 Server Lifecycle Hooks
serverStart.main
Runs on server start (dev and prod).
export function myPlugin(): FrameMasterPlugin {
return {
name: "my-plugin",
serverStart: {
main: async () => {
// Initialize database connections
await db.connect();
// Load configuration
const config = await loadConfig();
// Set up global state
global.appConfig = config;
console.log("Plugin initialized");
},
},
};
}serverStart.dev_main
Runs only in development.
serverStart: {
dev_main: async () => {
// Enable debug logging
enableDebugMode();
// Start file watcher
watchForChanges();
// Initialize hot reload
setupHotReload();
console.log("Dev mode initialized");
},
}serverReady
Runs after Bun.serve is listening. Receives { builder, pluginLoader, config, server }. Runs again after a config reload creates a new server.
serverStop
Inverse of start/ready. Runs on the old plugin generation before the loader is replaced, on createPluginTestEnv().dispose(), and on SIGINT/SIGTERM after Serve().
Hooks run sequentially in reverse priority (highest number first). Errors are logged and do not block shutdown.
import { deleteGlobalPluginContext, getGlobalPluginContext } from "frame-master/plugin";
{
name: "db",
version: "1.0.0",
createContext: async () => ({ pool: await openPool() }),
serverStop: async ({ reason }) => {
const ctx = getGlobalPluginContext("db");
await ctx?.pool.close();
deleteGlobalPluginContext("db");
},
}reason is "reload" | "dispose" | "signal" | "explicit". server is the instance about to stop, or null when no HTTP server was started.
createContext
Runs after the plugin loader and builder are ready. Return value is stored in getGlobalPluginContext(plugin.name).
🔀 Router Hooks
router.before_request
Called before request processing. Use to initialize context or set global values.
router: {
before_request: async (master) => {
master.setContext({
requestId: crypto.randomUUID(),
startTime: Date.now(),
user: null,
});
master.setGlobalValues({
__API_URL__: process.env.API_URL,
__VERSION__: "1.0.0",
});
},
}router.request
Intercept and handle requests.
router: {
request: async (master) => {
const url = new URL(master.request.url);
if (url.pathname.startsWith("/api/")) {
const data = await handleApiRequest(master.request);
master
.setResponse(JSON.stringify(data), {
status: 200,
header: {
"Content-Type": "application/json",
"X-Custom-Header": "value",
},
})
.sendNow(); // Skip remaining plugins
return;
}
},
}Warning:
sendNow()immediately sends the response and stops subsequent request hooks. Use only when you need to bypass the normal flow.
router.after_request
Runs after processing; great for headers, logging, or cleanup.
router: {
after_request: async (master) => {
const response = master.response;
if (!response) return;
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("X-XSS-Protection", "1; mode=block");
const context = master.getContext();
const duration = Date.now() - context.startTime;
response.headers.set("X-Response-Time", `${duration}ms`);
console.log(`[${master.request.method}] ${master.request.url} - ${duration}ms`);
},
}router.html_rewrite
Transform HTML responses using Bun's HTMLRewriter.
router: {
html_rewrite: {
initContext: (req) => {
return {
injectAnalytics: process.env.NODE_ENV === "production",
theme: req.headers.get("x-theme") || "dark",
userId: req.headers.get("x-user-id"),
};
},
rewrite: async (reWriter, master, context) => {
reWriter.on("head", {
element(element) {
if (context.injectAnalytics) {
element.append('<script src="/analytics.js"></script>', { html: true });
}
},
});
reWriter.on("body", {
element(element) {
element.setAttribute("data-theme", context.theme);
if (context.userId) element.setAttribute("data-user", context.userId);
},
});
reWriter.on("img", {
element(element) {
const src = element.getAttribute("src");
if (src && !src.startsWith("http")) element.setAttribute("loading", "lazy");
},
});
},
after: async (HTML, master, context) => {
console.log("HTML processing complete");
// Optionally return modified HTML
},
},
}Info:
reWriteris Bun'sHTMLRewriterinstance—use standard API methods to transform HTML.
🔨 Build Hooks
build.buildConfig
Merge extra build config.
build: {
buildConfig: async (builder) => {
return {
external: ["react", "react-dom"],
minify: process.env.NODE_ENV === "production",
sourcemap: "external",
splitting: true,
define: {
"__VERSION__": JSON.stringify("1.0.0"),
"__BUILD_TIME__": JSON.stringify(new Date().toISOString()),
},
plugins: [myBunPlugin],
};
},
}Info: Build configs from all plugins are merged. Arrays like
external/pluginsconcatenate; objects likedefinemerge (later wins).
build.beforeBuild
Pre-build tasks run in parallel across plugins.
build: {
beforeBuild: async (config, builder) => {
console.log("🔨 Preparing build...");
await generateTypes();
await generateRouteManifest();
await cleanTempDirectory();
await compileAssets();
},
}build.afterBuild
Post-build processing.
build: {
afterBuild: async (config, result, builder) => {
if (!result.success) return;
console.log(`✅ Built ${result.outputs.length} files`);
const manifestPath = `${config.outdir}/manifest.json`;
const manifest = {
version: "1.0.0",
buildTime: new Date().toISOString(),
files: result.outputs.map((o) => o.path),
};
await Bun.write(manifestPath, JSON.stringify(manifest, null, 2));
// Keep custom files from cleanup
result.outputs.push({
path: manifestPath,
kind: "asset",
hash: "",
loader: "file",
} as Bun.BuildArtifact);
await copyStaticAssets(config.outdir);
},
// Enable build logging
enableLoging: true,
}Danger: Output cleanup deletes files not in
result.outputs. Push any custom files you generate.
👁️ File System Hooks
fileSystemWatchDir
Directories to watch (dev mode only).
export function myPlugin(): FrameMasterPlugin {
return {
name: "my-plugin",
fileSystemWatchDir: ["src/", "public/styles/", "config/"],
};
}onFileSystemChange
Respond to watched file changes (dev mode only).
onFileSystemChange: async (eventType, filePath, absolutePath) => {
console.log(`File ${eventType}: ${filePath}`);
if (filePath.endsWith(".css")) {
await rebuildStyles();
console.log("Styles rebuilt");
}
if (filePath.includes("/components/")) {
clearComponentCache();
console.log("Component cache cleared");
}
if (filePath.includes("config/")) {
await reloadConfig();
console.log("Config reloaded");
}
};⚙️ Plugin Configuration
name & version
export function myPlugin(): FrameMasterPlugin {
return {
name: "my-custom-plugin",
version: "1.0.0",
};
}priority
Lower runs first; default is 50.
export function authPlugin(): FrameMasterPlugin {
return { name: "auth-plugin", priority: 0 };
}
export function loggingPlugin(): FrameMasterPlugin {
return { name: "logging-plugin", priority: 100 };
}requirement
Specify Frame-Master, Bun, and plugin requirements.
requirement: {
frameMasterVersion: "^1.0.0",
bunVersion: ">=1.2.0",
frameMasterPlugins: {
"frame-master-plugin-react-ssr": "^1.0.0",
"my-database-plugin": "^2.0.0",
},
}📡 WebSocket Hooks
serverConfig.routes
Define upgrade routes.
serverConfig: {
routes: {
"/ws/my-plugin": (req, server) => {
return server.upgrade(req, {
data: { "my-plugin-ws": true, userId: req.headers.get("x-user-id") },
});
},
},
},websocket.onOpen
websocket: {
onOpen: async (ws) => {
if (!ws.data["my-plugin-ws"]) return;
console.log("Client connected:", ws.data.userId);
ws.send(JSON.stringify({ type: "connected", timestamp: Date.now() }));
},
},websocket.onMessage
websocket: {
onMessage: async (ws, message) => {
if (!ws.data["my-plugin-ws"]) return;
const data = JSON.parse(message.toString());
switch (data.type) {
case "ping":
ws.send(JSON.stringify({ type: "pong" }));
break;
case "broadcast":
// Handle broadcast logic
break;
}
},
},websocket.onClose
websocket: {
onClose: async (ws) => {
if (!ws.data["my-plugin-ws"]) return;
console.log("Client disconnected:", ws.data.userId);
// Cleanup resources, remove from rooms, etc.
},
},Info: WebSocket handlers receive connections from all plugins. Use
ws.data(set during upgrade) to identify your plugin's connections.
🔧 Advanced Features
serverConfig
Customize Bun server options (except fetch, port, tls).
serverConfig: {
routes: {
"/ws/chat": (req, server) => server.upgrade(req, { data: { room: "chat" } }),
"/health": () => new Response("OK"),
},
maxRequestBodySize: 1024 * 1024 * 10, // 10MB
},cli
Extend the CLI with Commander.js commands.
cli: (command) => {
return command
.command("deploy")
.description("Deploy your application")
.option("-e, --env <environment>", "Target environment", "production")
.option("--dry-run", "Preview without deploying")
.action(async (options) => {
console.log(`Deploying ${options.env}...`);
if (options.dryRun) {
console.log("Dry run - no changes made");
return;
}
await deployToEnvironment(options.env);
});
};directives
Define custom directives for special file handling.
directives: [
{
name: "use-server",
regex:
/^(?:\s*(?:\/\/.*?\n|\s)*)?['"]use[-\s]server['"];?\s*(?:\/\/.*)?(?:\r?\n|$)/m,
},
{
name: "use-client",
regex:
/^(?:\s*(?:\/\/.*?\n|\s)*)?['"]use[-\s]client['"];?\s*(?:\/\/.*)?(?:\r?\n|$)/m,
},
{
name: "use-cache",
regex:
/^(?:\s*(?:\/\/.*?\n|\s)*)?['"]use[-\s]cache['"];?\s*(?:\/\/.*)?(?:\r?\n|$)/m,
},
];runtimePlugins
Provide Bun plugins for custom module resolution/transform.
import type { BunPlugin } from "bun";
const customLoader: BunPlugin = {
name: "custom-loader",
setup(build) {
build.onLoad({ filter: /\.custom$/ }, async (args) => {
const contents = await Bun.file(args.path).text();
return { contents: transformCustomFile(contents), loader: "js" };
});
build.onResolve({ filter: /^@custom\/.*/ }, (args) => {
return { path: resolveCustomPath(args.path), namespace: "custom" };
});
},
};
export function myPlugin(): FrameMasterPlugin {
return {
name: "my-plugin",
runtimePlugins: [customLoader],
};
}🔄 Plugin Lifecycle (execution order)
// Startup
createContext → serverStart.main → serverStart.dev_main → Bun.serve → serverReady
// Config reload
serverStop(old) → stop HTTP → reinit → onConfigReload(new) → Bun.serve → serverReady
// Ctrl+C / SIGTERM after Serve()
serverStop(reason: signal) → stop HTTP (force) → process.exit
// Each Request
before_request → request → after_request → html_rewrite
// Build Process
buildConfig → beforeBuild → [Bun Build] → afterBuildInfo: Within each hook, plugins execute by
priority.priority: 0runs beforepriority: 50; unspecified priorities default to 50.
🎯 Next Steps
- Creating Plugins — Build your own custom plugins
- Installing Plugins — Add plugins to your project
- Plugin Overview — Introduction to the plugin system
