Documentation

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.

terminal
# Generate a new plugin
frame-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

Use the format 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.

my-plugin.ts
import type { FrameMasterPlugin } from "frame-master/plugin/types";
export function myPlugin(options = {}): FrameMasterPlugin {
return {
// Required fields
name: "my-custom-plugin",
version: "1.0.0",
// Optional: Server lifecycle hooks
serverStart: {
main: async () => {
console.log("Plugin initialized!");
},
},
// Optional: Request handling
router: {
before_request: async (master) => {
// Initialize context
},
request: async (master) => {
// Handle requests
},
after_request: async (master) => {
// Modify responses
},
},
};
}
// Export as default for convenience
export default myPlugin;

Required Fields

namestring

Unique identifier for your plugin. Should be descriptive and follow naming conventions.

versionstring

Semantic version of your plugin (e.g., '1.0.0'). Used for dependency resolution.

⚙️ Plugin Options Pattern

Allow users to configure your plugin through options.

configurable-plugin.ts
import type { FrameMasterPlugin } from "frame-master/plugin/types";
// Define your options type
export type MyPluginOptions = {
apiKey?: string;
endpoint?: string;
debug?: boolean;
};
export function myPlugin(options: MyPluginOptions = {}): FrameMasterPlugin {
// Set defaults
const 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 handling
const data = await fetch(`${config.endpoint}/data`, {
headers: { "Authorization": `Bearer ${config.apiKey}` },
});
},
},
};
}
💡

TypeScript Support

Always export your options type for better developer experience. Users can import and use it for type-safe configuration.

🔀 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.

before-request.ts
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.

request-handler.ts
router: {
request: async (master) => {
const url = new URL(master.request.url);
// Handle custom route
if (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 process
if (url.pathname === "/hello") {
master.setResponse("Hello World", {
headers: { "content-type": "text/plain" },
});
}
},
}

After Request

Use after_request to modify responses or set cookies.

after-request.ts
router: {
after_request: async (master) => {
// Add custom headers
master.response?.headers.set("X-Custom-Header", "value");
// Set cookies from context
const context = master.getContext<{ sessionData: any }>();
if (context.sessionData) {
master.setCookie("session", context.sessionData, {
httpOnly: true,
encrypted: true,
maxAge: 86400, // 1 day
});
}
// Log request
console.log(`[${master.request.method}] ${master.request.url}`);
},
}

🎨 HTML Rewriting

Transform HTML content before sending to clients using HTMLRewriter.

html-rewrite.ts
router: {
html_rewrite: {
// Initialize context for HTML rewriting
initContext: (req) => {
return {
userAgent: req.request.headers.get("user-agent") || "",
isBot: /bot|crawler|spider/i.test(
req.request.headers.get("user-agent") || ""
),
};
},
// Rewrite HTML elements
rewrite: async (reWriter, master, context) => {
// Add meta tags
reWriter.on("head", {
element(element) {
element.append(
'<meta name="viewport" content="width=device-width">',
{ html: true }
);
},
});
// Modify specific elements
reWriter.on("div[data-inject]", {
element(element) {
const value = element.getAttribute("data-inject");
element.setInnerContent(`Injected: ${value}`);
},
});
// Add classes based on context
if (context.isBot) {
reWriter.on("body", {
element(element) {
element.setAttribute("class", "bot-visitor");
},
});
}
},
// Process final HTML
after: async (HTML, master, context) => {
console.log(`HTML processed: ${HTML.length} bytes`);
},
},
}
⚠️

HTML Rewrite Limitations

HTML rewriting only works when the response body is a string or ReadableStream. Binary responses are not processed.

🔨 Build Lifecycle Hooks

Customize the build process for your plugin (v1.1.0+).

build-hooks.ts
build: {
// Customize build configuration
buildConfig: (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 starts
beforeBuild: async (buildConfig, builder) => {
console.log("🔨 Starting build...");
// Clean output directory
await Bun.$`rm -rf dist/*`;
// Generate build manifest
await Bun.write("dist/manifest.json", JSON.stringify({
timestamp: new Date().toISOString(),
version: "1.0.0",
}));
},
// After build completes
afterBuild: async (buildConfig, result, builder) => {
if (result.success) {
console.log(`✅ Build completed: ${result.outputs.length} files`);
// Log generated files
for (const output of result.outputs) {
console.log(` 📦 ${output.path}`);
}
// Copy static assets
await Bun.$`cp -r static/* dist/`;
} else {
console.error("❌ Build failed");
}
},
// Enable detailed logging
enableLoging: process.env.DEBUG === "true",
}

🔌 WebSocket Support

Add real-time communication to your plugin.

websocket.ts
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 plugin
if (!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 connections
if (!ws.data["my-plugin-ws"]) return;
const data = JSON.parse(message.toString());
// Handle different message types
switch (data.type) {
case "ping":
ws.send(JSON.stringify({ type: "pong" }));
break;
case "data":
// Process data
const 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

WebSocket handlers receive connections from all plugins. Use ws.data to identify your plugin's connections.

🎯 Plugin Priority

Control the execution order of your plugin relative to others.

priority.ts
export function myPlugin(): FrameMasterPlugin {
return {
name: "my-plugin",
version: "1.0.0",
// Lower number = higher priority
// Executes before plugins with higher numbers
priority: 0, // Default is undefined and run after plugins with defined priority
router: {
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.

requirements.ts
requirement: {
// Other Frame-Master plugins this plugin depends on
frameMasterPlugins: {
"frame-master-plugin-session": "^1.0.0",
"frame-master-plugin-react-ssr": ">=1.0.0 <2.0.0",
},
// Minimum Frame-Master version
frameMasterVersion: "^1.1.0",
// Bun runtime version
bunVersion: ">=1.2.0",
}
⚠️

Version Checking

Frame-Master validates requirements at startup and will throw an error if dependencies are not met. This prevents runtime issues from incompatible versions.

👀 File System Watching

React to file changes in development mode.

file-watching.ts
// Directories to watch
fileSystemWatchDir: [
"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 modification
if (filePath.endsWith(".css")) {
await recompileStyles();
}
} else if (eventType === "rename") {
// Handle file rename/delete
await rebuildRoutes();
}
}

📝 Custom Directives

Define custom file directives for special behavior.

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

my-component.tsx
"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

package.json
{
"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

terminal
# 1. Ensure your code is ready
bun test
bun build
# 2. Update version
npm version patch # or minor, or major
# 3. Publish to npm
npm publish
# 4. Tag release on GitHub
git tag v1.0.0
git 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-config.ts
// ✅ Good: Clear defaults, typed options
type 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

error-handling.ts
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