Documentation

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: 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 (once)
serverStart.main() → serverStart.dev_main()
 
// 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