Frontend Plugin Development Guide
Build modular frontend extensions for Framework M applications.
Quick Start
# 1. Create a new app with frontend plugin
m new:app wms --with-frontend
# 2. Install dependencies
pnpm install
# 3. Start development
pnpm dev # Plugin auto-discovered and loaded
This creates:
wms/
├── pyproject.toml # Python backend
├── src/wms/doctypes/ # DocType definitions
├── frontend/ # Frontend plugin
│ ├── package.json # framework-m metadata
│ ├── tsconfig.json
│ └── src/
│ ├── plugin.config.ts # Plugin manifest
│ ├── pages/ # Route components
│ ├── components/ # UI components
│ └── services/ # Business logic
Plugin Manifest
Every plugin exports a FrameworkMPlugin object from src/plugin.config.ts:
import type { FrameworkMPlugin } from "@framework-m/plugin-sdk";
const plugin: FrameworkMPlugin = {
name: "wms",
version: "0.1.0",
// Menu items appear in the sidebar
menu: [
{
name: "wms.dashboard",
label: "WMS Dashboard",
route: "/wms/dashboard",
icon: "warehouse",
module: "WMS", // Groups items under "WMS" heading
category: "Overview", // Optional sub-group
order: 1,
},
{
name: "wms.inventory",
label: "Inventory",
route: "/wms/inventory",
icon: "package",
module: "WMS",
category: "Operations",
order: 2,
},
],
// Routes are lazy-loaded for code-splitting
routes: [
{
path: "/wms/dashboard",
element: () => import("./pages/Dashboard"),
},
{
path: "/wms/inventory",
element: () => import("./pages/Inventory"),
},
],
// Services are registered in the DI container
services: {
inventoryService: () => import("./services/InventoryService"),
},
};
export default plugin;
Auto-Discovery
Plugins are discovered automatically at build time. The @framework-m/vite-plugin scans all workspace packages for "framework-m" metadata in package.json:
{
"name": "@wms/frontend",
"framework-m": {
"plugin": "./src/plugin.config.ts",
"type": "frontend-module"
}
}
No manual imports or registration required. Add a new app with --with-frontend, and it's automatically composed into the shell.
Menu System
Menu items are merged from all plugins and organized into a tree:
Module (e.g., "WMS")
├── Category (e.g., "Operations")
│ ├── Item: Inventory
│ └── Item: Receiving
└── Category (e.g., "Reports")
└── Item: Stock Summary
Menu Item Properties
| Property | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Unique identifier (e.g., "wms.inventory") |
label | string | ✅ | Display text |
route | string | Route path | |
icon | string | Icon name | |
module | string | Top-level grouping | |
category | string | Sub-grouping within module | |
order | number | Sort order (lower = higher) |
Routes
Routes use lazy loading via dynamic import() for automatic code-splitting:
routes: [
{
path: "/wms/dashboard",
element: () => import("./pages/Dashboard"),
},
]
Each page component should be a default export:
// src/pages/Dashboard.tsx
export default function Dashboard() {
return <h1>WMS Dashboard</h1>;
}
Services (Dependency Injection)
Register services for cross-plugin communication:
// Plugin manifest
services: {
inventoryService: () => import("./services/InventoryService"),
}
// In any component
import { useService } from "@framework-m/plugin-sdk";
function StockLevel() {
const { service, loading } = useService("inventoryService");
if (loading) return <div>Loading...</div>;
// Use service...
}
Using Shared Components
Import reusable components from @framework-m/desk:
import { AutoForm, AutoTable, useDocTypeMeta } from "@framework-m/desk";
function InventoryForm({ itemId }: { itemId: string }) {
const { schema } = useDocTypeMeta("InventoryItem");
return (
<AutoForm
schema={schema}
formData={itemData}
onSubmit={handleSave}
/>
);
}
React Hooks
usePluginMenu()
Access the merged menu tree from all registered plugins:
import { usePluginMenu } from "@framework-m/plugin-sdk";
function CustomSidebar() {
const menu = usePluginMenu();
// menu is a tree: Module[] → Category[] → MenuItem[]
}
usePlugin(name)
Access a specific registered plugin:
import { usePlugin } from "@framework-m/plugin-sdk";
function PluginInfo() {
const wms = usePlugin("wms");
console.log(wms?.version); // "0.1.0"
}
useService(name)
Access a service from the DI container:
import { useService } from "@framework-m/plugin-sdk";
function MyComponent() {
const { service, loading, error } = useService("inventoryService");
}
Multi-Plugin Composition
Multiple plugins compose seamlessly:
m new:app wms --with-frontend
m new:app personnel --with-frontend
pnpm dev # Both auto-discovered, menus merged, routes registered
The sidebar will show:
DOCTYPES (from Refine)
├── Core
│ └── User, Role, ...
──────────────────────
WMS (from wms plugin)
├── Overview
│ └── WMS Dashboard
├── Operations
│ └── Inventory
PERSONNEL (from personnel plugin)
├── HR
│ └── Employee Directory
File Structure Best Practices
frontend/src/
├── plugin.config.ts # Plugin manifest (required)
├── pages/ # One file per route
│ ├── Dashboard.tsx
│ └── Inventory.tsx
├── components/ # Plugin-specific UI
│ └── StockCard.tsx
└── services/ # Business logic
└── InventoryService.ts
Related Documentation
- Plugin Architecture Implementation — Technical deep-dive
- Plugin Package Architecture — Package structure details
- Plugin API Reference — Complete API docs
- Building a Multi-Module App — Step-by-step tutorial