Frontend Plugin Development
Airone Plugin Development Guide
This guide explains how to create, develop, and distribute plugins for the Airone frontend using the new simplified plugin architecture.
Airone uses an extremely simplified plugin system that focuses on the core principle: “just connecting routing with React Components”. This eliminates complex lifecycle management, API layers, and dependency systems in favor of a minimal, type-safe approach.
graph TB
subgraph "Developer Work"
A[plugins.config.js Plugin Configuration] --> B[npm run build]
B --> C[generate-plugin-imports.js Auto-executed]
end
subgraph "Build Process"
C --> D[src/generatedPlugins.ts Auto-generated]
D --> E[Webpack Bundle]
F[src/App.tsx] --> E
G[pagoda-core types] --> E
end
subgraph "Runtime"
E --> H[Distributable ui.js]
H --> I[Browser]
end
subgraph "Plugin System"
J[Plugin Interface] --> K[id, name, version, routes]
L[PluginRoute Interface] --> M[path and element]
N[extractRoutes Function] --> O[Plugin to CustomRoute]
end
subgraph "Individual Plugins"
P[pagoda-plugin-hello-world] --> P1[Hello World Route]
Q[pagoda-plugin-dashboard] --> Q1[Dashboard Route]
end
subgraph "Integration Flow"
R[AppBase Component] --> S[Receives plugins array]
S --> T[Executes extractRoutes]
T --> U[Merges with existing routes]
U --> V[Passes to AppRouter]
end
A -.-> P
A -.-> Q
P --> D
Q --> D
K --> R
J --> P
J --> Q
L --> K
N --> T
style A fill:#e1f5fe
style C fill:#f3e5f5
style D fill:#fff3e0
style J fill:#e8f5e8
style R fill:#fff8e1
- Extreme Simplification: Plugin interface contains only
id,name,version, androutes - No Lifecycle Management: Removed initialize, activate, deactivate hooks
- No Plugin API: Direct component implementation without API layers
- Configuration-Based: Managed through
plugins.config.jsonly - Type Safety: Enforced using TypeScript’s
satisfiesoperator
- Node.js 18+ and npm 8+
- TypeScript 5.0+
- Basic knowledge of React and TypeScript
- Understanding of npm package development and local linking
Create a new plugin directory:
mkdir pagoda-plugin-my-feature
cd pagoda-plugin-my-feature
npm init -y
Install peer dependencies:
npm install --save-peer @dmm-com/pagoda-core react react-dom @mui/material @mui/icons-material
npm install --save-dev @types/react @types/react-dom typescript
Configure TypeScript:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020", "DOM"],
"declaration": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
// Provided by @dmm-com/pagoda-core
export interface Plugin {
id: string; // Unique identifier
name: string; // Display name
version: string; // Semver version
routes: PluginRoute[]; // Route definitions
}
export interface PluginRoute {
path: string; // Route path (recommend /ui/ prefix)
element: ReactNode; // React component element
}
- Simple: Only path and element required
- Type Safe: Use
satisfies Pluginfor compile-time validation - No Complexity: No lifecycle hooks, priorities, or API dependencies
// src/index.ts
import React from "react";
import type { Plugin } from "@dmm-com/pagoda-core";
import MyPluginPage from "./components/MyPluginPage";
const myPlugin = {
id: "my-awesome-plugin",
name: "My Awesome Plugin",
version: "1.0.0",
routes: [
{
path: "/ui/my-plugin",
element: React.createElement(MyPluginPage),
},
],
} satisfies Plugin; // Type safety enforcement
export default myPlugin;
// src/components/MyPluginPage.tsx
import React, { useState } from "react";
import { Box, Typography, Card, CardContent, Button } from "@mui/material";
// Optional: Plugin API integration (passed as props if available)
export interface MyPluginPageProps {
pluginAPI?: {
ui?: { showNotification?: (message: string, type: string) => void };
routing?: { navigate?: (path: string) => void };
};
}
const MyPluginPage: React.FC<MyPluginPageProps> = ({ pluginAPI }) => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + 1);
// Optional: Use plugin API if available
if (pluginAPI?.ui?.showNotification) {
pluginAPI.ui.showNotification(`Clicked ${count + 1} times!`, "success");
}
};
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
My Awesome Plugin
</Typography>
<Card>
<CardContent>
<Typography variant="body1" paragraph>
This is a demonstration of the simplified plugin system.
</Typography>
<Button variant="contained" onClick={handleClick}>
Click Me ({count})
</Button>
</CardContent>
</Card>
</Box>
);
};
export default MyPluginPage;
Package Name: Use
pagoda-plugin-prefix- ✅
pagoda-plugin-reports - ✅
pagoda-plugin-user-management - ❌
my-pagoda-plugin
- ✅
Plugin ID: Use kebab-case
- ✅
reports-dashboard - ✅
user-management - ❌
ReportsDashboard
- ✅
Routes: Use descriptive paths with
/ui/prefix- ✅
/ui/reports/dashboard - ✅
/ui/user-management/settings - ❌
/reports(missing /ui/ prefix)
- ✅
Add to plugins.config.js:
// plugins.config.js export default { plugins: [ "pagoda-plugin-hello-world", "pagoda-plugin-dashboard", "pagoda-plugin-your-plugin", // Add here ], };Install locally for development:
# In pagoda-minimal-builder directory npm link ../path/to/your-plugin npm run build npm run startVerify integration:
- Check browser console for loading messages
- Navigate to your plugin’s route
- Verify component renders correctly
- Use React functional components with TypeScript
- Export component interfaces for type safety
- Follow Material-UI design patterns
- Implement responsive design
- Handle optional pluginAPI props gracefully
{
"name": "pagoda-plugin-my-feature",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist", "src"],
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"peerDependencies": {
"@dmm-com/pagoda-core": "^1.1.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"@mui/material": "^6.0.0",
"@mui/icons-material": "^6.0.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"typescript": "^5.0.0"
}
}
Support multiple pages within a single plugin:
const myPlugin = {
id: "my-multi-page-plugin",
name: "My Multi-Page Plugin",
version: "1.0.0",
routes: [
{
path: "/ui/my-plugin",
element: React.createElement(DashboardPage)
},
{
path: "/ui/my-plugin/settings",
element: React.createElement(SettingsPage)
},
{
path: "/ui/my-plugin/reports",
element: React.createElement(ReportsPage)
}
],
} satisfies Plugin;
Override existing application routes:
// Override the default dashboard
const dashboardPlugin = {
id: "enhanced-dashboard",
name: "Enhanced Dashboard",
version: "1.0.0",
routes: [
{
path: "/ui/dashboard", // Override existing dashboard
element: React.createElement(EnhancedDashboard)
}
],
} satisfies Plugin;
Use React Context for plugin-wide state:
import React, { createContext, useContext, useState } from 'react';
interface PluginState {
isLoaded: boolean;
data: any[];
}
const PluginContext = createContext<PluginState | null>(null);
export const PluginProvider: React.FC<{children: React.ReactNode}> = ({children}) => {
const [state, setState] = useState<PluginState>({
isLoaded: false,
data: []
});
return (
<PluginContext.Provider value={state}>
{children}
</PluginContext.Provider>
);
};
export const usePluginState = () => {
const context = useContext(PluginContext);
if (!context) throw new Error('usePluginState must be used within PluginProvider');
return context;
};
# Build TypeScript
npm run build
# Type checking only
npm run typecheck
# Clean build artifacts
npm run clean
Using npm link (recommended):
# In your plugin directory npm run build npm link # In pagoda-minimal-builder directory npm link pagoda-plugin-your-nameAdd to plugins.config.js:
export default { plugins: [ "pagoda-plugin-hello-world", "pagoda-plugin-your-name", // Your plugin ], };Test integration:
# In pagoda-minimal-builder directory npm run build npm run start
Verify your plugin works correctly:
- Check browser console for loading messages
- Navigate to plugin routes in browser
- Test component functionality
- Verify TypeScript compilation without errors
For local/internal use:
- Build your plugin with
npm run build - Use
npm linkfor development testing - Use
npm packto create distributable package
For public distribution:
# Ensure proper configuration
npm run build
# Version your plugin
npm version patch # or minor/major
# Publish (if public)
npm publish
Users can install your plugin in their pagoda-minimal-builder:
# Install the plugin
npm install pagoda-plugin-your-name
# Add to plugins.config.js
# Then run build
npm run build
- Check naming: Ensure package name starts with
pagoda-plugin- - Verify export: Plugin should be default export
- Check plugins.config.js: Ensure plugin is listed in configuration
- Verify build: Check
src/generatedPlugins.tswas auto-generated
- Install peer dependencies: Make sure all peer deps are available
- Check imports: Verify
import type { Plugin } from "@dmm-com/pagoda-core" - Use satisfies: Ensure you use
satisfies Pluginsyntax - Update types: Use compatible versions of dependencies
- Check console: Look for error messages in browser console
- Verify imports: Ensure all imported modules are available
- Test components: Test React components independently
- Check paths: Verify route paths are accessible
- Path format: Ensure paths start with
/ui/ - React.createElement: Verify proper component element creation
- Component export: Ensure components are properly exported
Add logging to your plugin:
const myPlugin = {
id: "my-plugin",
name: "My Plugin",
version: "1.0.0",
routes: [
{
path: "/ui/my-plugin",
element: React.createElement(() => {
console.log('[MyPlugin] Component rendering');
return React.createElement('div', {}, 'Hello from My Plugin!');
}),
},
],
} satisfies Plugin;
console.log('[MyPlugin] Plugin defined:', myPlugin);
export default myPlugin;
- Use TypeScript: Ensure type safety with
satisfies Plugin - Export interfaces: Export component prop interfaces for type safety
- Handle errors gracefully: Wrap components in error boundaries
- Consistent naming: Follow kebab-case for IDs and paths
- Documentation: Comment your plugin code clearly
- Lazy loading: Use dynamic imports for heavy components
- React.memo: Use memoization for expensive components
- Minimize bundle: Keep dependencies lean
- Optimize images: Use appropriate image formats and sizes
- Semantic HTML: Use proper HTML elements
- Keyboard navigation: Ensure all interactive elements are keyboard accessible
- Screen readers: Provide proper ARIA labels and descriptions
- Color contrast: Follow WCAG guidelines for color contrast
import React from "react";
import type { Plugin } from "@dmm-com/pagoda-core";
const minimalPlugin = {
id: "minimal-plugin",
name: "Minimal Plugin",
version: "1.0.0",
routes: [
{
path: "/ui/minimal",
element: React.createElement('div', { style: { padding: '20px' } },
React.createElement('h1', {}, 'Minimal Plugin'),
React.createElement('p', {}, 'This is the simplest possible plugin.')
)
}
],
} satisfies Plugin;
export default minimalPlugin;
import React from "react";
import type { Plugin } from "@dmm-com/pagoda-core";
import DashboardPage from "./components/DashboardPage";
import SettingsPage from "./components/SettingsPage";
import ReportsPage from "./components/ReportsPage";
const analyticsPlugin = {
id: "analytics-plugin",
name: "Analytics Plugin",
version: "1.2.0",
routes: [
{
path: "/ui/analytics",
element: React.createElement(DashboardPage)
},
{
path: "/ui/analytics/settings",
element: React.createElement(SettingsPage)
},
{
path: "/ui/analytics/reports",
element: React.createElement(ReportsPage)
}
],
} satisfies Plugin;
export default analyticsPlugin;
- ❌ ExternalPluginLoader: No automatic plugin discovery
- ❌ Plugin API: No centralized API access
- ❌ Lifecycle hooks: No initialize/activate/deactivate methods
- ❌ Priority system: No plugin loading priorities
- ❌ Dependency management: No plugin dependencies
- ❌ Configuration options: No complex plugin configuration
- ✅ Configuration-based: Managed through
plugins.config.js - ✅ Type safety: Enforced with
satisfies Plugin - ✅ Direct integration: Components directly rendered in routes
- ✅ Simplicity: Minimal interface with maximum flexibility
- ✅ Build-time resolution: Static import generation at build time
- Documentation: Refer to this guide and existing plugin examples
- Code inspection: Study
pagoda-plugin-hello-worldandpagoda-plugin-dashboard - Console debugging: Use browser developer tools for troubleshooting
- Community: Engage with the development community for support
- Plugin examples: Share your plugin implementations
- Documentation improvements: Help enhance this guide
- Best practices: Contribute development patterns and practices
- Core system: Suggest improvements to the plugin architecture
The pagoda-minimal-builder provides a complete integration environment for testing and deploying plugins.
Navigate to pagoda-minimal-builder:
cd /path/to/airone/frontend/plugins/pagoda-minimal-builderInstall dependencies:
npm installStart development server:
npm run startAccess in browser:
- Main app: http://localhost:3000
- Hello World Plugin: http://localhost:3000/ui/hello-world
- Dashboard Plugin: http://localhost:3000/ui/dashboard
pagoda-minimal-builder/
├── src/
│ ├── App.tsx # Main application entry
│ └── generatedPlugins.ts # Auto-generated plugin imports
├── scripts/
│ └── generate-plugin-imports.js # Import generator
├── plugins.config.js # Plugin configuration
├── webpack.config.js # Build configuration
├── package.json # Dependencies and scripts
└── README.md # Usage documentation
- Configuration: Edit
plugins.config.jsto specify plugins - Auto-generation:
npm run buildtriggersgenerate-plugin-imports.js - Static imports:
src/generatedPlugins.tsis created with import statements - Bundle creation: Webpack creates
dist/ui.jswith all plugins included - No source editing: Add/remove plugins by configuration only
- Create your plugin following the guidelines above
- Link locally: Use
npm linkfor development testing - Configure: Add plugin name to
plugins.config.js - Build & test: Run
npm run build && npm run start - Debug: Check console logs and browser developer tools
- Deploy: Use generated
dist/ui.jsfor production
This simplified plugin system prioritizes ease of use and maintainability. The focus is on connecting React components to routes with minimal complexity, allowing developers to build powerful extensions without dealing with complex plugin lifecycles or APIs.