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.

[!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.