Tutorial: Building a Multi-Module App
Build a WMS (Warehouse Management) + Personnel app using the Framework M plugin system.
Prerequisites
- Framework M workspace set up (
pnpm installdone) - Node.js 20+, Python 3.12+
Step 1: Create the WMS App
m new:app wms --with-frontend
This creates both the Python backend and frontend plugin:
wms/
├── pyproject.toml
├── src/wms/doctypes/
├── frontend/
│ ├── package.json # framework-m plugin metadata
│ └── src/
│ └── plugin.config.ts # Plugin manifest
Step 2: Define a DocType
Create a DocType for inventory items:
m new:doctype InventoryItem --app wms
Edit the generated DocType to add fields (refer to Defining DocTypes).
Step 3: Add Frontend Pages
Create a dashboard page:
// wms/frontend/src/pages/Dashboard.tsx
import { useDocTypes } from "@framework-m/desk";
export default function WmsDashboard() {
const { resources } = useDocTypes();
return (
<div style={{ padding: "2rem" }}>
<h1>WMS Dashboard</h1>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "1rem" }}>
<div className="card">
<h3>Inventory Items</h3>
<p>Manage stock levels and locations</p>
</div>
<div className="card">
<h3>Receiving</h3>
<p>Process incoming shipments</p>
</div>
<div className="card">
<h3>Picking</h3>
<p>Fulfill orders from warehouse</p>
</div>
</div>
</div>
);
}
Create an inventory list page:
// wms/frontend/src/pages/Inventory.tsx
import { AutoTable, useDocTypeMeta } from "@framework-m/desk";
export default function Inventory() {
const { schema } = useDocTypeMeta("InventoryItem");
return (
<div style={{ padding: "2rem" }}>
<h1>Inventory</h1>
<AutoTable resource="InventoryItem" schema={schema} />
</div>
);
}
Step 4: Register Routes and Menus
Edit the plugin manifest:
// wms/frontend/src/plugin.config.ts
import type { FrameworkMPlugin } from "@framework-m/plugin-sdk";
const plugin: FrameworkMPlugin = {
name: "wms",
version: "0.1.0",
menu: [
{
name: "wms.dashboard",
label: "WMS Dashboard",
route: "/wms/dashboard",
icon: "warehouse",
module: "WMS",
category: "Overview",
order: 1,
},
{
name: "wms.inventory",
label: "Inventory",
route: "/wms/inventory",
icon: "package",
module: "WMS",
category: "Operations",
order: 2,
},
],
routes: [
{
path: "/wms/dashboard",
element: () => import("./pages/Dashboard"),
},
{
path: "/wms/inventory",
element: () => import("./pages/Inventory"),
},
],
};
export default plugin;
Step 5: Add a Second Module (Personnel)
m new:app personnel --with-frontend
Edit personnel/frontend/src/plugin.config.ts:
import type { FrameworkMPlugin } from "@framework-m/plugin-sdk";
const plugin: FrameworkMPlugin = {
name: "personnel",
version: "0.1.0",
menu: [
{
name: "personnel.directory",
label: "Employee Directory",
route: "/personnel/directory",
icon: "users",
module: "Personnel",
order: 1,
},
],
routes: [
{
path: "/personnel/directory",
element: () => import("./pages/Directory"),
},
],
};
export default plugin;
Step 6: Run
pnpm install
pnpm dev
Both plugins are auto-discovered. The sidebar shows:
DOCTYPES
├── Core: User, Role, ...
──────────────────────
WMS
├── Overview: WMS Dashboard
├── Operations: Inventory
PERSONNEL
├── Employee Directory
Step 7: Cross-Plugin Services (Optional)
Register a service in the WMS plugin:
// wms plugin.config.ts
services: {
inventoryService: () => import("./services/InventoryService"),
}
Consume it from the Personnel plugin:
// personnel/frontend/src/pages/Directory.tsx
import { useService } from "@framework-m/plugin-sdk";
export default function Directory() {
const { service: inventory } = useService("inventoryService");
// Access WMS inventory data from Personnel module
}
What's Happening Under the Hood
- Build time:
@framework-m/vite-pluginscans workspace, finds both plugins'package.jsonwith"framework-m"metadata - Virtual module: Generates
virtual:framework-m-pluginsimporting both plugin configs - App start:
App.tsxcallsbootstrapPlugins()→ registers both inPluginRegistry - Rendering:
PluginRegistryProvidermakes registry available → Sidebar usesusePluginMenu()→ Routes rendered viaPluginRoutes