Skip to main content

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

  1. Plugin Manifest (plugin.config.ts): Defines the plugin's metadata, sidebar menus (manifests), routes, and services.
  2. Vite Discovery Plugin: Scans the workspace for packages marked as Framework M modules.
  3. Virtual Module: Collects all discovered plugins into a single importable array.
  4. 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 be frontend-module.
  • plugin: Path to the plugin configuration file relative to package.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 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:

  1. Clone the Repo: Ensure they have the full workspace structure (apps/ and libs/).
  2. Install Dependencies: Run pnpm install or npm install at the root.
  3. Run Dev Server:
    cd apps/business-m
    uv run m dev
  4. 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_id in manifests is unique.
  • Ensure the icon name matches a valid Lucide icon.
  • Verify package.json has the "framework-m" field.

404 on navigation?

  • Ensure the route in resources matches the path in routes.
  • Check that the backend DocType (if using type: "DocType") is registered in the MetaRegistry.

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.

info

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.