Frontend Plugin System
DeployStack's frontend features a powerful plugin architecture that enables extending the application with additional functionality, UI components, routes, and state management. This modular approach allows for clean separation of concerns and extensible development.
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:
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 the Plugin
interface:
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:
mkdir -p src/plugins/my-custom-plugin
cd src/plugins/my-custom-plugin
2. Create a Component
Start with a simple Vue component:
<!-- 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:
// 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:
<!-- 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:
// 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:
<!-- 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:
// 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:
<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:
// 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
// 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
<!-- 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
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
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
// 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:
// 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
// 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
// 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
// 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
// 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
// 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
}
}
This plugin system documentation provides everything needed to create maintainable and well-tested plugins for the DeployStack frontend. The modular architecture ensures that functionality can be extended cleanly while maintaining the core application's stability and performance.