This document provides comprehensive guidelines for developing portal plugins in the Lume Web portal framework. It outlines the common structure, patterns, and best practices observed across all existing plugins.
Portal plugins are independently deployable micro-frontends that integrate with the main portal application through Module Federation. Each plugin is a self-contained Vite project that exposes components, routes, and capabilities to the host application.
Based on the analysis of existing plugins, there are several plugin patterns:
- portal-plugin-core: Essential functionality (NotFound routes, navigation)
- portal-plugin-dashboard: Main dashboard with authentication and file upload
- portal-plugin-ipfs: IPFS integration and file management
- portal-plugin-admin: Administrative interface
- portal-plugin-abuse: Abuse management system
- portal-plugin-abuse-report: Public abuse reporting interface
- portal-plugin-abuse-common: Shared types and API clients for abuse functionality
Every plugin must follow this standardized structure:
libs/portal-plugin-{name}/
├── package.json # Plugin metadata and dependencies
├── plugin.config.ts # Module Federation configuration
├── vite.config.ts # Vite build configuration
├── tsconfig.json # TypeScript configuration
├── tailwind.config.ts # Tailwind CSS configuration (optional)
├── postcss.config.cjs # PostCSS configuration
├── README.md # Plugin documentation
├── index.html # Development HTML entry point
├── src/ # Source code directory
│ ├── index.ts # Plugin factory function (or plugin.ts)
│ ├── routes.tsx # Route definitions
│ ├── capabilities/ # Plugin capabilities
│ │ └── refineConfig.ts # Refine configuration capability
│ ├── features/ # Plugin features
│ │ └── {featureName}/
│ │ ├── Feature.ts # Feature implementation
│ │ └── index.ts # Feature exports
│ ├── ui/ # UI components
│ │ ├── components/ # Reusable components
│ │ ├── routes/ # Route components
│ │ ├── dialogs/ # Modal/dialog components
│ │ ├── forms/ # Form components
│ │ ├── hooks/ # Custom React hooks
│ │ └── util/ # Utility functions
│ ├── types.ts # TypeScript type definitions
│ ├── contexts/ # React contexts (optional)
│ └── client/ # API client code (optional)
└── e2e/ # End-to-end tests (optional)
Every plugin must have a package.json with:
{
"name": "@lumeweb/portal-plugin-{name}",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"lint": "eslint ."
},
"dependencies": {
"@lumeweb/portal-framework-core": "workspace:*",
"@lumeweb/portal-framework-ui": "workspace:*",
"@lumeweb/portal-framework-ui-core": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router": "7.5.2"
}
}Required for all plugins except shared libraries:
import type { PluginConfig } from "@lumeweb/portal-framework-core/vite";
import { dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default {
name: "core:{plugin-name}",
dir: __dirname,
exposes: {
".": "./src/index", // or "./src/plugin" for complex plugins
// Additional exposed modules
},
} satisfies PluginConfig;All plugins use the framework's Config helper:
import { Config } from "@lumeweb/portal-framework-core/vite";
import * as sharedModules from "../../shared-modules";
import config from "./plugin.config";
export default Config({
dir: config.dir,
name: config.name,
type: "plugin",
devPort: 417X, // Unique port per plugin
exposes: config.exposes,
sharedModules: sharedModules.getSharedModules(),
});The main plugin file exports a factory function that returns a Plugin object:
import {
createNamespacedId,
Framework,
type Plugin,
} from "@lumeweb/portal-framework-core";
import { Capability as RefineConfigCapability } from "./capabilities/refineConfig";
import routes from "./routes";
import features from "./features";
export default function (): Plugin {
return {
id: createNamespacedId("core", "{plugin-name}"),
capabilities: [new RefineConfigCapability()],
features: features,
routes,
widgets: [], // Optional widget registrations
dependencies: [], // Optional plugin dependencies
capabilityAssociations: [], // Optional capability associations
async initialize(framework: Framework) {
console.log("Plugin {name} initialized");
},
async destroy(framework: Framework) {
console.log("Plugin {name} destroyed");
},
} satisfies Plugin;
}Define plugin routes using the RouteDefinition type:
import type { RouteDefinition } from "@lumeweb/portal-framework-core";
import { IconName } from "lucide-react";
const routes = [
{
path: "/{route-path}",
component: "{component-name}",
id: "{unique-route-id}",
navigation: {
label: "Navigation Label",
icon: IconName,
order: 1, // Optional ordering
},
},
{
path: "/parent",
component: "ParentLayout",
id: "parent-layout",
children: [
{
component: "ChildComponent",
index: true, // Default child route
},
{
component: "AnotherChild",
path: "child-path",
},
],
},
] satisfies RouteDefinition[];
export default routes;Capabilities define what functionality a plugin provides to the framework.
Most plugins include a Refine configuration capability:
// src/capabilities/refineConfig.ts
import {
RefineConfigCapability,
mergeRefineConfig,
} from "@lumeweb/portal-framework-core";
export class Capability implements RefineConfigCapability {
readonly id: string = "{plugin}:refine-config";
readonly type = "core:refine-config";
async initialize(framework: Framework) {
// Initialization logic
}
getConfig(existing?: Partial<RefineProps>) {
return mergeRefineConfig(existing, {
// Refine configuration
});
}
async destroy() {
// Cleanup logic
}
}Create custom capabilities for plugin-specific functionality:
export class CustomCapability implements BaseCapability {
readonly id: string = "{plugin}:custom-capability";
readonly type = "custom:type";
async initialize(framework: Framework) {
// Initialize capability
}
async destroy() {
// Cleanup capability
}
// Custom methods
getCustomConfig() {
return {};
}
}Features encapsulate major functionality within a plugin:
// src/features/{feature}/Feature.ts
import {
createNamespacedId,
Framework,
FrameworkFeature,
} from "@lumeweb/portal-framework-core";
export class Feature implements FrameworkFeature {
readonly id: NamespacedId = createNamespacedId("{plugin}", "{feature}");
async initialize(framework: Framework): Promise<void> {
// Initialize feature
}
async destroy(framework: Framework): Promise<void> {
// Cleanup feature
}
// Feature-specific methods
}src/ui/
├── components/ # Reusable components
│ ├── ComponentName.tsx
│ └── index.ts # Barrel exports
├── routes/ # Route-specific components
│ ├── routeName.tsx
│ └── layout.tsx # Layout components
├── dialogs/ # Modal/dialog components
│ └── dialogName.tsx
├── forms/ # Form components
│ └── formName.tsx
├── hooks/ # Custom hooks
│ └── useCustomHook.ts
└── util/ # Utility functions
└── util.ts
Route components should be exported as default exports and correspond to the route definitions:
// src/ui/routes/dashboard.tsx
export default function Dashboard() {
return <div>Dashboard Content</div>;
}Layout components provide common structure for child routes:
// src/ui/routes/account.layout.tsx
import { Outlet } from "react-router-dom";
export default function AccountLayout() {
return (
<div>
<header>Account Header</header>
<main><Outlet /></main>
</div>
);
}Use path aliases for cleaner imports:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}Use Tailwind CSS for styling. Include the Tailwind config:
// tailwind.config.ts
import type { Config } from "tailwindcss";
export default {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
} satisfies Config;- Unit tests: Use Vitest with Happy DOM
- E2E tests: Use Playwright (for complex plugins)
- Test files:
*.spec.tsfor unit tests,*.browser.spec.tsfor browser tests
- Format:
@lumeweb/portal-plugin-{name} - Examples:
@lumeweb/portal-plugin-dashboard,@lumeweb/portal-plugin-ipfs
- Format:
core:{plugin-name} - Examples:
core:dashboard,core:ipfs,core:admin
- Format:
{plugin}:{capability-name} - Examples:
dashboard:refine-config,ipfs:protocol
- Format:
{plugin}:{feature-name} - Examples:
dashboard:upload,ipfs:file-manager
Each plugin needs a unique development port:
- portal-plugin-core: 4174
- portal-plugin-dashboard: 4175
- portal-plugin-admin: 4175 (conflict - should be unique)
- portal-plugin-ipfs: 4176
- portal-plugin-abuse: TBD
- portal-plugin-abuse-report: TBD
Every plugin must expose:
".": Main plugin entry point
- Routes:
"./route-name": "./src/ui/routes/routeName" - Layouts:
"./layout-name": "./src/ui/routes/layoutName" - Widgets:
"./widgets/widget-name": "./src/ui/widgets/widgetName"
exposes: {
".": "./src/index",
"./dashboard": "./src/ui/routes/dashboard",
"./account/layout": "./src/ui/routes/account.layout",
"./widgets/upload/button": "./src/ui/widgets/upload/button",
}Plugins can declare dependencies on other plugins:
export default function (): Plugin {
return {
// ... other properties
dependencies: [
{
id: "core:core", // Depends on core plugin
},
],
};
}Plugins can associate capabilities with each other:
export default function (): Plugin {
return {
// ... other properties
capabilityAssociations: [
{
associated: ["ipfs:upload"],
primary: "ipfs:protocol",
},
],
};
}- Each plugin should have a single, well-defined purpose
- Avoid creating monolithic plugins
- Use shared libraries for common functionality
- Leverage framework capabilities for common functionality
- Use framework-provided UI components when possible
- Follow framework patterns for routing and navigation
- Implement proper error boundaries
- Provide meaningful error messages
- Handle initialization failures gracefully
- Lazy load heavy components
- Use code splitting for large features
- Optimize bundle sizes
- Write unit tests for core functionality
- Test plugin integration with the framework
- Include E2E tests for critical user flows
- Document plugin capabilities and features
- Provide usage examples
- Include setup and configuration instructions
# Create new plugin directory
mkdir libs/portal-plugin-{name}
cd libs/portal-plugin-{name}
# Initialize package.json
pnpm init
# Install dependencies
pnpm add @lumeweb/portal-framework-core @lumeweb/portal-framework-uiCreate the required configuration files following the templates above.
# Start development server
pnpm dev
# Build plugin
pnpm build
# Run linting
pnpm lintAdd the plugin to the app shell's plugin configuration and test integration.
- Port Conflicts: Ensure each plugin uses a unique development port
- Missing Exposes: All route components must be properly exposed in plugin.config.ts
- Capability Registration: Ensure capabilities are properly registered in the plugin factory
- Type Safety: Use proper TypeScript types for all framework interactions
- Bundle Size: Be mindful of bundle sizes, especially for large dependencies
Following these guidelines ensures consistency across all portal plugins and smooth integration with the portal framework. The modular architecture allows for independent development and deployment while maintaining a cohesive user experience.