@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
-
Use semantic versioning - Follow semver for plugin versions
version: '1.2.3' // major.minor.patch -
Provide default configuration - Make plugins work out of the box
defaultConfig: { enabled: true, apiKey: 'demo-key', } -
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) } -
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 */ -
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) -
Validate dependencies - Ensure required plugins are available
validateDependencies: true -
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 descriptionauthor?: string- Plugin authorhomepage?: string- Plugin homepage URLlicense?: string- Plugin licensekeywords?: string[]- Plugin keywords
dependencies?: PluginDependencies- Plugin dependenciesdefaultConfig?: PluginConfig- Default configurationpriority?: number- Hook execution priority (default: 0)hooks?: PluginLifecycleHooks- Lifecycle hooksonInstall?: (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 pluginuninstall(name)- Uninstall pluginenable(name)- Enable plugindisable(name)- Disable plugingetPlugin(name)- Get plugin by namegetPluginEntry(name)- Get plugin with stateisPluginInstalled(name)- Check if installedisPluginEnabled(name)- Check if enabledgetAllPlugins()- Get all pluginsgetEnabledPlugins()- Get enabled pluginsupdateConfig(name, config)- Update configgetConfig(name)- Get configemitHook(hookName, ...args)- Emit custom hook
PluginContext
Context object passed to hooks:
config: PluginConfig- Plugin configurationmanager- Manager instancegetPlugin(name)- Get another pluginisPluginEnabled(name)- Check if enabledemitHook(hookName, ...args)- Emit hook
PluginState Enum
INSTALLED- Plugin installed but not enabledENABLED- Plugin enabled and activeDISABLED- 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.