DeployStack Docs

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

  1. Check Extension Point Placement: Ensure <ExtensionPoint pointId="your-point-id" /> is placed in your application views
  2. Verify Point ID: Make sure the extension point ID matches between registration and usage
  3. Component Import: Check that components are correctly imported and registered
  4. 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.