Documentation

Plugin Hooks Reference

Complete reference guide for all available hooks in Frame-Master plugins.

📋 Quick Reference

Overview of all available hooks organized by category.

Server Hooks

2 hooks

Initialization and startup

serverStart.main
serverStart.dev_main

Router Hooks

4 hooks

Request processing

router.before_request
router.request
router.after_request
router.html_rewrite

Build Hooks

3 hooks

Build process control

build.buildConfig
build.beforeBuild
build.afterBuild

File System Hooks

2 hooks

File change watching

fileSystemWatchDir
onFileSystemChange

Configuration

4 props

Plugin metadata

name, version
priority, requirement

WebSocket

3 hooks

Real-time communication

websocket.onOpen
websocket.onMessage
websocket.onClose

Advanced

4 props

Custom functionality

serverConfig, cli
directives, runtimePlugins

🚀 Server Lifecycle Hooks

Hooks that execute during server initialization and startup.

serverStart.main

serverStart.main() => Promise<void>

Executes on the main thread when the server starts. Runs in both development and production modes.

server-start-main.ts
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

serverStart.dev_main() => Promise<void>

Executes only in development mode. Use for dev-specific initialization like file watchers or debug tools.

server-start-dev.ts
serverStart: {
dev_main: async () => {
// Enable debug logging
enableDebugMode();
// Start file watcher
watchForChanges();
// Initialize hot reload
setupHotReload();
console.log("Dev mode initialized");
},
}

🔀 Router Hooks

Hooks for intercepting and modifying HTTP requests and responses.

router.before_request

router.before_request(master: RequestManager) => Promise<void>

Called before request processing begins. Use to initialize context or set global values.

Available Methods:

  • master.setContext(data) - Set request-specific context data
  • master.setGlobalValues(values) - Inject global values accessible in client code
  • master.request - Access the incoming Request object
before-request.ts
router: {
before_request: async (master) => {
// Initialize context
master.setContext({
requestId: crypto.randomUUID(),
startTime: Date.now(),
user: null,
});
// Inject global values (accessible as globalThis.__API_URL__)
master.setGlobalValues({
__API_URL__: process.env.API_URL,
__VERSION__: "1.0.0",
});
},
}

router.request

router.request(master: RequestManager) => Promise<void>

Called during request processing. Can intercept and handle requests or let them pass through.

Available Methods:

  • master.request - The incoming Request object
  • master.setResponse(body, options) - Set the response body and options
  • master.sendNow() - Send response immediately, skipping other plugins
  • master.getContext() - Get request context data
request.ts
router: {
request: async (master) => {
const url = new URL(master.request.url);
// Handle API routes
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;
}
// Let other plugins handle it
},
}
⚠️

sendNow() Behavior

Calling sendNow() immediately sends the response and prevents subsequent request hooks from executing. Only use this when you want to bypass the normal request flow.

router.after_request

router.after_request(master: RequestManager) => Promise<void>

Called after request processing. Use to modify response headers or perform cleanup.

Available Methods:

  • master.response - The Response object (may be null)
  • master.request - The original Request object
  • master.getContext() - Get request context data
after-request.ts
router: {
after_request: async (master) => {
const response = master.response;
if (!response) return;
// Add security headers
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("X-XSS-Protection", "1; mode=block");
// Add timing information
const context = master.getContext();
const duration = Date.now() - context.startTime;
response.headers.set("X-Response-Time", `${duration}ms`);
// Log request
console.log(`[${master.request.method}] ${master.request.url} - ${duration}ms`);
},
}

router.html_rewrite

router.html_rewrite{ initContext, rewrite, after }

Transform HTML responses using Bun's HTMLRewriter API.

html-rewrite.ts
router: {
html_rewrite: {
// Initialize context for HTML rewriting
initContext: (req) => {
return {
injectAnalytics: process.env.NODE_ENV === "production",
theme: req.headers.get("x-theme") || "dark",
userId: req.headers.get("x-user-id"),
};
},
// Rewrite HTML elements
rewrite: async (reWriter, master, context) => {
// Inject scripts in head
reWriter.on("head", {
element(element) {
if (context.injectAnalytics) {
element.append(
'<script src="/analytics.js"></script>',
{ html: true }
);
}
},
});
// Modify body attributes
reWriter.on("body", {
element(element) {
element.setAttribute("data-theme", context.theme);
if (context.userId) {
element.setAttribute("data-user", context.userId);
}
},
});
// Transform specific elements
reWriter.on("img", {
element(element) {
const src = element.getAttribute("src");
if (src && !src.startsWith("http")) {
element.setAttribute("loading", "lazy");
}
},
});
},
// Final processing after HTML rewrite
after: async (HTML, master, context) => {
// Additional HTML transformations
console.log("HTML processing complete");
// You can modify HTML string here if needed
// return modifiedHTML;
},
},
}
💡

HTMLRewriter API

The reWriter parameter is Bun's HTMLRewriter instance. You can use all standard HTMLRewriter methods to transform HTML.

🔨 Build Hooks

Hooks for customizing and participating in the build process.

build.buildConfig

build.buildConfig(builder: Builder) => Promise<Partial<BuildConfig>>

Return build configuration to merge with the global config. All plugins' configs are merged.

build-config.ts
build: {
buildConfig: async (builder) => {
return {
// External modules (won't be bundled)
external: ["react", "react-dom"],
// Minification settings
minify: process.env.NODE_ENV === "production",
// Source maps
sourcemap: "external",
// Code splitting
splitting: true,
// Define replacements
define: {
"__VERSION__": JSON.stringify("1.0.0"),
"__BUILD_TIME__": JSON.stringify(new Date().toISOString()),
},
// Bun plugins for the build
plugins: [myBunPlugin],
};
},
}
💡

Config Merging

All plugins' build configs are merged together. Array fields like external and plugins are concatenated. Object fields like define are merged (later wins).

build.beforeBuild

build.beforeBuild(config: BuildConfig, builder: Builder) => Promise<void>

Called before the build starts. Use for pre-build tasks like cleaning or generating files.

before-build.ts
build: {
beforeBuild: async (config, builder) => {
console.log("🔨 Preparing build...");
// Generate type definitions
await generateTypes();
// Generate route manifest
await generateRouteManifest();
// Clean temp files
await cleanTempDirectory();
// Pre-compile assets
await compileAssets();
},
}
💡

Parallel Execution

All plugins' beforeBuild hooks run in parallel for better performance.

build.afterBuild

build.afterBuild(config: BuildConfig, result: BuildOutput, builder: Builder) => Promise<void>

Called after successful build. Use for post-processing, generating manifests, or custom file operations.

after-build.ts
build: {
afterBuild: async (config, result, builder) => {
if (!result.success) return;
console.log(`✅ Built ${result.outputs.length} files`);
// Generate manifest
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));
// IMPORTANT: Register custom files to prevent cleanup deletion
result.outputs.push({
path: manifestPath,
kind: "asset",
hash: "",
loader: "file",
} as Bun.BuildArtifact);
// Copy static assets
await copyStaticAssets(config.outdir);
},
// Enable build logging
enableLoging: true,
}

Output Cleanup Warning

Frame-Master automatically cleans up the output directory after each build. Any files not in result.outputs will be deleted. Always push custom files to the outputs array!

👁️ File System Hooks

Hooks for reacting to file system changes in development mode.

fileSystemWatchDir

fileSystemWatchDirstring[]

Array of directory paths to watch for changes. Only active in development mode.

watch-dirs.ts
export function myPlugin(): FrameMasterPlugin {
return {
name: "my-plugin",
// Specify directories to watch
fileSystemWatchDir: [
"src/",
"public/styles/",
"config/",
],
// ...
};
}

onFileSystemChange

onFileSystemChange(eventType: string, filePath: string, absolutePath: string) => Promise<void>

Called when a file in watched directories changes. Only active in development mode.

Parameters:

  • eventType - Type of change ("change", "rename", etc.)
  • filePath - Relative path to the changed file
  • absolutePath - Absolute path to the changed file
on-change.ts
onFileSystemChange: async (eventType, filePath, absolutePath) => {
console.log(`File ${eventType}: ${filePath}`);
// Rebuild CSS on style changes
if (filePath.endsWith(".css")) {
await rebuildStyles();
console.log("Styles rebuilt");
}
// Clear cache on component changes
if (filePath.includes("/components/")) {
clearComponentCache();
console.log("Component cache cleared");
}
// Reload config on config changes
if (filePath.includes("config/")) {
await reloadConfig();
console.log("Config reloaded");
}
}

⚙️ Plugin Configuration

Configure plugin metadata, priority, and requirements.

name & version

namestringrequired

Unique identifier for the plugin. Used in logging and error messages.

versionstringrequired

Semantic version of the plugin (e.g., '1.0.0'). Required for dependency checking.

name-version.ts
export function myPlugin(): FrameMasterPlugin {
return {
name: "my-custom-plugin",
version: "1.0.0",
// ...
};
}

priority

prioritynumber

Execution priority. Lower numbers run first. Default is 50.

Default: 50

priority.ts
// Auth plugin - runs first
export function authPlugin(): FrameMasterPlugin {
return {
name: "auth-plugin",
priority: 0,
// ...
};
}
// Logging plugin - runs last
export function loggingPlugin(): FrameMasterPlugin {
return {
name: "logging-plugin",
priority: 100,
// ...
};
}

requirement

requirement{ frameMasterVersion?, bunVersion?, frameMasterPlugins? }

Specify version requirements for Frame-Master, Bun, and other plugins.

requirements.ts
requirement: {
// Require Frame-Master version
frameMasterVersion: "^1.0.0",
// Require Bun runtime version
bunVersion: ">=1.2.0",
// Require other plugins
frameMasterPlugins: {
"frame-master-plugin-react-ssr": "^1.0.0",
"my-database-plugin": "^2.0.0",
},
}

📡 WebSocket Hooks

Hooks for real-time WebSocket communication.

serverConfig.routes

serverConfig.routesRecord<string, (req, server) => Response | undefined>

Define WebSocket upgrade routes. Returns undefined to upgrade the connection.

ws-route.ts
serverConfig: {
routes: {
"/ws/my-plugin": (req, server) => {
// Upgrade the connection with custom data
return server.upgrade(req, {
data: { "my-plugin-ws": true, userId: req.headers.get("x-user-id") },
});
},
},
},

websocket.onOpen

websocket.onOpen(ws: Bun.ServerWebSocket) => Promise<void> | void

Called when a WebSocket connection is established.

ws-open.ts
websocket: {
onOpen: async (ws) => {
// Check if this connection belongs to your plugin
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(ws: Bun.ServerWebSocket, message: string | ArrayBufferView) => Promise<void> | void

Called when a message is received from the WebSocket client.

ws-message.ts
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(ws: Bun.ServerWebSocket) => Promise<void> | void

Called when the WebSocket connection is closed.

ws-close.ts
websocket: {
onClose: async (ws) => {
if (!ws.data["my-plugin-ws"]) return;
console.log("Client disconnected:", ws.data.userId);
// Cleanup resources, remove from rooms, etc.
},
},
💡

Shared WebSocket Handlers

WebSocket handlers receive connections from all plugins. Use ws.data to identify which plugin the connection belongs to by checking the data set during the upgrade.

🔧 Advanced Features

Advanced plugin capabilities for custom functionality.

serverConfig

serverConfigPartial<Bun.Serve.Options>

Customize Bun server options (excluding fetch, port, tls).

server-config.ts
serverConfig: {
// Custom routes for WebSocket upgrades or special handling
routes: {
"/ws/chat": (req, server) => server.upgrade(req, { data: { room: "chat" } }),
"/health": () => new Response("OK"),
},
// Other Bun.serve options
maxRequestBodySize: 1024 * 1024 * 10, // 10MB
},

cli

cli(command: Command) => Command

Extend Frame-Master CLI with custom commands using Commander.js.

cli.ts
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 to ${options.env}...`);
if (options.dryRun) {
console.log("Dry run - no changes made");
return;
}
// Your deployment logic
await deployToEnvironment(options.env);
});
}
💡

CLI Access

Custom commands are accessible via frame-master extended-cli <your-command>. See Commander.js docs for full API.

directives

directivesArray<{ name: string, regex: RegExp }>

Define custom directives for special file handling.

directives.ts
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

runtimePluginsBunPlugin[]

Bun runtime plugins for custom module resolution and transformation.

runtime-plugins.ts
import type { BunPlugin } from "bun";
const customLoader: BunPlugin = {
name: "custom-loader",
setup(build) {
// Handle .custom files
build.onLoad({ filter: /\.custom$/ }, async (args) => {
const contents = await Bun.file(args.path).text();
return {
contents: transformCustomFile(contents),
loader: "js",
};
});
// Resolve custom imports
build.onResolve({ filter: /^@custom\/.*/ }, (args) => {
return {
path: resolveCustomPath(args.path),
namespace: "custom",
};
});
},
};
export function myPlugin(): FrameMasterPlugin {
return {
name: "my-plugin",
runtimePlugins: [customLoader],
// ...
};
}

🔄 Plugin Lifecycle

Hooks execute in priority order (lower numbers first).

// Startup (once)
serverStart.main() → serverStart.dev_main()
// Each Request
before_request → request → after_request → html_rewrite
// Build Process
buildConfig → beforeBuild → [Bun Build] → afterBuild
💡

Priority Order

Within each hook, plugins execute in priority order. A plugin with priority: 0 runs before one with priority: 50. Plugins without a priority default to 50.

🎯Next Steps