Architecture Overview
The plugin system is designed with flexibility and maintainability in mind:- Modular Extension: Add new UI components at designated extension points
- Route Registration: Register new routes in the Vue Router
- State Management: Add new Pinia stores for plugin-specific state
- Lifecycle Management: Initialize and cleanup plugins properly
- Type Safety: Full TypeScript support for plugin development
Plugin Structure
A standard plugin follows this directory structure:Copy
your-plugin/
├── index.ts # Main plugin entry point (required)
├── components/ # Plugin-specific components
│ ├── PluginComponent.vue
│ └── PluginCard.vue
├── views/ # Plugin-specific views/pages
│ ├── PluginPage.vue
│ └── PluginSettings.vue
├── store.ts # Plugin-specific Pinia store (optional)
├── composables/ # Plugin-specific composables (optional)
│ └── usePluginFeature.ts
├── types.ts # Plugin-specific types (optional)
└── README.md # Plugin documentation
Plugin Interface
Every plugin must implement thePlugin interface:
Copy
interface Plugin {
meta: PluginMeta
initialize(app: App, router: Router, pinia: Pinia): Promise<void> | void
cleanup?(): Promise<void> | void
}
interface PluginMeta {
id: string // Unique plugin identifier
name: string // Human-readable plugin name
version: string // Plugin version (semver)
description: string // Plugin description
author?: string // Plugin author (optional)
}
Creating Your First Plugin
1. Basic Plugin Structure
Create a new directory for your plugin:Copy
mkdir -p src/plugins/my-custom-plugin
cd src/plugins/my-custom-plugin
2. Create a Component
Start with a simple Vue component:Copy
<!-- src/plugins/my-custom-plugin/components/CustomWidget.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { BarChart, TrendingUp, Server } from 'lucide-vue-next'
const data = ref({
totalItems: 0,
activeItems: 0,
totalRequests: 0
})
const isLoading = ref(true)
async function fetchData() {
isLoading.value = true
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000))
data.value = {
totalItems: 12,
activeItems: 8,
totalRequests: 1542
}
} catch (error) {
console.error('Failed to fetch data:', error)
} finally {
isLoading.value = false
}
}
onMounted(() => {
fetchData()
})
</script>
<template>
<div class="custom-widget p-6 bg-white rounded-lg shadow-md border">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 flex items-center">
<BarChart class="h-5 w-5 mr-2 text-indigo-600" />
Custom Plugin Widget
</h3>
<button
@click="fetchData"
class="text-sm text-indigo-600 hover:text-indigo-800"
:disabled="isLoading"
>
{{ isLoading ? 'Loading...' : 'Refresh' }}
</button>
</div>
<div v-if="isLoading" class="animate-pulse">
<div class="grid grid-cols-3 gap-4">
<div v-for="i in 3" :key="i" class="h-16 bg-gray-200 rounded"></div>
</div>
</div>
<div v-else class="grid grid-cols-3 gap-4">
<div class="text-center p-4 bg-blue-50 rounded-lg">
<Server class="h-6 w-6 mx-auto mb-2 text-blue-600" />
<div class="text-2xl font-bold text-blue-700">{{ data.totalItems }}</div>
<div class="text-sm text-blue-600">Total Items</div>
</div>
<div class="text-center p-4 bg-green-50 rounded-lg">
<TrendingUp class="h-6 w-6 mx-auto mb-2 text-green-600" />
<div class="text-2xl font-bold text-green-700">{{ data.activeItems }}</div>
<div class="text-sm text-green-600">Active Items</div>
</div>
<div class="text-center p-4 bg-purple-50 rounded-lg">
<BarChart class="h-6 w-6 mx-auto mb-2 text-purple-600" />
<div class="text-2xl font-bold text-purple-700">{{ data.totalRequests.toLocaleString() }}</div>
<div class="text-sm text-purple-600">Total Requests</div>
</div>
</div>
</div>
</template>
3. Implement the Plugin
Create the main plugin file:Copy
// src/plugins/my-custom-plugin/index.ts
import type { Plugin } from '@/plugin-system/types'
import type { App } from 'vue'
import type { Router } from 'vue-router'
import type { Pinia } from 'pinia'
import { registerExtensionPoint } from '@/plugin-system/extension-points'
import CustomWidget from './components/CustomWidget.vue'
import CustomPage from './views/CustomPage.vue'
class MyCustomPlugin implements Plugin {
meta = {
id: 'my-custom-plugin',
name: 'My Custom Plugin',
version: '1.0.0',
description: 'Provides custom functionality and widgets for the application',
author: 'Your Name'
}
initialize(app: App, router: Router, pinia: Pinia) {
console.log('Initializing My Custom Plugin...')
// Register the widget at an extension point
registerExtensionPoint('dashboard-widgets', CustomWidget, this.meta.id, {
order: 10, // Show early in the dashboard
props: {
refreshInterval: 30000 // Refresh every 30 seconds
}
})
// Register a dedicated page route
router.addRoute({
path: '/custom',
name: 'Custom',
component: CustomPage,
meta: {
title: 'Custom Plugin',
requiresAuth: true
}
})
console.log('My Custom Plugin initialized successfully')
}
cleanup() {
console.log('Cleaning up My Custom Plugin...')
// Extension points are automatically cleaned up by the plugin manager
}
}
export default MyCustomPlugin
4. Create a Dedicated Page
Create a full page view for your plugin:Copy
<!-- src/plugins/my-custom-plugin/views/CustomPage.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { BarChart, Settings } from 'lucide-vue-next'
import CustomWidget from '../components/CustomWidget.vue'
const pageData = ref({
title: 'Custom Plugin Dashboard',
description: 'This page is provided by the custom plugin'
})
</script>
<template>
<div class="custom-page max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 flex items-center">
<BarChart class="h-8 w-8 mr-3 text-indigo-600" />
{{ pageData.title }}
</h1>
<p class="mt-2 text-gray-600">
{{ pageData.description }}
</p>
</div>
<!-- Include the widget component -->
<div class="mb-8">
<CustomWidget />
</div>
<!-- Additional page content -->
<div class="bg-white p-6 rounded-lg shadow-md border">
<h2 class="text-xl font-semibold mb-4 flex items-center">
<Settings class="h-6 w-6 mr-2 text-gray-600" />
Plugin Settings
</h2>
<p class="text-gray-600">
This is where you could add plugin-specific settings and configuration options.
</p>
</div>
</div>
</template>
5. Add Plugin State Management
Create a Pinia store for your plugin:Copy
// src/plugins/my-custom-plugin/store.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface CustomData {
totalItems: number
activeItems: number
totalRequests: number
}
export const useCustomStore = defineStore('my-custom-plugin', () => {
// State
const data = ref<CustomData>({
totalItems: 0,
activeItems: 0,
totalRequests: 0
})
const isLoading = ref(false)
const lastUpdated = ref<Date | null>(null)
// Getters
const activePercentage = computed(() => {
if (data.value.totalItems === 0) return 0
return Math.round((data.value.activeItems / data.value.totalItems) * 100)
})
const isHealthy = computed(() => activePercentage.value > 80)
// Actions
async function fetchData() {
isLoading.value = true
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000))
data.value = {
totalItems: Math.floor(Math.random() * 20) + 10,
activeItems: Math.floor(Math.random() * 15) + 5,
totalRequests: Math.floor(Math.random() * 5000) + 1000
}
lastUpdated.value = new Date()
} catch (error) {
console.error('Failed to fetch data:', error)
throw error
} finally {
isLoading.value = false
}
}
return {
// State
data,
isLoading,
lastUpdated,
// Getters
activePercentage,
isHealthy,
// Actions
fetchData
}
})
Extension Points
Extension points are designated areas in your application where plugins can inject components.Using Extension Points in Your App
Add extension points to your main application components:Copy
<!-- In your Dashboard.vue or other main components -->
<template>
<div class="dashboard">
<h1>Dashboard</h1>
<!-- Extension point for dashboard widgets -->
<div class="widgets-grid">
<ExtensionPoint pointId="dashboard-widgets" />
</div>
<!-- Extension point for sidebar items -->
<aside class="sidebar">
<ExtensionPoint pointId="sidebar-items" />
</aside>
<!-- Extension point for action buttons -->
<div class="actions">
<ExtensionPoint pointId="action-buttons" />
</div>
</div>
</template>
Registering Components at Extension Points
In your plugin’s initialize method:Copy
// Register a single component
registerExtensionPoint(
'dashboard-widgets', // Extension point ID
CustomWidget, // Vue component
this.meta.id, // Plugin ID
{
order: 10, // Display order (optional)
props: { // Props to pass to component (optional)
refreshInterval: 30000
}
}
)
// Register multiple components
registerExtensionPoint('action-buttons', RefreshButton, this.meta.id, { order: 1 })
registerExtensionPoint('action-buttons', ExportButton, this.meta.id, { order: 2 })
Conditional Rendering
Show specific plugin components based on conditions:Copy
<template>
<!-- Show only specific plugin -->
<ExtensionPoint pointId="dashboard-widgets" pluginName="my-custom-plugin" />
<!-- Show all plugins at this extension point -->
<ExtensionPoint pointId="dashboard-widgets" />
</template>
Registering Plugins
Add your plugin to the plugin loader:Copy
// src/plugins/index.ts
import type { Plugin } from '../plugin-system/types'
import HelloWorldPlugin from './hello-world'
import MyCustomPlugin from './my-custom-plugin'
export async function loadPlugins(): Promise<Plugin[]> {
return [
new HelloWorldPlugin(),
new MyCustomPlugin(),
// Add more plugins here
]
}
Plugin Development Best Practices
1. Plugin Naming and Structure
Copy
// Good plugin naming
class ServerMonitoringPlugin implements Plugin {
meta = {
id: 'server-monitoring', // kebab-case
name: 'Server Monitoring', // Human readable
version: '1.2.3', // Semantic versioning
description: 'Real-time server monitoring', // Clear description
author: 'Your Team'
}
}
// Avoid generic names
// ❌ class UtilsPlugin
// ❌ class MyPlugin
// ❌ class Plugin1
2. Component Naming Convention
Copy
<!-- Good: Prefix with plugin name -->
<!-- MonitoringWidget.vue -->
<!-- MonitoringDashboard.vue -->
<!-- MonitoringChart.vue -->
<!-- Avoid generic names that might conflict -->
<!-- ❌ Widget.vue -->
<!-- ❌ Dashboard.vue -->
<!-- ❌ Chart.vue -->
3. Error Handling
Copy
class RobustPlugin implements Plugin {
initialize(app: App, router: Router, pinia: Pinia) {
try {
// Plugin initialization logic
this.setupComponents()
this.registerRoutes(router)
this.initializeStore(pinia)
console.log(`${this.meta.name} initialized successfully`)
} catch (error) {
console.error(`Failed to initialize ${this.meta.name}:`, error)
// Graceful degradation - don't throw unless critical
this.handleInitializationError(error)
}
}
private handleInitializationError(error: Error) {
// Log detailed error information
console.error('Plugin initialization error details:', {
pluginId: this.meta.id,
version: this.meta.version,
error: error.message,
stack: error.stack
})
}
}
4. Resource Cleanup
Copy
class CleanPlugin implements Plugin {
private intervals: number[] = []
private eventListeners: Array<{ element: EventTarget, type: string, listener: EventListener }> = []
initialize(app: App, router: Router, pinia: Pinia) {
// Set up intervals
const intervalId = setInterval(() => {
this.refreshData()
}, 30000)
this.intervals.push(intervalId)
// Set up event listeners
const listener = (event: Event) => this.handleEvent(event)
document.addEventListener('visibilitychange', listener)
this.eventListeners.push({
element: document,
type: 'visibilitychange',
listener
})
}
cleanup() {
// Clean up intervals
this.intervals.forEach(id => clearInterval(id))
this.intervals = []
// Clean up event listeners
this.eventListeners.forEach(({ element, type, listener }) => {
element.removeEventListener(type, listener)
})
this.eventListeners = []
console.log(`${this.meta.name} cleaned up successfully`)
}
}
5. Type Safety
Copy
// Define clear interfaces for your plugin data
interface PluginData {
totalItems: number
activeItems: number
status: 'active' | 'inactive' | 'error'
}
interface PluginConfig {
refreshInterval?: number
enableNotifications?: boolean
apiEndpoint?: string
}
// Use proper typing in components
interface Props {
data: PluginData
config?: PluginConfig
onRefresh?: () => void
}
const props = withDefaults(defineProps<Props>(), {
config: () => ({}),
onRefresh: () => {}
})
Plugin Composables
Create reusable composition functions:Copy
// src/plugins/my-custom-plugin/composables/useCustomFeature.ts
import { ref, onMounted, onUnmounted } from 'vue'
import { useCustomStore } from '../store'
export function useCustomFeature(refreshInterval = 30000) {
const store = useCustomStore()
const isAutoRefreshEnabled = ref(true)
let intervalId: number | null = null
function startAutoRefresh() {
if (intervalId) clearInterval(intervalId)
intervalId = setInterval(() => {
if (isAutoRefreshEnabled.value) {
store.fetchData()
}
}, refreshInterval)
}
function stopAutoRefresh() {
if (intervalId) {
clearInterval(intervalId)
intervalId = null
}
}
function toggleAutoRefresh() {
isAutoRefreshEnabled.value = !isAutoRefreshEnabled.value
if (isAutoRefreshEnabled.value) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
}
onMounted(() => {
store.fetchData()
startAutoRefresh()
})
onUnmounted(() => {
stopAutoRefresh()
})
return {
data: store.data,
isLoading: store.isLoading,
activePercentage: store.activePercentage,
isHealthy: store.isHealthy,
isAutoRefreshEnabled,
refreshData: store.fetchData,
toggleAutoRefresh
}
}
Testing Plugins
Unit Testing Plugin Components
Copy
// tests/plugins/my-custom-plugin/CustomWidget.test.ts
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import CustomWidget from '@/plugins/my-custom-plugin/components/CustomWidget.vue'
describe('CustomWidget', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should render widget correctly', async () => {
const wrapper = mount(CustomWidget)
// Wait for component to load
await wrapper.vm.$nextTick()
expect(wrapper.find('.custom-widget').exists()).toBe(true)
expect(wrapper.text()).toContain('Custom Plugin Widget')
})
it('should show loading state initially', () => {
const wrapper = mount(CustomWidget)
expect(wrapper.find('.animate-pulse').exists()).toBe(true)
})
it('should handle refresh button click', async () => {
const wrapper = mount(CustomWidget)
const refreshButton = wrapper.find('button')
await refreshButton.trigger('click')
expect(wrapper.vm.isLoading).toBe(true)
})
})
Integration Testing
Copy
// tests/plugins/my-custom-plugin/integration.test.ts
import { mount } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import { createPinia } from 'pinia'
import MyCustomPlugin from '@/plugins/my-custom-plugin'
import { PluginManager } from '@/plugin-system/plugin-manager'
describe('My Custom Plugin Integration', () => {
let pluginManager: PluginManager
let router: any
let pinia: any
beforeEach(() => {
router = createRouter({
history: createWebHistory(),
routes: []
})
pinia = createPinia()
pluginManager = new PluginManager()
})
it('should initialize plugin successfully', async () => {
const plugin = new MyCustomPlugin()
await expect(
plugin.initialize(null as any, router, pinia)
).resolves.not.toThrow()
// Check if routes were added
const routes = router.getRoutes()
expect(routes.some((route: any) => route.name === 'Custom')).toBe(true)
})
})
Troubleshooting
Common Issues
Plugin Not Loading
Copy
// Debug plugin loading
export async function loadPlugins(): Promise<Plugin[]> {
const plugins: Plugin[] = []
try {
const HelloWorldPlugin = (await import('./hello-world')).default
plugins.push(new HelloWorldPlugin())
console.log('✅ HelloWorldPlugin loaded')
} catch (error) {
console.error('❌ Failed to load HelloWorldPlugin:', error)
}
try {
const CustomPlugin = (await import('./my-custom-plugin')).default
plugins.push(new CustomPlugin())
console.log('✅ CustomPlugin loaded')
} catch (error) {
console.error('❌ Failed to load CustomPlugin:', error)
}
return plugins
}
Extension Points Not Rendering
- Check Extension Point Placement: Ensure
<ExtensionPoint pointId="your-point-id" />is placed in your application views - Verify Point ID: Make sure the extension point ID matches between registration and usage
- Component Import: Check that components are correctly imported and registered
- Console Errors: Look for JavaScript errors that might prevent component rendering
Plugin State Issues
Copy
// Debug plugin state
class DebuggablePlugin implements Plugin {
initialize(app: App, router: Router, pinia: Pinia) {
// Add global debug method
app.config.globalProperties.$debugPlugin = (pluginId: string) => {
console.log('Plugin Debug Info:', {
id: this.meta.id,
routes: router.getRoutes().filter(r => r.meta?.pluginId === pluginId),
stores: pinia._s,
extensionPoints: this.getRegisteredExtensionPoints()
})
}
}
private getRegisteredExtensionPoints() {
// Return extension points registered by this plugin
return []
}
}
Performance Debugging
Copy
// Performance monitoring for plugins
class PerformanceMonitoredPlugin implements Plugin {
initialize(app: App, router: Router, pinia: Pinia) {
const startTime = performance.now()
try {
this.doInitialization()
const endTime = performance.now()
console.log(`${this.meta.id} initialization took ${endTime - startTime}ms`)
} catch (error) {
const endTime = performance.now()
console.error(`${this.meta.id} failed after ${endTime - startTime}ms:`, error)
throw error
}
}
private doInitialization() {
// Your plugin initialization logic
}
}

