Framework M Plugin System Guide
This guide explains how the Framework M plugin architecture works and how to set up auto-discovery for new modules in your local workspace.
Overview
Framework M uses a Pluggable Shell Architecture. The main application (business-m) acts as a "Shell" that discovers and mounts independent plugins (like finance, wms, or m_invoice) at runtime.
Key Components
- Plugin Manifest (
plugin.config.ts): Defines the plugin's metadata, sidebar menus (manifests), routes, and services. - Vite Discovery Plugin: Scans the workspace for packages marked as Framework M modules.
- Virtual Module: Collects all discovered plugins into a single importable array.
- FrameworkM Shell: The UI container that consumes these plugins and renders the dual-sidebar navigation.
1. Marking a Package as a Plugin
For a package (app or lib) to be discoverable, it must have a specific entry in its package.json.
Example: libs/finance/frontend/package.json
{
"name": "@finance/frontend",
"framework-m": {
"type": "frontend-module",
"plugin": "./src/plugin.config.ts"
}
}
type: Must befrontend-module.plugin: Path to the plugin configuration file relative topackage.json.
2. Configuring the Plugin Manifest
Each plugin must export a FrameworkMPlugin object.
Example: libs/finance/frontend/src/plugin.config.ts
import type { FrameworkMPlugin } from "@framework-m/plugin-sdk";
import { initializeFinanceCustomization } from "./customization";
const plugin: FrameworkMPlugin = {
name: "finance",
version: "0.1.0",
// onInit hook for custom logic (e.g., registering custom view renderers)
onInit: () => {
initializeFinanceCustomization();
},
manifests: [
{
app_id: "finance",
label: "Finance",
icon: "layout-grid",
resources: [
{
name: "finance.dashboard",
label: "Dashboard",
route: "/finance/dashboard",
icon: "layout-dashboard",
}
],
}
],
// ...
};
Manifest Deduplication
When combining multiple plugins, be careful with shared app_id values (like core). If multiple plugins define a manifest for the same app_id, the shell will attempt to merge them.
To avoid duplicate icons in the sidebar, ensure that global items (like System Settings) are defined in only one place.
If you see duplicate icons in the sidebar, check the manifests array in your plugin.config.ts files and comment out any redundant definitions of the same app_id.
3. Enabling Auto-Discovery in the Shell
The host application (apps/business-m) needs to be configured to find these plugins.
Step A: Update vite.config.ts
The Vite plugin needs to know where to look for package.json files.
// apps/business-m/frontend/vite.config.ts
import { frameworkMPlugin } from "@framework-m/vite-plugin";
export default defineConfig({
plugins: [
react(),
frameworkMPlugin({
searchPaths: [
path.resolve(__dirname, "../../apps"),
path.resolve(__dirname, "../../libs"),
],
}),
],
});
Step B: Integrate in index.tsx
Use the virtual:framework-m-plugins module to pass the discovered list to the FrameworkMShell.
// apps/business-m/frontend/src/index.tsx
import discoveredPlugins from "virtual:framework-m-plugins";
createRoot(rootElement).render(
<FrameworkMShell
title="BusinessM"
initialPlugins={discoveredPlugins}
>
<Dashboard />
</FrameworkMShell>
);
4. Local Setup for New Developers
If a new developer joins the project, they only need to:
- Clone the Repo: Ensure they have the full workspace structure (
apps/andlibs/). - Install Dependencies: Run
pnpm installornpm installat the root. - Run Dev Server:
cd apps/business-muv run m dev
- Verification:
The console will log the discovered plugins:
[framework-m/vite-plugin] Discovered 2 Framework M plugin(s): @finance/frontend, business-m
5. Troubleshooting
Plugin not appearing in sidebar?
- Check that
app_idinmanifestsis unique. - Ensure the
iconname matches a valid Lucide icon. - Verify
package.jsonhas the"framework-m"field.
404 on navigation?
- Ensure the
routeinresourcesmatches thepathinroutes. - Check that the backend DocType (if using
type: "DocType") is registered in theMetaRegistry.
6. Service Namespacing and API Prefixing
When working in a Macroservice architecture, plugins are often served by different backend services. Framework M provides a built-in namespacing strategy to handle this.
Setting the Service Context
If your plugin belongs to a specific macroservice (e.g., finance), ensure that the FRAMEWORK_M_SERVICE_NAME environment variable is set during the build process.
Naming Convention: Service names must be lowercase (e.g., finance, wms). This name becomes part of the API path: /api/finance/{version}/.
# Example build command for a specific service
FRAMEWORK_M_SERVICE_NAME=finance pnpm build
The @framework-m/vite-plugin will inject this name, allowing the frontend to automatically prefix all requests with /api/finance/{version}/.
Namespaced Resource Access
If you need to access a resource from another service (e.g., hr), use the namespaced resource pattern:
// Accessing a DocType from another macroservice
const data = await dataProvider.getOne("hr/Employee", { id: "123" });
The system will automatically resolve this to GET /api/hr/{version}/Employee/123 instead of using your local service prefix.