MyCuppa

@mycuppa/plugin

The @mycuppa/plugin package provides a powerful plugin system that allows you to extend your application with modular, reusable functionality. Think of it like WordPress plugins or VS Code extensions for your React app.

Installation

npm install @mycuppa/plugin
# or
pnpm add @mycuppa/plugin

Quick Start

import { createPluginManager, createPlugin } from '@mycuppa/plugin'

// Create a plugin
const analyticsPlugin = createPlugin({
  metadata: {
    name: 'analytics',
    version: '1.0.0',
    description: 'Google Analytics integration',
  },
  hooks: {
    onEnable: async (context) => {
      console.log('Analytics plugin enabled!')
      // Initialize Google Analytics
    },
  },
  customHooks: {
    'app:pageview': async (context, page) => {
      // Track page view
      console.log('Page viewed:', page)
    },
  },
})

// Create plugin manager
const pluginManager = createPluginManager()

// Install and enable plugin
await pluginManager.install(analyticsPlugin)

// Use plugins in your app
await pluginManager.emitHook('app:pageview', '/dashboard')

Core Concepts

Plugin

A plugin is a self-contained module that adds functionality to your application. Each plugin has:

  • Metadata - Name, version, description, author
  • Dependencies - Other plugins this plugin requires
  • Lifecycle Hooks - Called at install, enable, disable, uninstall
  • Custom Hooks - Application events this plugin responds to
  • Configuration - Settings that customize plugin behavior
  • Priority - Execution order for hooks

Plugin Manager

The Plugin Manager is responsible for:

  • Installing and uninstalling plugins
  • Enabling and disabling plugins
  • Managing plugin dependencies
  • Executing lifecycle and custom hooks
  • Validating plugin compatibility

Hooks

Hooks are events that plugins can listen to and respond to:

  • Lifecycle Hooks - Built-in hooks for plugin lifecycle
  • Custom Hooks - Application-specific events you define

Creating Plugins

Basic Plugin

import { createPlugin } from '@mycuppa/plugin'

const myPlugin = createPlugin({
  metadata: {
    name: 'my-plugin',
    version: '1.0.0',
    description: 'My awesome plugin',
    author: 'Your Name',
    license: 'MIT',
  },
  hooks: {
    onEnable: async (context) => {
      console.log('Plugin enabled!')
    },
    onDisable: async (context) => {
      console.log('Plugin disabled!')
    },
  },
})

Plugin with Configuration

const themingPlugin = createPlugin({
  metadata: {
    name: 'theming',
    version: '1.0.0',
    description: 'Custom theme support',
  },
  defaultConfig: {
    primaryColor: '#007bff',
    darkMode: false,
    fontSize: 16,
  },
  hooks: {
    onEnable: async (context) => {
      const { primaryColor, darkMode } = context.config
      console.log(`Theme enabled: ${primaryColor}, dark: ${darkMode}`)

      // Apply theme to DOM
      document.documentElement.style.setProperty('--primary', primaryColor)
      if (darkMode) {
        document.body.classList.add('dark-mode')
      }
    },
  },
})

// Install with custom config
await pluginManager.install(themingPlugin, {
  primaryColor: '#ff0000',
  darkMode: true,
})

Plugin with Dependencies

const advancedAnalyticsPlugin = createPlugin({
  metadata: {
    name: 'advanced-analytics',
    version: '2.0.0',
    description: 'Advanced analytics with custom events',
  },
  dependencies: {
    'analytics': '1.0.0', // Requires analytics plugin v1.0.0
    'user-tracking': '*', // Any version
  },
  customHooks: {
    'app:custom-event': async (context, event) => {
      // This plugin depends on the base analytics plugin
      const analytics = context.manager.getPlugin('analytics')
      if (analytics) {
        // Use base plugin's functionality
        console.log('Custom event:', event)
      }
    },
  },
})

Plugin with Priority

// Higher priority plugins execute first
const loggerPlugin = createPlugin({
  metadata: {
    name: 'logger',
    version: '1.0.0',
  },
  priority: 100, // Execute before other plugins
  customHooks: {
    'app:request': async (context, request) => {
      console.log('Request started:', request)
    },
  },
})

const cachingPlugin = createPlugin({
  metadata: {
    name: 'caching',
    version: '1.0.0',
  },
  priority: 50, // Execute after logger
  customHooks: {
    'app:request': async (context, request) => {
      // Check cache
    },
  },
})

// When 'app:request' is emitted:
// 1. Logger plugin runs first (priority: 100)
// 2. Caching plugin runs second (priority: 50)

Plugin Lifecycle Hooks

onInstall

Called when the plugin is first installed:

const plugin = createPlugin({
  metadata: { name: 'my-plugin', version: '1.0.0' },
  hooks: {
    onInstall: async (context) => {
      console.log('Plugin installed!')
      // Set up initial state, create database tables, etc.
    },
  },
})

onEnable

Called when the plugin is enabled:

const plugin = createPlugin({
  metadata: { name: 'my-plugin', version: '1.0.0' },
  hooks: {
    onEnable: async (context) => {
      console.log('Plugin enabled!')
      // Start services, register event listeners, etc.
    },
  },
})

onDisable

Called when the plugin is disabled:

const plugin = createPlugin({
  metadata: { name: 'my-plugin', version: '1.0.0' },
  hooks: {
    onDisable: async (context) => {
      console.log('Plugin disabled!')
      // Stop services, unregister listeners, clean up
    },
  },
})

onUninstall

Called when the plugin is uninstalled:

const plugin = createPlugin({
  metadata: { name: 'my-plugin', version: '1.0.0' },
  hooks: {
    onUninstall: async (context) => {
      console.log('Plugin uninstalled!')
      // Remove data, delete database tables, etc.
    },
  },
})

Custom Hooks

Custom hooks allow plugins to respond to application-specific events.

Defining Custom Hooks

const notificationPlugin = createPlugin({
  metadata: {
    name: 'notifications',
    version: '1.0.0',
  },
  customHooks: {
    // Respond to user login
    'user:login': async (context, user) => {
      console.log(`Welcome notification sent to ${user.name}`)
    },

    // Respond to data changes
    'data:updated': async (context, entity, changes) => {
      console.log(`${entity} was updated:`, changes)
    },

    // Respond to errors
    'app:error': async (context, error) => {
      console.error('Error notification:', error.message)
    },
  },
})

Emitting Custom Hooks

// In your application code
const pluginManager = createPluginManager()

// After user logs in
await pluginManager.emitHook('user:login', {
  id: '123',
  name: 'John Doe',
  email: 'john@example.com',
})

// After data update
await pluginManager.emitHook('data:updated', 'user', {
  name: { old: 'John', new: 'Jane' },
})

// When error occurs
await pluginManager.emitHook('app:error', new Error('Something went wrong'))

Hook Context

Every hook receives a context object:

customHooks: {
  'my-hook': async (context, ...args) => {
    // Access plugin configuration
    const { apiKey } = context.config

    // Check if another plugin is enabled
    if (context.manager.isPluginEnabled('analytics')) {
      // Use that plugin
      const analytics = context.manager.getPlugin('analytics')
    }

    // Emit another hook
    await context.manager.emitHook('my-other-hook', 'data')
  },
}

Plugin Manager

Creating the Manager

import { createPluginManager } from '@mycuppa/plugin'

const pluginManager = createPluginManager({
  // Auto-enable plugins after install (default: true)
  autoEnable: true,

  // Validate plugin dependencies (default: true)
  validateDependencies: true,
})

Installing Plugins

// Install with default config
await pluginManager.install(myPlugin)

// Install with custom config
await pluginManager.install(myPlugin, {
  apiKey: 'abc123',
  enabled: true,
})

// Install multiple plugins
await Promise.all([
  pluginManager.install(plugin1),
  pluginManager.install(plugin2),
  pluginManager.install(plugin3),
])

Enabling and Disabling Plugins

// Enable a plugin
await pluginManager.enable('my-plugin')

// Disable a plugin
await pluginManager.disable('my-plugin')

// Check if enabled
if (pluginManager.isPluginEnabled('my-plugin')) {
  console.log('Plugin is active')
}

Uninstalling Plugins

// Uninstall a plugin
await pluginManager.uninstall('my-plugin')

Getting Plugin Information

// Get a specific plugin
const plugin = pluginManager.getPlugin('my-plugin')

// Get all plugins
const allPlugins = pluginManager.getAllPlugins()

// Get enabled plugins only
const enabledPlugins = pluginManager.getEnabledPlugins()

// Check if installed
if (pluginManager.isPluginInstalled('my-plugin')) {
  console.log('Plugin is installed')
}

Updating Configuration

// Update plugin configuration at runtime
pluginManager.updateConfig('theming', {
  primaryColor: '#00ff00',
  darkMode: false,
})

// Get current configuration
const config = pluginManager.getConfig('theming')
console.log(config.primaryColor) // '#00ff00'

Real-World Examples

Analytics Plugin

const analyticsPlugin = createPlugin({
  metadata: {
    name: 'analytics',
    version: '1.0.0',
    description: 'Google Analytics integration',
  },
  defaultConfig: {
    trackingId: '',
    anonymizeIp: true,
    debug: false,
  },
  hooks: {
    onEnable: async (context) => {
      const { trackingId, debug } = context.config

      // Load Google Analytics script
      const script = document.createElement('script')
      script.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}`
      document.head.appendChild(script)

      // Initialize GA
      window.dataLayer = window.dataLayer || []
      function gtag() {
        window.dataLayer.push(arguments)
      }
      gtag('js', new Date())
      gtag('config', trackingId, {
        anonymize_ip: context.config.anonymizeIp,
        debug_mode: debug,
      })

      console.log('Analytics initialized:', trackingId)
    },
  },
  customHooks: {
    'app:pageview': async (context, path) => {
      if (window.gtag) {
        window.gtag('event', 'page_view', {
          page_path: path,
        })
      }
    },
    'app:event': async (context, eventName, eventData) => {
      if (window.gtag) {
        window.gtag('event', eventName, eventData)
      }
    },
  },
})

// Install
await pluginManager.install(analyticsPlugin, {
  trackingId: 'UA-XXXXXXXXX-X',
})

// Use in your app
await pluginManager.emitHook('app:pageview', '/dashboard')
await pluginManager.emitHook('app:event', 'button_click', { button_name: 'signup' })

Dark Mode Plugin

const darkModePlugin = createPlugin({
  metadata: {
    name: 'dark-mode',
    version: '1.0.0',
    description: 'Dark mode support',
  },
  defaultConfig: {
    enabled: false,
    autoDetect: true, // Auto-detect from system preference
  },
  hooks: {
    onEnable: async (context) => {
      const { enabled, autoDetect } = context.config

      if (autoDetect) {
        const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)')
        const isDark = darkModeQuery.matches
        document.body.classList.toggle('dark-mode', isDark)

        // Listen for changes
        darkModeQuery.addEventListener('change', (e) => {
          document.body.classList.toggle('dark-mode', e.matches)
        })
      } else {
        document.body.classList.toggle('dark-mode', enabled)
      }
    },
  },
  customHooks: {
    'theme:toggle': async (context) => {
      const isDark = document.body.classList.contains('dark-mode')
      document.body.classList.toggle('dark-mode', !isDark)

      // Update config
      context.manager.updateConfig('dark-mode', { enabled: !isDark })
    },
  },
})

// Toggle dark mode from anywhere
await pluginManager.emitHook('theme:toggle')

Authentication Logger Plugin

const authLoggerPlugin = createPlugin({
  metadata: {
    name: 'auth-logger',
    version: '1.0.0',
    description: 'Logs authentication events',
  },
  dependencies: {
    'analytics': '*', // Depends on analytics plugin
  },
  customHooks: {
    'user:login': async (context, user) => {
      console.log(`User logged in: ${user.email}`)

      // Send to analytics
      await context.manager.emitHook('app:event', 'user_login', {
        user_id: user.id,
      })
    },
    'user:logout': async (context, user) => {
      console.log(`User logged out: ${user.email}`)

      // Send to analytics
      await context.manager.emitHook('app:event', 'user_logout', {
        user_id: user.id,
      })
    },
  },
})

Feature Flag Plugin

const featureFlagsPlugin = createPlugin({
  metadata: {
    name: 'feature-flags',
    version: '1.0.0',
    description: 'Feature flag management',
  },
  defaultConfig: {
    flags: {
      newDashboard: false,
      experimentalFeature: false,
      betaAccess: false,
    },
  },
  customHooks: {
    'feature:check': async (context, featureName) => {
      const flags = context.config.flags
      return flags[featureName] || false
    },
    'feature:enable': async (context, featureName) => {
      const flags = { ...context.config.flags, [featureName]: true }
      context.manager.updateConfig('feature-flags', { flags })
    },
    'feature:disable': async (context, featureName) => {
      const flags = { ...context.config.flags, [featureName]: false }
      context.manager.updateConfig('feature-flags', { flags })
    },
  },
})

// Check feature flags
const isEnabled = await pluginManager.emitHook('feature:check', 'newDashboard')

Plugin Dependencies

Defining Dependencies

const advancedPlugin = createPlugin({
  metadata: {
    name: 'advanced-feature',
    version: '2.0.0',
  },
  dependencies: {
    'base-plugin': '1.0.0', // Exact version
    'utility-plugin': '*', // Any version
    'optional-plugin': '1.2.3', // Specific version
  },
  hooks: {
    onEnable: async (context) => {
      // All dependencies are guaranteed to be installed and enabled
      const basePlugin = context.manager.getPlugin('base-plugin')
      console.log('Using base plugin:', basePlugin.metadata.version)
    },
  },
})

Automatic Dependency Resolution

// When you enable a plugin, dependencies are automatically enabled first
await pluginManager.enable('advanced-feature')

// This will:
// 1. Enable 'base-plugin' (if not already enabled)
// 2. Enable 'utility-plugin' (if not already enabled)
// 3. Enable 'optional-plugin' (if not already enabled)
// 4. Enable 'advanced-feature'

Preventing Dependent Plugin Disable

// If another plugin depends on this plugin, you can't disable it
await pluginManager.disable('base-plugin')
// Error: Cannot disable plugin "base-plugin" because it is required by: advanced-feature

Integration with React

Plugin Provider

import { createPluginManager } from '@mycuppa/plugin'
import { createContext, useContext, useEffect, useState } from 'react'

const PluginContext = createContext(null)

export function PluginProvider({ children, plugins }) {
  const [pluginManager] = useState(() => createPluginManager())
  const [isReady, setIsReady] = useState(false)

  useEffect(() => {
    const installPlugins = async () => {
      for (const { plugin, config } of plugins) {
        await pluginManager.install(plugin, config)
      }
      setIsReady(true)
    }

    installPlugins()
  }, [])

  if (!isReady) {
    return <div>Loading plugins...</div>
  }

  return (
    <PluginContext.Provider value={pluginManager}>
      {children}
    </PluginContext.Provider>
  )
}

export function usePluginManager() {
  const manager = useContext(PluginContext)
  if (!manager) {
    throw new Error('usePluginManager must be used within PluginProvider')
  }
  return manager
}

Using Plugins in Components

function AnalyticsButton() {
  const pluginManager = usePluginManager()

  const handleClick = async () => {
    await pluginManager.emitHook('app:event', 'button_click', {
      button_name: 'cta',
    })
  }

  return <button onClick={handleClick}>Sign Up</button>
}

Plugin Settings UI

function PluginSettings() {
  const pluginManager = usePluginManager()
  const [plugins, setPlugins] = useState([])

  useEffect(() => {
    setPlugins(pluginManager.getAllPlugins())
  }, [])

  const togglePlugin = async (name) => {
    if (pluginManager.isPluginEnabled(name)) {
      await pluginManager.disable(name)
    } else {
      await pluginManager.enable(name)
    }
    setPlugins(pluginManager.getAllPlugins())
  }

  return (
    <div>
      <h2>Plugins</h2>
      <ul>
        {plugins.map((plugin) => {
          const entry = pluginManager.getPluginEntry(plugin.metadata.name)
          return (
            <li key={plugin.metadata.name}>
              <strong>{plugin.metadata.name}</strong>
              <span> v{plugin.metadata.version}</span>
              <p>{plugin.metadata.description}</p>
              <button onClick={() => togglePlugin(plugin.metadata.name)}>
                {entry?.state === 'enabled' ? 'Disable' : 'Enable'}
              </button>
            </li>
          )
        })}
      </ul>
    </div>
  )
}

Best Practices

  1. Use semantic versioning - Follow semver for plugin versions

    version: '1.2.3' // major.minor.patch
    
  2. Provide default configuration - Make plugins work out of the box

    defaultConfig: {
      enabled: true,
      apiKey: 'demo-key',
    }
    
  3. Handle errors gracefully - Don't crash the app if a plugin fails

    try {
      await pluginManager.emitHook('my-hook', data)
    } catch (error) {
      console.error('Plugin error:', error)
    }
    
  4. Document your hooks - Make it clear what hooks your plugin provides

    /**
     * Hooks:
     * - user:login (user) - Called when user logs in
     * - user:logout (user) - Called when user logs out
     */
    
  5. Keep plugins focused - One plugin should do one thing well

    // Good: Focused plugins
    - analytics-plugin
    - dark-mode-plugin
    - notifications-plugin
    
    // Avoid: Kitchen sink plugins
    - super-mega-plugin (does everything)
    
  6. Validate dependencies - Ensure required plugins are available

    validateDependencies: true
    
  7. Use priority wisely - Higher priority for critical plugins

    priority: 100 // Authentication plugin
    priority: 50  // Analytics plugin
    priority: 10  // UI enhancement plugin
    

API Reference

createPlugin(options)

Creates a new plugin.

Parameters:

  • metadata: PluginMetadata - Plugin metadata (required)
    • name: string - Unique plugin name (required)
    • version: string - Semver version (required)
    • description?: string - Plugin description
    • author?: string - Plugin author
    • homepage?: string - Plugin homepage URL
    • license?: string - Plugin license
    • keywords?: string[] - Plugin keywords
  • dependencies?: PluginDependencies - Plugin dependencies
  • defaultConfig?: PluginConfig - Default configuration
  • priority?: number - Hook execution priority (default: 0)
  • hooks?: PluginLifecycleHooks - Lifecycle hooks
    • onInstall?: (context) => void | Promise<void>
    • onEnable?: (context) => void | Promise<void>
    • onDisable?: (context) => void | Promise<void>
    • onUninstall?: (context) => void | Promise<void>
  • customHooks?: PluginCustomHooks - Custom hooks

Returns: Plugin

createPluginManager(options?)

Creates a new plugin manager.

Parameters:

  • autoEnable?: boolean - Auto-enable on install (default: true)
  • validateDependencies?: boolean - Validate dependencies (default: true)

Returns: PluginManager

PluginManager Methods

  • install(plugin, config?) - Install plugin
  • uninstall(name) - Uninstall plugin
  • enable(name) - Enable plugin
  • disable(name) - Disable plugin
  • getPlugin(name) - Get plugin by name
  • getPluginEntry(name) - Get plugin with state
  • isPluginInstalled(name) - Check if installed
  • isPluginEnabled(name) - Check if enabled
  • getAllPlugins() - Get all plugins
  • getEnabledPlugins() - Get enabled plugins
  • updateConfig(name, config) - Update config
  • getConfig(name) - Get config
  • emitHook(hookName, ...args) - Emit custom hook

PluginContext

Context object passed to hooks:

  • config: PluginConfig - Plugin configuration
  • manager - Manager instance
    • getPlugin(name) - Get another plugin
    • isPluginEnabled(name) - Check if enabled
    • emitHook(hookName, ...args) - Emit hook

PluginState Enum

  • INSTALLED - Plugin installed but not enabled
  • ENABLED - Plugin enabled and active
  • DISABLED - Plugin disabled

Comparison to Traditional Approach

Traditional approach (static features):

// All features are always active, hard to extend
if (config.analyticsEnabled) {
  trackEvent('pageview', '/dashboard')
}

if (config.darkModeEnabled) {
  document.body.classList.add('dark-mode')
}

// Adding new features requires modifying core code

With @mycuppa/plugin:

// Features are modular plugins that can be installed/uninstalled
const analyticsPlugin = createPlugin({ /* ... */ })
const darkModePlugin = createPlugin({ /* ... */ })

await pluginManager.install(analyticsPlugin)
await pluginManager.install(darkModePlugin)

// Adding new features is just installing a new plugin
// No core code changes needed

The Cuppa plugin system provides a modular, extensible architecture that allows you to build applications with add-on functionality, similar to WordPress, VS Code, or other plugin-based platforms.