Plugin Composition Patterns
Overview
Framework M's multi-package UI architecture enables powerful composition patterns where multiple packages can contribute, extend, and override UI components at build time.
Registration Order
Plugins are registered in a specific order that determines override precedence:
// 1. Installed packages (alphabetically by package name)
registerPlugin({ name: "business_m" });
registerPlugin({ name: "crm_app" });
registerPlugin({ name: "wms_app" });
// 2. Local workspace apps (alphabetically by app name)
registerPlugin({ name: "custom_app" });
// Later registrations win for the same route/component
Pattern 1: Page Override
Replace a page from another package entirely.
Example: Custom CRM Lead Form
Standard CRM (crm_app):
// crm_app/frontend/index.ts
registerPlugin({
name: "crm_app",
pages: [
{
path: "/app/lead/:id",
component: LeadForm,
},
],
});
Custom Extension (acme_crm_ext):
// acme_crm_ext/frontend/index.ts
import { LeadForm as StandardLeadForm } from "@crm-app/frontend/pages/LeadForm";
function AcmeLeadForm(props) {
return (
<div>
<StandardLeadForm {...props} />
{/* Add Acme-specific fields */}
<AcmeCustomFields leadId={props.id} />
</div>
);
}
registerPlugin({
name: "acme_crm_ext",
pages: [
{
path: "/app/lead/:id",
component: AcmeLeadForm,
override: true, // ← Explicitly override
},
],
});
Result: When users navigate to /app/lead/123, they see AcmeLeadForm instead of the standard LeadForm.
Pattern 2: Slot Injection
Add components to predefined slots without overriding the entire page.
Example: Dashboard Widgets
Base Package (framework-m-desk):
// Defines slot in Dashboard
registerPlugin({
name: "framework_m_desk",
pages: [
{
path: "/app/dashboard",
component: Dashboard,
slots: ["dashboard.widgets"], // ← Define available slots
},
],
});
// Dashboard.tsx renders slot
import { SlotRenderer } from "@framework-m/core";
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<SlotRenderer slot="dashboard.widgets" />
</div>
);
}
Extension Packages (multiple):
// business_m/frontend/index.ts
registerPlugin({
name: "business_m",
slots: [
{
slot: "dashboard.widgets",
component: SalesWidget,
priority: 10,
},
],
});
// wms_app/frontend/index.ts
registerPlugin({
name: "wms_app",
slots: [
{
slot: "dashboard.widgets",
component: StockLevelWidget,
priority: 20, // ← Higher priority = rendered first
},
],
});
// crm_app/frontend/index.ts
registerPlugin({
name: "crm_app",
slots: [
{
slot: "dashboard.widgets",
component: LeadPipelineWidget,
priority: 15,
},
],
});
Result: Dashboard automatically renders all three widgets in priority order: StockLevelWidget (20) → LeadPipelineWidget (15) → SalesWidget (10).
Pattern 3: Component Extension
Wrap or enhance components from base packages.
Example: Enhanced Customer Form
Base Package (business_m):
// business_m/frontend/components/CustomerForm.tsx
export function CustomerForm({ customer, onSave }) {
return (
<form>
<Input name="customer_name" value={customer.name} />
<Input name="email" value={customer.email} />
<Button onClick={onSave}>Save</Button>
</form>
);
}
Extension (custom_app):
// custom_app/frontend/components/EnhancedCustomerForm.tsx
import { CustomerForm } from "@business-m/frontend/components/CustomerForm";
import { CreditScoreWidget } from "./CreditScoreWidget";
export function EnhancedCustomerForm(props) {
return (
<div>
{/* Standard form */}
<CustomerForm {...props} />
{/* Additional features */}
<CreditScoreWidget customerId={props.customer.id} />
<PaymentHistorySection customerId={props.customer.id} />
</div>
);
}
registerPlugin({
name: "custom_app",
components: {
CustomerForm: EnhancedCustomerForm, // ← Override component
},
});
Pattern 4: Hook Composition
Extend or wrap hooks from other packages.
Example: Enhanced Data Fetching
Base Package:
// business_m/frontend/hooks/useCustomer.ts
export function useCustomer(id: string) {
const { data, isLoading } = useQuery(["customer", id], () =>
fetch(`/api/customer/${id}`),
);
return { customer: data, isLoading };
}
Extension:
// custom_app/frontend/hooks/useCustomerWithScore.ts
import { useCustomer } from "@business-m/frontend/hooks/useCustomer";
export function useCustomerWithScore(id: string) {
const { customer, isLoading } = useCustomer(id);
// Add credit score data
const { data: score } = useQuery(["credit-score", id], () =>
fetch(`/api/credit-score/${id}`),
);
return {
customer: { ...customer, creditScore: score },
isLoading,
};
}
Pattern 5: Multi-Package Composition
Combine features from multiple packages into a unified experience.
Example: Unified Customer View
// custom_app/frontend/pages/UnifiedCustomer.tsx
import { CustomerCard } from "@business-m/frontend/components/CustomerCard";
import { CustomerOrders } from "@wms-app/frontend/components/CustomerOrders";
import { CustomerLeads } from "@crm-app/frontend/components/CustomerLeads";
import { CustomerSupport } from "@hr-app/frontend/components/CustomerSupport";
export function UnifiedCustomerView({ customerId }) {
return (
<Page>
<Grid>
{/* From business-m */}
<CustomerCard id={customerId} />
{/* From wms-app */}
<CustomerOrders customerId={customerId} />
{/* From crm-app */}
<CustomerLeads customerId={customerId} />
{/* From hr-app */}
<CustomerSupport customerId={customerId} />
</Grid>
</Page>
);
}
registerPlugin({
name: "custom_app",
pages: [
{
path: "/app/customer/:id/unified",
component: UnifiedCustomerView,
},
],
});
Pattern 6: Conditional Registration
Register different UI based on configuration or feature flags.
Example: Environment-Specific Features
// custom_app/frontend/index.ts
import { registerPlugin } from "@framework-m/core";
import { DevelopmentTools } from "./components/DevelopmentTools";
import { AdminPanel } from "./components/AdminPanel";
const isDevelopment = import.meta.env.MODE === "development";
const enableAdmin = import.meta.env.VITE_ENABLE_ADMIN === "true";
registerPlugin({
name: "custom_app",
slots: [
// Development-only tools
...(isDevelopment
? [
{
slot: "desk.toolbar",
component: DevelopmentTools,
},
]
: []),
// Admin-only panel
...(enableAdmin
? [
{
slot: "desk.sidebar.top",
component: AdminPanel,
},
]
: []),
],
});
Pattern 7: Type-Safe Component Registry
Share component types across packages for type-safe composition.
Setup Type Definitions
Base Package (business_m):
// business_m/frontend/types/components.ts
export interface CustomerCardProps {
customerId: string;
variant?: "compact" | "detailed";
onUpdate?: (customer: Customer) => void;
}
// business_m/frontend/components/CustomerCard.tsx
import type { CustomerCardProps } from "../types/components";
export function CustomerCard(props: CustomerCardProps) {
// Implementation
}
Extension Package:
// custom_app/frontend/components/DashboardCustomerCard.tsx
import type { CustomerCardProps } from "@business-m/frontend/types/components";
import { CustomerCard } from "@business-m/frontend/components/CustomerCard";
// Type-safe wrapper
export function DashboardCustomerCard(props: CustomerCardProps) {
return (
<div className="dashboard-widget">
<CustomerCard {...props} variant="compact" />
</div>
);
}
Pattern 8: Theme Customization
Override theme tokens from base packages.
Example: Custom Branding
Base Theme (framework-m-desk):
// framework-m-desk/frontend/theme/index.ts
export const theme = {
colors: {
primary: "#3b82f6",
secondary: "#8b5cf6",
},
fonts: {
body: "Inter, sans-serif",
heading: "Poppins, sans-serif",
},
};
Custom Theme (custom_app):
// custom_app/frontend/theme/index.ts
import { theme as baseTheme } from "@framework-m-desk/frontend/theme";
export const theme = {
...baseTheme,
colors: {
...baseTheme.colors,
primary: "#0ea5e9", // ← Acme Corp blue
secondary: "#06b6d4",
},
fonts: {
...baseTheme.fonts,
body: "Roboto, sans-serif", // ← Custom font
},
};
registerPlugin({
name: "custom_app",
theme, // ← Override theme
});
Override Resolution Rules
When multiple plugins register the same path/component/slot:
Pages
// Rule: Last registration wins
// crm_app registers first
registerPlugin({
name: "crm_app",
pages: [{ path: "/app/lead/:id", component: LeadForm }],
});
// custom_app registers later → WINS
registerPlugin({
name: "custom_app",
pages: [{ path: "/app/lead/:id", component: CustomLeadForm }],
});
// Result: /app/lead/123 renders CustomLeadForm
Slots
// Rule: All registrations render (sorted by priority)
registerPlugin({
name: "pkg1",
slots: [{ slot: "sidebar", component: Widget1, priority: 10 }],
});
registerPlugin({
name: "pkg2",
slots: [{ slot: "sidebar", component: Widget2, priority: 20 }],
});
// Result: Renders [Widget2, Widget1] (priority 20 first)
Components
// Rule: Last registration in component registry wins
registerPlugin({
name: "business_m",
components: { CustomerCard: BaseCustomerCard },
});
registerPlugin({
name: "custom_app",
components: { CustomerCard: CustomCustomerCard }, // ← WINS
});
// Other plugins importing CustomerCard get CustomCustomerCard
Best Practices
1. Explicit Override Flag
Always use override: true when intentionally replacing a page:
registerPlugin({
name: "custom_app",
pages: [
{
path: "/app/customer/:id",
component: CustomCustomerPage,
override: true, // ← Makes intent clear
},
],
});
2. Semantic Slot Names
Use hierarchical, descriptive slot names:
// ✅ Good
"desk.sidebar.top";
"desk.sidebar.bottom";
"page.customer.header";
"page.customer.footer";
// ❌ Bad
"slot1";
"sidebar";
"custom";
3. Document Public APIs
If your package exports components for others to use, document them:
/**
* Customer card component with customization options.
*
* @example
* ```tsx
* import { CustomerCard } from "@business-m/frontend";
*
* <CustomerCard
* customerId="CUST-001"
* variant="detailed"
* onUpdate={handleUpdate}
* />
* ```
*/
export function CustomerCard(props: CustomerCardProps) {
// Implementation
}
4. Version Compatibility
Check for compatible versions when composing:
import { version as businessMVersion } from "@business-m/frontend";
if (!businessMVersion.startsWith("1.")) {
console.warn("custom_app requires business-m ^1.0.0");
}
Debugging Composition
View Registered Plugins
import { getRegisteredPlugins } from "@framework-m/core";
console.log(getRegisteredPlugins());
// [
// { name: "business_m", version: "1.0.0", pages: [...], slots: [...] },
// { name: "crm_app", version: "2.1.0", pages: [...], slots: [...] },
// { name: "custom_app", version: "1.0.0", pages: [...], slots: [...] },
// ]
Debug Slot Rendering
import { getSlotComponents } from "@framework-m/core";
console.log(getSlotComponents("dashboard.widgets"));
// [
// { plugin: "wms_app", component: StockLevelWidget, priority: 20 },
// { plugin: "crm_app", component: LeadPipelineWidget, priority: 15 },
// { plugin: "business_m", component: SalesWidget, priority: 10 },
// ]
Trace Component Origin
import { getComponentSource } from "@framework-m/core";
const source = getComponentSource("CustomerCard");
console.log(source);
// { plugin: "custom_app", originalPlugin: "business_m", overridden: true }
Next Steps
- Package Frontend Guide - Complete package structure reference
- Plugin-Host Composition Patterns - Reference examples and CI/CD templates
- Multi-Package UI Composition Architecture - Progressive deployment strategy
- Protocol Reference - Full plugin-related protocol documentation