Skip to main content

Plugin SDK API Reference

Complete API reference for @framework-m/plugin-sdk and @framework-m/vite-plugin.

@framework-m/plugin-sdk​

Types​

FrameworkMPlugin​

interface FrameworkMPlugin {
name: string;
version: string;
minSdkVersion?: string;
peerPlugins?: string[];
manifests?: NavigationManifest[];
navigationIcons?: Record<string, NavigationIconComponent>;
routes?: RouteDefinition[];
services?: Record<string, ServiceFactory>;
providers?: Provider[];
doctypes?: DocTypeExtension[];
ownedDocTypes?: string[];
widgets?: Widget[];
pages?: GlobalPageExtension[];
traversals?: Record<string, (...args: unknown[]) => unknown>;
discoveryUnit?: DiscoveryUnit;
onInit?: () => Promise<void> | void;
onDestroy?: () => Promise<void> | void;
}
interface NavigationManifest {
app_id: string;
label: string;
icon: string;
resources: NavigationNode[];
}
interface NavigationNode {
name: string;
label: string;
type?: "DocType" | "Page" | "Report" | "Kiosk" | "Link";
route: string;
/** A Lucide name or a trusted code-loaded icon component. */
icon?: NavigationIcon;
feature_id?: string;
visibility_policy?: VisibilityPolicy;
badge?: string;
hardware_intent?: string;
interaction_mode?: string;
workspace_route?: string;
children?: NavigationNode[];
hidden?: boolean;
permissions?: string[];
}

NavigationManifest.icon remains string-only for the primary rail. Contextual sidebar nodes accept either a Lucide name or a component imported by a code plugin. Server JSON can use a namespaced string such as inventory:brand-mark; the owning plugin maps that key through navigationIcons. Raw SVG markup and remote URLs are intentionally unsupported.

VisibilityPolicy​

interface VisibilityPolicy {
roles?: string[];
attributes?: Record<string, unknown>;
feature_flag?: string;
}

RouteDefinition​

interface RouteDefinition {
path: string;
element:
| LazyExoticComponent<ComponentType>
| (() => Promise<{ default: ComponentType }>);
/** Render outside the authentication guard when true */
isPublic?: boolean;
}

ServiceFactory​

type ServiceFactory<T = unknown> = () => T | Promise<T>;

PermissionChecker​

type PermissionChecker = (permissions: string[]) => boolean | Promise<boolean>;

Provider​

interface Provider {
component: () => Promise<{ default: ComponentType }>;
props?: Record<string, unknown>;
}

DocTypeExtension​

interface DocTypeExtension {
doctype: string;
fields?: Record<string, unknown>;
actions?: Array<Record<string, unknown>>;
components?: Record<string, ComponentType>;
}

Widget​

interface Widget {
id: string;
title: string;
component: () => Promise<{ default: ComponentType }>;
size?: "small" | "medium" | "large";
permissions?: string[];
}

GlobalPageExtension​

interface GlobalPageExtension {
name: string;
component: ComponentType;
}

DiscoveryUnit​

Remote discovery metadata used in macroservice deployments.

interface DiscoveryUnit {
url: string;
apiUrl: string;
metaUrl: string;
openapiUrl?: string;
}

Classes​

PluginRegistry​

class PluginRegistry {
/** Access the global registry singleton */
static getInstance(): PluginRegistry;

/** Register a plugin configuration */
register(plugin: FrameworkMPlugin): Promise<void>;

/** Unregister a plugin by name */
unregister(pluginName: string): Promise<boolean>;

/** Register a resource owner hint for API namespacing */
registerOwner(resource: string, owner: string | null): void;

/**
* Resolve a standard `/api/...` URL to a service-specific namespace.
* In macroservice mode this prefixes the URL with the owning plugin's
* service name or discovery unit.
*/
resolveApiUrl(url: string): string;

/** Determine which plugin owns a specific resource */
getOwner(resourceName: string): string | null | undefined;

/** Get merged navigation manifests from all plugins */
getApps(): NavigationManifest[];

/** Get aggregated route definitions from all plugins */
getRoutes(): RouteDefinition[];

/** Get a specific plugin by name */
getPlugin(name: string): FrameworkMPlugin | undefined;

/** Get all registered plugins */
getAllPlugins(): FrameworkMPlugin[];

/** Get the plugin that owns a given resource */
getResourcePlugin(resourceName: string): FrameworkMPlugin | undefined;

/** Resolve a named service from the registry DI container */
getService<T = unknown>(name: string): Promise<T>;

/** Get the DI service container */
getServiceContainer(): ServiceContainer;

/** Aggregate dashboard widgets from all plugins */
getWidgets(): Widget[];

/** Configure a permission checker */
setPermissionChecker(checker: PermissionChecker | null): void;
getPermissionChecker(): PermissionChecker | null;

/** Compatibility and diagnostics */
checkCompatibility(): CompatibilityReport[];
getDiagnostics(severity?: PluginRegistryDiagnosticSeverity): PluginRegistryDiagnostic[];
clearDiagnostics(): void;

/** Subscribe to plugin lifecycle events */
on<E extends PluginRegistryEvent>(event: E, handler: PluginRegistryEventHandler<E>): () => void;

/** Reset registry state for test isolation */
reset(): void;
}

ServiceContainer​

class ServiceContainer {
register<T>(name: string, factory: ServiceFactory<T>): void;
get<T = unknown>(name: string): Promise<T>;
has(name: string): boolean;
getAll(): string[];
clear(): void;
}

React Context and Hooks​

PluginRegistryProvider​

import {
PluginRegistryProvider,
PluginRegistry,
} from "@framework-m/plugin-sdk";

const registry = new PluginRegistry();
await registry.register(wmsPlugin);

<PluginRegistryProvider registry={registry}>
<App />
</PluginRegistryProvider>;

usePluginApps()​

import { usePluginApps } from "@framework-m/plugin-sdk";

function Sidebar() {
const apps = usePluginApps();
return apps.map(app => (
<section key={app.app_id}>
<h3>{app.label}</h3>
{app.resources.map(node => (
<div key={node.name}>{node.label}</div>
))}
</section>
));
}

usePlugin(name)​

import { usePlugin } from "@framework-m/plugin-sdk";

function PluginInfo() {
const wms = usePlugin("wms");
return wms ? (
<span>
{wms.name} v{wms.version}
</span>
) : null;
}

useService(name)​

import { useService } from "@framework-m/plugin-sdk";

function StockLevel() {
const { service, isLoading, error } = useService("inventoryService");
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <div>{service.getStockLevel()}</div>;
}

useWidgets()​

import { useWidgets } from "@framework-m/plugin-sdk";

function Dashboard() {
const widgets = useWidgets();
return widgets.map(widget => <WidgetHost key={widget.id} widget={widget} />);
}

@framework-m/vite-plugin​

frameworkMPlugin(options?)​

import { frameworkMPlugin } from "@framework-m/vite-plugin";

export default defineConfig({
plugins: [react(), frameworkMPlugin()],
});

What it does:

  1. Workspace Scanning: Automatically traverses the workspace to find package.json files containing "framework-m": { "plugin": "..." }.
  2. Selective Bundling:
    • If the local package.json identifies as a Plugin, it only bundles the local plugin code.
    • If the local package is a Shell App, it bundles all discovered plugins from the workspace.
  3. Virtual Modules: Generates virtual:framework-m-plugins to aggregate discovered plugins.
  4. MFE Configuration: Injects FRAMEWORK_M_SERVICE_NAME via define for runtime identity.

Virtual Module Output​

// virtual:framework-m-plugins
import plugin_0 from "/path/to/apps/wms/frontend/src/plugin.config.ts";
import plugin_1 from "/path/to/apps/hr/frontend/src/plugin.config.ts";
export default [plugin_0, plugin_1];

Backend Plugin Extension APIs​

In addition to frontend widgets and pages, backend services can be customized or extended by overriding protocol dependencies in the DI container.

OAuthClaimMapper​

Use the OAuthClaimMapper interface to intercept and parse token claim payloads from external OIDC identity providers during federated or social login callback exchanges.

Interface Signature​

from typing import Any
from framework_m_core.interfaces.oauth import OAuth2UserInfo


class OAuthClaimMapper:
"""Interface for custom OIDC claims mapping."""

async def map_claims(self, user_info: OAuth2UserInfo) -> dict[str, Any]:
"""Map raw provider claims to LocalUser model attribute names.

Args:
user_info: The normalized OAuth2UserInfo details, containing
the provider name and raw_claims dict.

Returns:
A dictionary of LocalUser attribute names -> mapping values.
"""
pass

OAuth2UserInfo​

Normalized container payload representing user metadata fetched from OIDC /userinfo or token payload claims.

Model Attributes​

from dataclasses import dataclass


@dataclass(frozen=True)
class OAuth2UserInfo:
provider: str # OAuth provider name (e.g. google, oidc-main)
provider_user_id: str # Unique provider subject ID (e.g. sub or custom mapped oid)
email: str | None # Email address returned by the provider
display_name: str # Normalized display name of the user
avatar_url: str | None = None # Avatar image URL (if available)
phone_number: str | None = None # Verified phone number claim (if available)
phone_verified: bool = False # Whether the phone number is verified
raw_claims: dict[str, Any] = None # Dictionary containing all raw OIDC token claims

Lifecycle & Diagnostics​

The registry emits events and diagnostics during registration.

type PluginRegistryEvent = "plugin:registered" | "plugin:error";
type PluginRegistryDiagnosticSeverity = "info" | "warning" | "error";

Common diagnostic codes:

CodeSeverityMeaning
SDK_INCOMPATIBLEwarningminSdkVersion is newer than the current SDK.
PLUGIN_DUPLICATEwarningPlugin name already registered and will be replaced.
ROUTE_COLLISIONerrorDuplicate route path across plugins.
MANIFEST_APP_ID_COLLISIONinfo/warningMultiple plugins contribute to the same app_id.
NODE_ROUTE_COLLISIONerrorDuplicate navigation node route across plugins.
SERVICE_COLLISIONerrorDuplicate service name across plugins.

Minimal Plugin Example​

// apps/my-plugin/frontend/plugin.config.ts
import type { FrameworkMPlugin } from "@framework-m/plugin-sdk";

const plugin: FrameworkMPlugin = {
name: "my-plugin",
version: "1.0.0",
manifests: [
{
app_id: "my-plugin",
label: "My Plugin",
icon: "box",
resources: [
{
name: "my-plugin.item",
label: "Items",
route: "/app/my-plugin.item/list",
type: "DocType",
},
],
},
],
routes: [
{
path: "/my-plugin/dashboard",
element: () => import("./pages/Dashboard"),
},
],
};

export default plugin;

See Also​