Creating Plugins
Learn how to create custom Frame-Master plugins and contribute to the ecosystem.
🎯 Introduction
Plugins are the heart of Frame-Master. They allow you to extend functionality, add features, and customize behavior at every level of your application.
What can plugins do?
- Handle HTTP requests and modify responses
- Add custom routes and WebSocket endpoints
- Transform HTML before sending to clients
- Integrate with databases and external services
- Customize the build process
- Add development tools and hot reload features
- Manage authentication and sessions
🚀 Quick Start
Generate a plugin boilerplate using the Frame-Master CLI.
# Generate a new pluginframe-master plugin create my-custom-plugin# This creates:# my-custom-plugin/# ├── index.ts # Plugin entry point# ├── package.json # Plugin metadata# ├── README.md # Documentation# └── tsconfig.json # TypeScript config
Plugin Naming Convention
frame-master-plugin-[name] for consistency with the ecosystem. For example: frame-master-plugin-auth, frame-master-plugin-database.📦 Basic Plugin Structure
Every Frame-Master plugin exports a factory function that returns a FrameMasterPlugin object.
import type { FrameMasterPlugin } from "frame-master/plugin/types";export function myPlugin(options = {}): FrameMasterPlugin {return {// Required fieldsname: "my-custom-plugin",version: "1.0.0",// Optional: Server lifecycle hooksserverStart: {main: async () => {console.log("Plugin initialized!");},},// Optional: Request handlingrouter: {before_request: async (master) => {// Initialize context},request: async (master) => {// Handle requests},after_request: async (master) => {// Modify responses},},};}// Export as default for convenienceexport default myPlugin;
Required Fields
namestringUnique identifier for your plugin. Should be descriptive and follow naming conventions.
versionstringSemantic version of your plugin (e.g., '1.0.0'). Used for dependency resolution.
⚙️ Plugin Options Pattern
Allow users to configure your plugin through options.
import type { FrameMasterPlugin } from "frame-master/plugin/types";// Define your options typeexport type MyPluginOptions = {apiKey?: string;endpoint?: string;debug?: boolean;};export function myPlugin(options: MyPluginOptions = {}): FrameMasterPlugin {// Set defaultsconst config = {apiKey: options.apiKey || process.env.API_KEY,endpoint: options.endpoint || "https://api.example.com",debug: options.debug ?? false,};return {name: "my-custom-plugin",version: "1.0.0",serverStart: {main: async () => {if (config.debug) {console.log("Plugin config:", config);}},},router: {request: async (master) => {// Use config in request handlingconst data = await fetch(`${config.endpoint}/data`, {headers: { "Authorization": `Bearer ${config.apiKey}` },});},},};}
TypeScript Support
🔀 Request Handling
Intercept and process HTTP requests in your plugin.
Before Request
Use before_request to initialize context or set global values accessible client-side.
router: {before_request: async (master) => {// Set context data (server-side only)master.setContext({userId: await getUserFromSession(master),timestamp: Date.now(),});// Set global values (accessible client-side)master.setGlobalValues({__API_URL__: process.env.API_URL,__VERSION__: "1.0.0",});},}
Request Interceptor
Use request to handle specific routes or modify requests.
router: {request: async (master) => {const url = new URL(master.request.url);// Handle custom routeif (url.pathname === "/api/data") {const data = await fetchData();master.setResponse(JSON.stringify(data), {headers: {"content-type": "application/json",},}).sendNow(); // Skip other plugins}// Or just set response without sendNow// to allow other plugins to processif (url.pathname === "/hello") {master.setResponse("Hello World", {headers: { "content-type": "text/plain" },});}},}
After Request
Use after_request to modify responses or set cookies.
router: {after_request: async (master) => {// Add custom headersmaster.response?.headers.set("X-Custom-Header", "value");// Set cookies from contextconst context = master.getContext<{ sessionData: any }>();if (context.sessionData) {master.setCookie("session", context.sessionData, {httpOnly: true,encrypted: true,maxAge: 86400, // 1 day});}// Log requestconsole.log(`[${master.request.method}] ${master.request.url}`);},}
🎨 HTML Rewriting
Transform HTML content before sending to clients using HTMLRewriter.
router: {html_rewrite: {// Initialize context for HTML rewritinginitContext: (req) => {return {userAgent: req.request.headers.get("user-agent") || "",isBot: /bot|crawler|spider/i.test(req.request.headers.get("user-agent") || ""),};},// Rewrite HTML elementsrewrite: async (reWriter, master, context) => {// Add meta tagsreWriter.on("head", {element(element) {element.append('<meta name="viewport" content="width=device-width">',{ html: true });},});// Modify specific elementsreWriter.on("div[data-inject]", {element(element) {const value = element.getAttribute("data-inject");element.setInnerContent(`Injected: ${value}`);},});// Add classes based on contextif (context.isBot) {reWriter.on("body", {element(element) {element.setAttribute("class", "bot-visitor");},});}},// Process final HTMLafter: async (HTML, master, context) => {console.log(`HTML processed: ${HTML.length} bytes`);},},}
HTML Rewrite Limitations
string or ReadableStream. Binary responses are not processed.🔨 Build Lifecycle Hooks
Customize the build process for your plugin (v1.1.0+).
build: {// Customize build configurationbuildConfig: (builder) => ({external: ["react", "react-dom"],minify: process.env.NODE_ENV === "production",sourcemap: "external",target: "browser",define: {"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),},}),// Before build startsbeforeBuild: async (buildConfig, builder) => {console.log("🔨 Starting build...");// Clean output directoryawait Bun.$`rm -rf dist/*`;// Generate build manifestawait Bun.write("dist/manifest.json", JSON.stringify({timestamp: new Date().toISOString(),version: "1.0.0",}));},// After build completesafterBuild: async (buildConfig, result, builder) => {if (result.success) {console.log(`✅ Build completed: ${result.outputs.length} files`);// Log generated filesfor (const output of result.outputs) {console.log(` 📦 ${output.path}`);}// Copy static assetsawait Bun.$`cp -r static/* dist/`;} else {console.error("❌ Build failed");}},// Enable detailed loggingenableLoging: process.env.DEBUG === "true",}
🔌 WebSocket Support
Add real-time communication to your plugin.
serverConfig: {routes: {// Define WebSocket upgrade route"/ws/my-plugin": (req, server) => {return server.upgrade(req, {data: { "my-plugin-ws": true, userId: null },});},},},websocket: {onOpen: async (ws) => {// Check if this WebSocket belongs to our pluginif (!ws.data["my-plugin-ws"]) return;console.log("WebSocket connected");ws.send(JSON.stringify({ type: "connected" }));},onMessage: async (ws, message) => {// Only handle our plugin's WebSocket connectionsif (!ws.data["my-plugin-ws"]) return;const data = JSON.parse(message.toString());// Handle different message typesswitch (data.type) {case "ping":ws.send(JSON.stringify({ type: "pong" }));break;case "data":// Process dataconst result = await processData(data.payload);ws.send(JSON.stringify({ type: "result", data: result }));break;}},onClose: async (ws) => {if (!ws.data["my-plugin-ws"]) return;console.log("WebSocket disconnected");// Cleanup resources},}
Shared WebSocket Handlers
ws.data to identify your plugin's connections.🎯 Plugin Priority
Control the execution order of your plugin relative to others.
export function myPlugin(): FrameMasterPlugin {return {name: "my-plugin",version: "1.0.0",// Lower number = higher priority// Executes before plugins with higher numberspriority: 0, // Default is undefined and run after plugins with defined priorityrouter: {request: async (master) => {// This runs early due to priority 0},},};}
Priority Guidelines
- Authentication plugins: priority -10 to -1 (run first)
- Response modification: priority 1-10 (run last)
- Logging/analytics: priority 10+ (run last)
- Request processing: priority undefined (defualt) (run after plugins with defined priority)
📋 Plugin Requirements
Specify dependencies and version requirements for your plugin.
requirement: {// Other Frame-Master plugins this plugin depends onframeMasterPlugins: {"frame-master-plugin-session": "^1.0.0","frame-master-plugin-react-ssr": ">=1.0.0 <2.0.0",},// Minimum Frame-Master versionframeMasterVersion: "^1.1.0",// Bun runtime versionbunVersion: ">=1.2.0",}
Version Checking
👀 File System Watching
React to file changes in development mode.
// Directories to watchfileSystemWatchDir: ["src/components","src/styles","config",],// Handle file changes (dev mode only)onFileSystemChange: async (eventType, filePath, absolutePath) => {console.log(`File ${eventType}: ${filePath}`);if (eventType === "change") {// Handle file modificationif (filePath.endsWith(".css")) {await recompileStyles();}} else if (eventType === "rename") {// Handle file rename/deleteawait rebuildRoutes();}}
📝 Custom Directives
Define custom file directives for special behavior.
directives: [{name: "use-client",regex: /^(?:\s*(?:\/\/.*?\n|\s)*)?['"]use[-\s]client['"];?\s*(?:\/\/.*)?(?:\r?\n|$)/m,},{name: "use-server",regex: /^(?:\s*(?:\/\/.*?\n|\s)*)?['"]use[-\s]server['"];?\s*(?:\/\/.*)?(?:\r?\n|$)/m,},]
Users can then use these directives in their files:
"use client";export function ClientComponent() {return <div>Rendered on client</div>;}
📦 Publishing Your Plugin
Share your plugin with the Frame-Master community.
Package.json Setup
{"name": "frame-master-plugin-my-plugin","version": "1.0.0","description": "Description of what your plugin does","main": "index.ts","type": "module","keywords": ["frame-master","frame-master-plugin","your-keywords"],"author": "Your Name","license": "MIT","peerDependencies": {"frame-master": "^1.0.0"},"repository": {"type": "git","url": "https://github.com/yourusername/frame-master-plugin-my-plugin"}}
Publishing Steps
# 1. Ensure your code is readybun testbun build# 2. Update versionnpm version patch # or minor, or major# 3. Publish to npmnpm publish# 4. Tag release on GitHubgit tag v1.0.0git push --tags
Plugin Checklist
- ✅ Clear documentation in README.md
- ✅ Example configuration
- ✅ TypeScript type definitions
- ✅ Proper versioning (semver)
- ✅ Tests for critical functionality
- ✅ License file (MIT recommended)
- ✅ GitHub repository with issues enabled
✨ Best Practices
Tips for creating high-quality Frame-Master plugins.
1. Clear Configuration
Provide sensible defaults and clear options.
// ✅ Good: Clear defaults, typed optionstype Options = {apiUrl?: string;timeout?: number;retries?: number;};export function myPlugin(options: Options = {}) {const config = {apiUrl: options.apiUrl ?? "https://api.example.com",timeout: options.timeout ?? 5000,retries: options.retries ?? 3,};// ...}
2. Error Handling
router: {request: async (master) => {try {const data = await fetchData();master.setResponse(JSON.stringify(data));} catch (error) {console.error("Plugin error:", error);master.setResponse(JSON.stringify({ error: "Internal server error" }),{status: 500,headers: { "content-type": "application/json" },});}},}
3. Performance
- Cache expensive operations (database queries, API calls)
- Use async/await for I/O operations
- Avoid blocking the event loop
- Clean up resources in appropriate hooks
- Use priority to optimize plugin order
4. Documentation
- Document all configuration options
- Provide usage examples
- Explain plugin behavior and limitations
- Include migration guides for breaking changes
- Add JSDoc comments to exported functions
