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, pluginManager?: PluginManager): Promise<void>
cleanup(): Promise<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
dependencies?: string[] // Other plugin IDs this plugin depends on
}
Creating Your First Plugin
1. Basic Plugin Structure
Create a new directory for your plugin:
mkdir -p src/plugins/mcp-metrics-plugin
cd src/plugins/mcp-metrics-plugin
2. Create a Component
Start with a simple Vue component:
<!-- src/plugins/mcp-metrics-plugin/components/MetricsWidget.vue -->
<script setup lang=\"ts\">
import { ref, onMounted } from 'vue'
import { BarChart, TrendingUp, Server } from 'lucide-vue-next'
const metrics = ref({
totalServers: 0,
activeDeployments: 0,
totalRequests: 0
})
const isLoading = ref(true)
async function fetchMetrics() {
isLoading.value = true
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000))
metrics.value = {
totalServers: 12,
activeDeployments: 8,
totalRequests: 1542
}
} catch (error) {
console.error('Failed to fetch metrics:', error)
} finally {
isLoading.value = false
}
}
onMounted(() => {
fetchMetrics()
})
</script>
<template>
<div class=\"metrics-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\" />
MCP Server Metrics
</h3>
<button
@click=\"fetchMetrics\"
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\">{{ metrics.totalServers }}</div>
<div class=\"text-sm text-blue-600\">Total Servers</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\">{{ metrics.activeDeployments }}</div>
<div class=\"text-sm text-green-600\">Active Deployments</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\">{{ metrics.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/mcp-metrics-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 MetricsWidget from './components/MetricsWidget.vue'
import MetricsPage from './views/MetricsPage.vue'
class McpMetricsPlugin implements Plugin {
meta = {
id: 'mcp-metrics-plugin',
name: 'MCP Metrics Plugin',
version: '1.0.0',
description: 'Provides comprehensive metrics and analytics for MCP server deployments',
author: 'DeployStack Team'
}
async initialize(app: App, router: Router, pinia: Pinia) {
console.log('Initializing MCP Metrics Plugin...')
// Register the metrics widget in the dashboard
registerExtensionPoint('dashboard-widgets', MetricsWidget, this.meta.id, {
order: 10, // Show early in the dashboard
props: {
refreshInterval: 30000 // Refresh every 30 seconds
}
})
// Register a dedicated metrics page
router.addRoute({
path: '/metrics',
name: 'Metrics',
component: MetricsPage,
meta: {
title: 'MCP Metrics',
requiresAuth: true
}
})
// Add navigation item (if supported by your app)
registerExtensionPoint('main-navigation', {
template: `
<router-link
to=\"/metrics\"
class=\"nav-link flex items-center space-x-2 px-3 py-2 rounded-md hover:bg-gray-100\"
>
<BarChart class=\"h-5 w-5\" />
<span>Metrics</span>
</router-link>
`,
components: { BarChart: () => import('lucide-vue-next').then(m => m.BarChart) }
}, this.meta.id)
console.log('MCP Metrics Plugin initialized successfully')
}
async cleanup() {
console.log('Cleaning up MCP Metrics Plugin...')
// Perform any necessary cleanup
// Extension points are automatically cleaned up by the plugin manager
}
}
export default McpMetricsPlugin
4. Create a Dedicated Page
Create a full page view for your plugin:
<!-- src/plugins/mcp-metrics-plugin/views/MetricsPage.vue -->
<script setup lang=\"ts\">
import { ref, onMounted } from 'vue'
import { BarChart, TrendingUp, Activity, Clock } from 'lucide-vue-next'
import MetricsWidget from '../components/MetricsWidget.vue'
const detailedMetrics = ref({
responseTime: '125ms',
uptime: '99.9%',
errorRate: '0.1%',
throughput: '1.2k req/min'
})
const chartData = ref([])
const isLoading = ref(true)
async function fetchDetailedMetrics() {
isLoading.value = true
try {
// Simulate API call for detailed metrics
await new Promise(resolve => setTimeout(resolve, 1500))
// Mock chart data
chartData.value = Array.from({ length: 24 }, (_, i) => ({
hour: i,
requests: Math.floor(Math.random() * 100) + 50,
errors: Math.floor(Math.random() * 5)
}))
} catch (error) {
console.error('Failed to fetch detailed metrics:', error)
} finally {
isLoading.value = false
}
}
onMounted(() => {
fetchDetailedMetrics()
})
</script>
<template>
<div class=\"metrics-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\" />
MCP Server Metrics
</h1>
<p class=\"mt-2 text-gray-600\">
Comprehensive analytics and performance metrics for your MCP server deployments
</p>
</div>
<!-- Overview Widget -->
<div class=\"mb-8\">
<MetricsWidget />
</div>
<!-- Detailed Metrics -->
<div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8\">
<div class=\"bg-white p-6 rounded-lg shadow-md border\">
<div class=\"flex items-center\">
<Clock class=\"h-8 w-8 text-blue-600\" />
<div class=\"ml-4\">
<div class=\"text-2xl font-bold text-gray-900\">{{ detailedMetrics.responseTime }}</div>
<div class=\"text-sm text-gray-600\">Avg Response Time</div>
</div>
</div>
</div>
<div class=\"bg-white p-6 rounded-lg shadow-md border\">
<div class=\"flex items-center\">
<TrendingUp class=\"h-8 w-8 text-green-600\" />
<div class=\"ml-4\">
<div class=\"text-2xl font-bold text-gray-900\">{{ detailedMetrics.uptime }}</div>
<div class=\"text-sm text-gray-600\">Uptime</div>
</div>
</div>
</div>
<div class=\"bg-white p-6 rounded-lg shadow-md border\">
<div class=\"flex items-center\">
<Activity class=\"h-8 w-8 text-red-600\" />
<div class=\"ml-4\">
<div class=\"text-2xl font-bold text-gray-900\">{{ detailedMetrics.errorRate }}</div>
<div class=\"text-sm text-gray-600\">Error Rate</div>
</div>
</div>
</div>
<div class=\"bg-white p-6 rounded-lg shadow-md border\">
<div class=\"flex items-center\">
<BarChart class=\"h-8 w-8 text-purple-600\" />
<div class=\"ml-4\">
<div class=\"text-2xl font-bold text-gray-900\">{{ detailedMetrics.throughput }}</div>
<div class=\"text-sm text-gray-600\">Throughput</div>
</div>
</div>
</div>
</div>
<!-- Charts Section -->
<div class=\"bg-white p-6 rounded-lg shadow-md border\">
<h2 class=\"text-xl font-semibold mb-4\">24-Hour Request Pattern</h2>
<div v-if=\"isLoading\" class=\"animate-pulse\">
<div class=\"h-64 bg-gray-200 rounded\"></div>
</div>
<div v-else class=\"h-64\">
<!-- Simple chart representation -->
<div class=\"flex items-end justify-between h-full space-x-1\">
<div
v-for=\"(data, index) in chartData\"
:key=\"index\"
class=\"bg-indigo-500 min-w-[1rem] rounded-t transition-all hover:bg-indigo-600\"
:style=\"{ height: `${(data.requests / 150) * 100}%` }\"
:title=\"`Hour ${data.hour}: ${data.requests} requests`\"
></div>
</div>
</div>
</div>
</div>
</template>
5. Add Plugin State Management
Create a Pinia store for your plugin:
// src/plugins/mcp-metrics-plugin/store.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface MetricsData {
totalServers: number
activeDeployments: number
totalRequests: number
responseTime: string
uptime: string
errorRate: string
throughput: string
}
export interface ChartDataPoint {
hour: number
requests: number
errors: number
}
export const useMetricsStore = defineStore('mcp-metrics', () => {
// State
const metrics = ref<MetricsData>({
totalServers: 0,
activeDeployments: 0,
totalRequests: 0,
responseTime: '0ms',
uptime: '0%',
errorRate: '0%',
throughput: '0 req/min'
})
const chartData = ref<ChartDataPoint[]>([])
const isLoading = ref(false)
const lastUpdated = ref<Date | null>(null)
// Getters
const healthScore = computed(() => {
const uptimePercent = parseFloat(metrics.value.uptime.replace('%', ''))
const errorPercent = parseFloat(metrics.value.errorRate.replace('%', ''))
return Math.max(0, 100 - errorPercent * 10) * (uptimePercent / 100)
})
const isHealthy = computed(() => healthScore.value > 80)
// Actions
async function fetchMetrics() {
isLoading.value = true
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000))
metrics.value = {
totalServers: Math.floor(Math.random() * 20) + 10,
activeDeployments: Math.floor(Math.random() * 15) + 5,
totalRequests: Math.floor(Math.random() * 5000) + 1000,
responseTime: `${Math.floor(Math.random() * 200) + 50}ms`,
uptime: `${(99 + Math.random()).toFixed(1)}%`,
errorRate: `${(Math.random() * 2).toFixed(1)}%`,
throughput: `${(Math.random() * 2 + 0.5).toFixed(1)}k req/min`
}
lastUpdated.value = new Date()
} catch (error) {
console.error('Failed to fetch metrics:', error)
throw error
} finally {
isLoading.value = false
}
}
async function fetchChartData() {
try {
// Simulate API call for chart data
await new Promise(resolve => setTimeout(resolve, 800))
chartData.value = Array.from({ length: 24 }, (_, i) => ({
hour: i,
requests: Math.floor(Math.random() * 100) + 50,
errors: Math.floor(Math.random() * 5)
}))
} catch (error) {
console.error('Failed to fetch chart data:', error)
throw error
}
}
function refreshAllData() {
return Promise.all([
fetchMetrics(),
fetchChartData()
])
}
return {
// State
metrics,
chartData,
isLoading,
lastUpdated,
// Getters
healthScore,
isHealthy,
// Actions
fetchMetrics,
fetchChartData,
refreshAllData
}
})
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
MetricsWidget, // 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=\"mcp-metrics-plugin\" />
<!-- Show all except specific plugin -->
<ExtensionPoint pointId=\"dashboard-widgets\" :exclude=\"['debug-plugin']\" />
<!-- Show based on user permissions -->
<ExtensionPoint
pointId=\"admin-actions\"
v-if=\"userHasAdminRole\"
/>
</template>
Advanced Plugin Features
Plugin Dependencies
Specify dependencies in your plugin metadata:
class AdvancedPlugin implements Plugin {
meta = {
id: 'advanced-plugin',
name: 'Advanced Plugin',
version: '1.0.0',
description: 'Advanced functionality that requires metrics plugin',
author: 'You',
dependencies: ['mcp-metrics-plugin'] // Require metrics plugin
}
async initialize(app: App, router: Router, pinia: Pinia, pluginManager?: PluginManager) {
// Check if required plugins are available
const metricsPlugin = pluginManager?.getPlugin('mcp-metrics-plugin')
if (!metricsPlugin) {
throw new Error('MCP Metrics Plugin is required but not available')
}
// Use functionality from the metrics plugin
console.log('Metrics plugin available:', metricsPlugin.meta.name)
}
}
Plugin Configuration
Support configuration through the plugin manager:
interface PluginConfig {
refreshInterval?: number
enableNotifications?: boolean
theme?: 'light' | 'dark'
}
class ConfigurablePlugin implements Plugin {
private config: PluginConfig = {}
async initialize(app: App, router: Router, pinia: Pinia, pluginManager?: PluginManager) {
// Get plugin configuration
this.config = pluginManager?.getPluginConfig(this.meta.id) || {}
// Use configuration
const refreshInterval = this.config.refreshInterval || 30000
const enableNotifications = this.config.enableNotifications !== false
console.log('Plugin config:', this.config)
}
}
Inter-Plugin Communication
Plugins can communicate through events or shared stores:
// Event-based communication
class PublisherPlugin implements Plugin {
async initialize(app: App) {
// Emit events
app.config.globalProperties.$pluginEventBus.emit('metrics-updated', data)
}
}
class SubscriberPlugin implements Plugin {
async initialize(app: App) {
// Listen to events
app.config.globalProperties.$pluginEventBus.on('metrics-updated', (data) => {
console.log('Received metrics update:', data)
})
}
}
Plugin Composables
Create reusable composition functions:
// src/plugins/mcp-metrics-plugin/composables/useMetrics.ts
import { ref, onMounted, onUnmounted } from 'vue'
import { useMetricsStore } from '../store'
export function useMetrics(refreshInterval = 30000) {
const store = useMetricsStore()
const isAutoRefreshEnabled = ref(true)
let intervalId: number | null = null
function startAutoRefresh() {
if (intervalId) clearInterval(intervalId)
intervalId = setInterval(() => {
if (isAutoRefreshEnabled.value) {
store.fetchMetrics()
}
}, refreshInterval)
}
function stopAutoRefresh() {
if (intervalId) {
clearInterval(intervalId)
intervalId = null
}
}
function toggleAutoRefresh() {
isAutoRefreshEnabled.value = !isAutoRefreshEnabled.value
if (isAutoRefreshEnabled.value) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
}
onMounted(() => {
store.fetchMetrics()
startAutoRefresh()
})
onUnmounted(() => {
stopAutoRefresh()
})
return {
metrics: store.metrics,
isLoading: store.isLoading,
healthScore: store.healthScore,
isHealthy: store.isHealthy,
isAutoRefreshEnabled,
refreshMetrics: store.fetchMetrics,
toggleAutoRefresh
}
}
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 McpMetricsPlugin from './mcp-metrics-plugin'
import AdvancedPlugin from './advanced-plugin'
export async function loadPlugins(): Promise<Plugin[]> {
return [
new HelloWorldPlugin(),
new McpMetricsPlugin(),
new AdvancedPlugin(),
// Add more plugins here
]
}
Testing Plugins
Unit Testing Plugin Components
// tests/plugins/mcp-metrics-plugin/MetricsWidget.test.ts
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import MetricsWidget from '@/plugins/mcp-metrics-plugin/components/MetricsWidget.vue'
describe('MetricsWidget', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should render metrics correctly', async () => {
const wrapper = mount(MetricsWidget)
// Wait for component to load
await wrapper.vm.$nextTick()
expect(wrapper.find('.metrics-widget').exists()).toBe(true)
expect(wrapper.text()).toContain('MCP Server Metrics')
})
it('should show loading state initially', () => {
const wrapper = mount(MetricsWidget)
expect(wrapper.find('.animate-pulse').exists()).toBe(true)
})
it('should handle refresh button click', async () => {
const wrapper = mount(MetricsWidget)
const refreshButton = wrapper.find('button')
await refreshButton.trigger('click')
expect(wrapper.vm.isLoading).toBe(true)
})
})
Integration Testing
// tests/plugins/mcp-metrics-plugin/integration.test.ts
import { mount } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import { createPinia } from 'pinia'
import McpMetricsPlugin from '@/plugins/mcp-metrics-plugin'
import { PluginManager } from '@/plugin-system/PluginManager'
describe('MCP Metrics 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 McpMetricsPlugin()
await expect(
plugin.initialize(null as any, router, pinia, pluginManager)
).resolves.not.toThrow()
// Check if routes were added
const routes = router.getRoutes()
expect(routes.some((route: any) => route.name === 'Metrics')).toBe(true)
})
it('should register extension points', async () => {
const plugin = new McpMetricsPlugin()
await plugin.initialize(null as any, router, pinia, pluginManager)
// Verify extension points were registered
const dashboardExtensions = pluginManager.getExtensionPoints('dashboard-widgets')
expect(dashboardExtensions).toHaveLength(1)
expect(dashboardExtensions[0].pluginId).toBe(plugin.meta.id)
})
})
Plugin Store Testing
// tests/plugins/mcp-metrics-plugin/store.test.ts
import { createPinia, setActivePinia } from 'pinia'
import { useMetricsStore } from '@/plugins/mcp-metrics-plugin/store'
describe('Metrics Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should initialize with default values', () => {
const store = useMetricsStore()
expect(store.metrics.totalServers).toBe(0)
expect(store.metrics.activeDeployments).toBe(0)
expect(store.isLoading).toBe(false)
expect(store.lastUpdated).toBeNull()
})
it('should update metrics after fetching', async () => {
const store = useMetricsStore()
await store.fetchMetrics()
expect(store.metrics.totalServers).toBeGreaterThan(0)
expect(store.lastUpdated).toBeInstanceOf(Date)
})
it('should calculate health score correctly', async () => {
const store = useMetricsStore()
// Set known values for testing
store.metrics.uptime = '99.5%'
store.metrics.errorRate = '0.1%'
expect(store.healthScore).toBeCloseTo(98.5)
expect(store.isHealthy).toBe(true)
})
})
Plugin Development Best Practices
1. Plugin Naming and Structure
// Good plugin naming
class McpServerMonitoringPlugin implements Plugin {
meta = {
id: 'mcp-server-monitoring', // kebab-case
name: 'MCP Server Monitoring', // Human readable
version: '1.2.3', // Semantic versioning
description: 'Real-time monitoring for MCP servers', // Clear description
author: 'DeployStack Team'
}
}
// Avoid generic names
// ❌ class UtilsPlugin
// ❌ class MyPlugin
// ❌ class Plugin1
2. Component Naming Convention
<!-- Good: Prefix with plugin name -->
<!-- MetricsWidget.vue -->
<!-- MetricsDashboard.vue -->
<!-- MetricsChart.vue -->
<!-- Avoid generic names that might conflict -->
<!-- ❌ Widget.vue -->
<!-- ❌ Dashboard.vue -->
<!-- ❌ Chart.vue -->
3. Error Handling
class RobustPlugin implements Plugin {
async initialize(app: App, router: Router, pinia: Pinia) {
try {
// Plugin initialization logic
await this.setupComponents()
await this.registerRoutes(router)
await this.initializeStore(pinia)
console.log(`${this.meta.name} initialized successfully`)
} catch (error) {
console.error(`Failed to initialize ${this.meta.name}:`, error)
// Graceful degradation
this.handleInitializationError(error)
// Don't throw unless it's critical
// throw 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
})
// Maybe show a user notification
// Maybe disable certain features
// Maybe use fallback functionality
}
}
4. Resource Cleanup
class CleanPlugin implements Plugin {
private intervals: number[] = []
private eventListeners: Array<{ element: EventTarget, type: string, listener: EventListener }> = []
async 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
})
}
async 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 MetricsData {
totalServers: number
activeDeployments: number
totalRequests: number
responseTime: string
uptime: string
errorRate: string
throughput: string
}
interface PluginConfig {
refreshInterval?: number
enableNotifications?: boolean
apiEndpoint?: string
}
// Use proper typing in components
interface Props {
data: MetricsData
config?: PluginConfig
onRefresh?: () => void
}
const props = withDefaults(defineProps<Props>(), {
config: () => ({}),
onRefresh: () => {}
})
6. Performance Considerations
class PerformantPlugin implements Plugin {
private cache = new Map<string, { data: any, timestamp: number }>()
private readonly CACHE_DURATION = 30000 // 30 seconds
async fetchData(key: string): Promise<any> {
// Check cache first
const cached = this.cache.get(key)
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
return cached.data
}
// Fetch fresh data
const data = await this.performApiCall(key)
// Update cache
this.cache.set(key, {
data,
timestamp: Date.now()
})
return data
}
private async performApiCall(key: string): Promise<any> {
// Actual API call implementation
const response = await fetch(`/api/plugin-data/${key}`)
return response.json()
}
}
Plugin Lifecycle Management
Initialization Order
Plugins are initialized in the order they're listed in the plugin loader, but you can control dependencies:
// In your plugin loader
export async function loadPlugins(): Promise<Plugin[]> {
const plugins = [
new CorePlugin(), // Initialize first (no dependencies)
new MetricsPlugin(), // Depends on core
new AdvancedPlugin(), // Depends on metrics
]
// Sort by dependencies if needed
return sortPluginsByDependencies(plugins)
}
function sortPluginsByDependencies(plugins: Plugin[]): Plugin[] {
// Implementation to sort plugins based on their dependencies
// This ensures plugins are initialized in the correct order
return plugins // simplified for example
}
Runtime Plugin Management
// Enable/disable plugins at runtime
class PluginManager {
private activePlugins = new Map<string, Plugin>()
async enablePlugin(pluginId: string): Promise<void> {
const plugin = this.getAvailablePlugin(pluginId)
if (!plugin) {
throw new Error(`Plugin ${pluginId} not found`)
}
if (this.activePlugins.has(pluginId)) {
console.warn(`Plugin ${pluginId} is already enabled`)
return
}
await plugin.initialize(this.app, this.router, this.pinia, this)
this.activePlugins.set(pluginId, plugin)
console.log(`Plugin ${pluginId} enabled successfully`)
}
async disablePlugin(pluginId: string): Promise<void> {
const plugin = this.activePlugins.get(pluginId)
if (!plugin) {
console.warn(`Plugin ${pluginId} is not currently enabled`)
return
}
await plugin.cleanup()
this.activePlugins.delete(pluginId)
// Remove extension points
this.removePluginExtensionPoints(pluginId)
console.log(`Plugin ${pluginId} disabled successfully`)
}
}
Plugin Distribution
Plugin Packaging
Create a proper plugin package structure:
my-plugin-package/
├── package.json # NPM package configuration
├── README.md # Plugin documentation
├── CHANGELOG.md # Version history
├── LICENSE # License file
├── src/
│ ├── index.ts # Main plugin export
│ ├── components/ # Plugin components
│ ├── stores/ # Plugin stores
│ └── types/ # Plugin types
├── dist/ # Compiled plugin (generated)
├── docs/ # Additional documentation
└── examples/ # Usage examples
Package.json for Plugin
{
"name": "@deploystack/mcp-metrics-plugin",
"version": "1.0.0",
"description": "MCP server metrics and analytics plugin for DeployStack",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"lint": "eslint src/**/*.ts",
"test": "vitest"
},
"keywords": [
"deploystack",
"plugin",
"mcp",
"metrics",
"analytics"
],
"peerDependencies": {
"vue": "^3.3.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0"
},
"devDependencies": {
"@types/vue": "^3.3.0",
"typescript": "^5.0.0",
"vite": "^4.3.0"
}
}
Plugin Registry
For team-wide plugin sharing:
// Plugin registry service
export class PluginRegistry {
private static instance: PluginRegistry
private plugins = new Map<string, Plugin>()
static getInstance(): PluginRegistry {
if (!this.instance) {
this.instance = new PluginRegistry()
}
return this.instance
}
register(plugin: Plugin): void {
if (this.plugins.has(plugin.meta.id)) {
throw new Error(`Plugin ${plugin.meta.id} is already registered`)
}
this.plugins.set(plugin.meta.id, plugin)
console.log(`Plugin ${plugin.meta.id} registered successfully`)
}
getPlugin(id: string): Plugin | undefined {
return this.plugins.get(id)
}
getAllPlugins(): Plugin[] {
return Array.from(this.plugins.values())
}
getPluginsByAuthor(author: string): Plugin[] {
return this.getAllPlugins().filter(plugin => plugin.meta.author === author)
}
}
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 MetricsPlugin = (await import('./mcp-metrics-plugin')).default
plugins.push(new MetricsPlugin())
console.log('✅ MetricsPlugin loaded')
} catch (error) {
console.error('❌ Failed to load MetricsPlugin:', error)
}
return plugins
}
Extension Points Not Rendering
<!-- Debug extension points -->
<template>
<div>
<h3>Debug Extension Points</h3>
<div v-for="(point, id) in extensionPoints" :key="id">
<strong>{{ id }}:</strong> {{ point.length }} components
<ul>
<li v-for="ext in point" :key="ext.pluginId">
{{ ext.pluginId }} (order: {{ ext.options?.order || 0 }})
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue'
const extensionPoints = inject('extensionPoints', {})
</script>
Plugin State Issues
// Debug plugin state
class DebuggablePlugin implements Plugin {
async 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 {
async initialize(app: App, router: Router, pinia: Pinia) {
const startTime = performance.now()
try {
await 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 async doInitialization() {
// Your plugin initialization logic
}
}
This comprehensive plugin system documentation provides everything needed to create powerful, 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.
Internationalization (i18n)
Guide to implementing multi-language support in DeployStack frontend using Vue I18n with modular file structure.
Router Optimization & Authentication Caching
Complete guide to the router performance optimizations and smart authentication caching system implemented in DeployStack frontend.