Skip to main content

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 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
PropertyTypeRequiredDescription
namestringUnique identifier (e.g., "wms.inventory")
labelstringDisplay text
routestringRoute path
iconstringIcon name
modulestringTop-level grouping
categorystringSub-grouping within module
ordernumberSort 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