Skip to main content

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