Documentation

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: reWriter is Bun's HTMLRewriter instance—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/plugins concatenate; objects like define merge (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] → afterBuild

Info: Within each hook, plugins execute by priority. priority: 0 runs before priority: 50; unspecified priorities default to 50.

🎯 Next Steps