Plugin Hooks Reference
Complete reference guide for all available hooks in Frame-Master plugins.
📋 Quick Reference
- Server Hooks:
serverStart.main,serverStart.dev_main - 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");
},
}🔀 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 (once)
serverStart.main() → serverStart.dev_main()
// 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
