Skip to main content

Plugin Package Architecture - Quick Reference

Visual Architecture Diagram

┌─────────────────────────────────────────────────────────────────┐
│ EXTERNAL DEPENDENCIES │
│ @refinedev/*, react, react-router-dom, @tanstack/react-query │
└────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ @framework-m/desk (CORE UI LIBRARY) │
│ 📦 Published to GitLab npm: @framework-m/desk@0.1.0 │
│ 📁 Location: libs/framework-m-desk/ │
├─────────────────────────────────────────────────────────────────┤
│ Responsibilities: │
│ • Refine.dev data provider (REST API) │
│ • Auth provider (JWT, sessions) │
│ • Live provider (WebSocket) │
│ • Base components: <ListView>, <FormView>, <ShowView> │
│ • Hooks: useDocType(), useMetadata(), useWorkflow() │
│ • Utilities: formatDate(), validateForm(), etc. │
├─────────────────────────────────────────────────────────────────┤
│ Who uses it: │
│ ✓ Shell application (frontend/) │
│ ✓ All app plugins (apps/*/frontend/) │
│ ✓ Any Framework M project │
├─────────────────────────────────────────────────────────────────┤
│ Breaking changes? NO - Existing users unaffected │
└────────────────────────────┬────────────────────────────────────┘

┌────────────┴──────────────┐
│ │
▼ ▼
┌───────────────────────────┐ ┌──────────────────────────────────┐
│ @framework-m/plugin-sdk │ │ App Plugins (Workspace) │
│ (PLUGIN INFRASTRUCTURE) │ │ NOT published to npm │
│ │ │ │
│ 📦 Published: GitLab npm │ │ Example: @my-company/wms │
│ 📁 libs/plugin-sdk/ │ │ 📁 apps/wms/frontend/ │
├───────────────────────────┤ ├──────────────────────────────────┤
│ Exports: │ │ Structure: │
│ • PluginRegistry │ │ • plugin.config.ts │
│ • ServiceContainer │ │ • package.json (framework-m) │
│ • usePluginMenu() │ │ • src/pages/ │
│ • useService() │ │ • src/components/ │
│ • usePlugin() │ │ • src/services/ │
│ • FrameworkMPlugin type │ ├──────────────────────────────────┤
│ • MenuItem type │ │ Defines: │
├───────────────────────────┤ │ • Menu items │
│ Used by: │ │ • Routes │
│ ✓ Shell app │ │ • Services │
│ ✓ Vite plugin │ │ • Providers │
│ ✓ App plugins │ ├──────────────────────────────────┤
└───────────────────────────┘ │ Examples: │
│ │ • apps/wms/frontend/ │
│ │ • apps/personnel/frontend/ │
│ │ • apps/finance/frontend/ │
│ └──────────────────────────────────┘
│ │
│ │
▼ │
┌───────────────────────────────────────┐ │
│ @framework-m/vite-plugin │ │
│ (BUILD-TIME TOOLING) │ │
│ │ │
│ 📦 Published: GitLab npm │ │
│ 📁 libs/vite-plugin/ │ │
├───────────────────────────────────────┤ │
│ Responsibilities: │ │
│ • Scan workspace for plugin packages │ │
│ • Generate virtual module │ │
│ • Configure code-splitting │ │
│ • Setup HMR for plugins │ │
├───────────────────────────────────────┤ │
│ Used by: │ │
│ ✓ Shell app (vite.config.ts only) │ │
└───────────────┬───────────────────────┘ │
│ │
│ discovers │ imports
│ │
▼ │
┌─────────────────────────────────────────────┴───────────────────┐
│ frontend/ (SHELL APPLICATION) │
│ NOT published - Static build output │
│ 📁 frontend/ │
├──────────────────────────────────────────────────────────────────┤
│ Responsibilities: │
│ • Bootstrap PluginRegistry │
│ • Compose all plugins into single UI │
│ • Provide base layout (Sidebar, Header) │
│ • Handle auth flow │
│ • Error boundaries │
├──────────────────────────────────────────────────────────────────┤
│ Dependencies: │
│ • @framework-m/desk ^0.1.0 │
│ • @framework-m/plugin-sdk ^0.1.0 │
│ • @framework-m/vite-plugin ^0.1.0 (devDependency) │
│ • Workspace plugins (apps/*/frontend/) via symlinks │
├──────────────────────────────────────────────────────────────────┤
│ Build Output (frontend/dist/): │
│ • index.html │
│ • assets/main-[hash].js (shell + framework) │
│ • assets/wms-[hash].js (WMS plugin - lazy) │
│ • assets/personnel-[hash].js (Personnel plugin - lazy) │
│ │
│ Deployed as: │
│ • Python package (m build → pip install) │
│ • CDN (aws s3 sync dist/ s3://bucket/) │
│ • Container (COPY dist/ /app/static) │
└──────────────────────────────────────────────────────────────────┘

Data Flow: How Plugin Menus Work

┌─────────────────────────────────────────────────────────────────┐
│ Step 1: Plugin Definition │
│ apps/wms/frontend/plugin.config.ts │
├──────────────────────────────────────────────────────────────────┤
│ export default { │
│ name: 'wms', │
│ menu: [ │
│ { │
│ name: 'wms.warehouse', │
│ label: 'Warehouse', │
│ route: '/doctypes/wms.warehouse', │
│ module: 'Inventory', │
│ } │
│ ] │
│ } satisfies FrameworkMPlugin │
└────────────────────────────┬─────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ Step 2: Build-Time Discovery │
│ @framework-m/vite-plugin scans workspace │
├──────────────────────────────────────────────────────────────────┤
│ 1. Find all package.json files │
│ 2. Filter those with "framework-m" metadata │
│ 3. Collect plugin.config.ts paths │
│ 4. Generate virtual module: │
│ │
│ // virtual:framework-m-plugins │
│ import wms from 'apps/wms/frontend/dist/plugin.config.js'; │
│ import personnel from 'apps/personnel/.../plugin.config.js'; │
│ export default [wms, personnel]; │
└────────────────────────────┬─────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ Step 3: Runtime Registration │
│ frontend/src/App.tsx bootstrap() │
├──────────────────────────────────────────────────────────────────┤
│ import plugins from 'virtual:framework-m-plugins'; │
│ │
│ const registry = new PluginRegistry(); │
│ for (const plugin of plugins) { │
│ await registry.register(plugin); │
│ } │
│ │
│ // Registry now contains: │
│ // - wms.menu = [{name: 'wms.warehouse', ...}] │
│ // - personnel.menu = [...] │
└────────────────────────────┬─────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ Step 4: Menu Merging │
│ PluginRegistry.getMenu() │
├──────────────────────────────────────────────────────────────────┤
│ 1. Collect all menu items from all plugins │
│ 2. Group by module: │
│ - Inventory: [wms.warehouse, wms.bin_location, ...] │
│ - HR: [personnel.employee, ...] │
│ 3. Sort by order │
│ 4. Return merged tree: │
│ │
│ [ │
│ { │
│ name: 'inventory', │
│ label: 'Inventory', │
│ children: [ │
│ { name: 'wms.warehouse', label: 'Warehouse' }, │
│ { name: 'wms.bin_location', label: 'Bin Location' } │
│ ] │
│ }, │
│ { name: 'hr', label: 'HR', children: [...] } │
│ ] │
└────────────────────────────┬─────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ Step 5: UI Rendering │
│ frontend/src/layout/Sidebar.tsx │
├──────────────────────────────────────────────────────────────────┤
│ function Sidebar() { │
│ const pluginMenu = usePluginMenu(); // Hook from plugin-sdk │
│ │
│ return ( │
│ <nav> │
│ {pluginMenu.map(moduleGroup => ( │
│ <ModuleSection key={moduleGroup.name}> │
│ <Icon name={moduleGroup.icon} /> │
│ {moduleGroup.label} │
│ {moduleGroup.children.map(item => ( │
│ <MenuItem │
│ route={item.route} │
│ label={item.label} │
│ /> │
│ ))} │
│ </ModuleSection> │
│ ))} │
│ </nav> │
│ ) │
│ } │
└──────────────────────────────────────────────────────────────────┘

Package Dependency Chain

Level 0 (External):
react, react-dom, @refinedev/*, @tanstack/react-query


Level 1 (Core):
@framework-m/desk

├──────────────────┐
│ │
▼ ▼
Level 2 (Plugin System):
@framework-m/plugin-sdk App Plugins (@my-company/wms)
│ │
├─────────────────────────┤
│ │
▼ │
Level 3 (Build Tools):
@framework-m/vite-plugin │
│ │
└────────┬────────────────┘


Level 4 (Shell):
frontend/ (Shell App)


Level 5 (Build Output):
frontend/dist/ (Static Assets)

Workspace Structure Example

my-erp-project/
├── pnpm-workspace.yaml
├── package.json

├── frontend/ # Shell Application
│ ├── package.json
│ │ └── dependencies:
│ │ ├── @framework-m/desk: ^0.1.0
│ │ └── @framework-m/plugin-sdk: ^0.1.0
│ ├── vite.config.ts
│ │ └── plugins: [frameworkMPlugin()]
│ ├── src/
│ │ ├── App.tsx # Bootstrap plugins
│ │ └── layout/Sidebar.tsx # Uses usePluginMenu()
│ └── dist/ # Build output

├── apps/
│ ├── wms/
│ │ ├── backend/ # Python app
│ │ │ └── pyproject.toml
│ │ └── frontend/ # WMS Plugin
│ │ ├── package.json
│ │ │ ├── name: @my-company/wms
│ │ │ ├── private: true
│ │ │ ├── framework-m:
│ │ │ │ └── plugin: ./dist/plugin.config.js
│ │ │ └── dependencies:
│ │ │ ├── @framework-m/desk: ^0.1.0
│ │ │ └── @framework-m/plugin-sdk: ^0.1.0
│ │ ├── plugin.config.ts # Menu, routes, services
│ │ └── src/
│ │ ├── pages/
│ │ ├── components/
│ │ └── services/
│ │
│ └── personnel/
│ ├── backend/
│ └── frontend/ # Personnel Plugin
│ ├── package.json
│ ├── plugin.config.ts
│ └── src/

└── libs/ # Framework packages
├── framework-m-desk/ # Published to npm
│ ├── package.json
│ │ └── name: @framework-m/desk
│ └── src/
├── framework-m-plugin-sdk/ # Published to npm
│ ├── package.json
│ │ └── name: @framework-m/plugin-sdk
│ └── src/
└── framework-m-vite-plugin/ # Published to npm
├── package.json
│ └── name: @framework-m/vite-plugin
└── src/

Package Publishing Matrix

PackageLocationPublished?RegistryVersioning
@framework-m/desklibs/framework-m-desk/✅ YesGitLab npmFollows Python
@framework-m/plugin-sdklibs/framework-m-plugin-sdk/✅ YesGitLab npmIndependent
@framework-m/vite-pluginlibs/framework-m-vite-plugin/✅ YesGitLab npmIndependent
frontend/ (Shell)frontend/❌ No--
@my-company/wmsapps/wms/frontend/❌ NeverWorkspacePrivate
@my-company/personnelapps/personnel/frontend/❌ NeverWorkspacePrivate

When to Use Each Package

Use @framework-m/desk when:

  • ✅ Building any Framework M frontend (single or multi-app)
  • ✅ Need data provider for REST API
  • ✅ Need auth provider
  • ✅ Need base DocType components
  • ✅ Want to use Framework M hooks

Use @framework-m/plugin-sdk when:

  • ✅ Building multi-app project with plugins
  • ✅ Need to access plugin menus/routes
  • ✅ Want to register services
  • ✅ Creating reusable app modules

Use @framework-m/vite-plugin when:

  • ✅ Setting up shell app build
  • ✅ Need auto-discovery of plugins
  • ✅ Want code-splitting per plugin
  • ✅ Building multi-app project

Don't use plugins if:

  • ❌ Single-app project (just use @framework-m/desk directly)
  • ❌ Simple customization (add pages directly to frontend/src/)
  • ❌ Prototyping (overhead not worth it)

Migration Path

Existing Single-App Projects (No Changes Needed)

// Current setup - Still works!
{
"name": "my-app-frontend",
"dependencies": {
"@refinedev/core": "^5.0.0",
"react": "^19.0.0"
}
}

// Add desk when ready (optional)
{
"dependencies": {
"@framework-m/desk": "^0.1.0", // ← Just add this
"react": "^19.0.0"
}
}

New Multi-App Projects

// Shell app
{
"name": "frontend",
"dependencies": {
"@framework-m/desk": "^0.1.0",
"@framework-m/plugin-sdk": "^0.1.0"
},
"devDependencies": {
"@framework-m/vite-plugin": "^0.1.0"
}
}

// Plugin app (workspace package)
{
"name": "@my-company/wms",
"private": true,
"framework-m": {
"plugin": "./dist/plugin.config.js"
},
"dependencies": {
"@framework-m/desk": "workspace:^0.1.0",
"@framework-m/plugin-sdk": "workspace:^0.1.0"
}
}

Dependency Architecture: Avoiding Singleton Violations

When building modular frontends (Shell + Plugins), a critical challenge is managing shared stateful libraries like React, React Query, i18next, and Refine. These libraries often rely on global Singletons or Contexts that must only have one instance in the browser.

The Problem: Singleton Clashes

If both libs/framework-m-desk and a plugin (apps/wms/frontend) were to include @tanstack/react-query in their dependencies, the browser might load two separate copies of the library. This results in:

  • Context mismatch (plugin cannot find the Shell's QueryClient).
  • Inconsistent state (Shell shows "Loading" while Plugin is "Idle").
  • Inflated bundle sizes.

The Solution: Universal PeerDependencies

Our core libraries (@framework-m/desk and @framework-m/ui) utilize peerDependencies for all bridge-level orchestration and stateful libraries.

LibraryRole in Desk/UIRelationship
reactComponent EnginepeerDependency
@refinedev/coreData OrchestrationpeerDependency
@tanstack/react-queryState ManagementpeerDependency
i18nextLocalizationpeerDependency

Host vs. Plugin Responsibilities

1. The Host (Shell) - frontend/

The Host is the final bundler and the single source of truth for dependency resolution.

  • Responsibility: It must include all peerDependencies required by its linked libraries in its own direct dependencies.
  • Result: It provides the actual implementation of the singleton at runtime.

2. The Core Library - libs/framework-m-desk/

  • Responsibility: Declares peerDependencies. It says: "I need these to work, but I expect the consumer to provide them."
  • Result: It remains lightweight and avoids forcing its version onto the bundle.

3. The Plugin - apps/*/frontend/

  • Responsibility: Should follow the same pattern—declare shared libraries as peerDependencies or devDependencies (for testing), but never as final dependencies if they are already provided by the Shell.
  • Result: The Plugin's build output contains only its unique logic, while the Shell handles the global plumbing.

Single Source of Truth Table

FeatureProvider (Owner)Consumer
Theme@framework-m/ui (Provider)All Plugins
Auth@framework-m/desk (AuthProvider)Shell Layout
API CacheShell (QueryClientProvider)All Hooks
RoutingShell (BrowserRouter)All Plugins

By strictly adhering to this pattern, we ensure that the entire Microservice Frontend (MFE) ecosystem behaves as a single cohesive application while allowing independent development of business modules.