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.
[!TIP] If you see duplicate icons in the sidebar, check the
manifestsarray in yourplugin.config.tsfiles and comment out any redundant definitions of the sameapp_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.
[!IMPORTANT] 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.