Creating Plugins
Learn how to create custom plugins for DeesseJS
Creating Plugins
This guide will walk you through creating your own DeesseJS plugins from scratch.
Getting Started
Initialize a Plugin
Use the CLI to scaffold a new plugin:
npx deessejs plugin create my-pluginThis creates a basic plugin structure:
my-plugin/
├── src/
│ ├── index.ts # Main plugin file
│ ├── client.ts # Client-side code
│ └── server.ts # Server-side code
├── package.json
├── tsconfig.json
└── README.mdManual Setup
Or create the structure manually:
mkdir my-plugin
cd my-plugin
npm init -y
npm install @deessejs/coreBasic Plugin
Minimal Plugin Example
// src/index.ts
import { definePlugin } from '@deessejs/core'
interface PluginOptions {
enabled?: boolean
apiKey?: string
}
export const myPlugin = definePlugin<PluginOptions>({
name: 'my-plugin',
version: '1.0.0',
activate(options) {
console.log('My Plugin activated!', options)
return {
// Plugin extensions
}
},
deactivate() {
console.log('My Plugin deactivated!')
},
})Export the Plugin
// src/index.ts
export { myPlugin } from './plugin'
export type { PluginOptions } from './types'Plugin Types
1. Admin Extension Plugin
Add custom pages and widgets to the admin dashboard:
export const adminPlugin = definePlugin({
name: 'admin-extension',
version: '1.0.0',
activate(options) {
return {
admin: {
// Add navigation items
navigation: [
{
type: 'link',
title: 'Analytics',
href: '/admin/analytics',
icon: 'BarChart',
},
],
// Add pages
pages: [
{
id: 'analytics',
title: 'Analytics',
path: '/analytics',
component: './admin/AnalyticsPage',
permissions: ['analytics:view'],
},
],
// Add dashboard widgets
widgets: [
{
id: 'stats-widget',
component: './admin/widgets/StatsWidget',
position: 'top',
size: 'large',
},
],
},
}
},
})Admin Page Component
// admin/AnalyticsPage.tsx
export default function AnalyticsPage() {
const { data } = useSWR('/api/analytics', fetcher)
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">Analytics</h1>
<div className="grid grid-cols-3 gap-4">
<StatCard label="Views" value={data?.views} />
<StatCard label="Users" value={data?.users} />
<StatCard label="Conversions" value={data?.conversions} />
</div>
</div>
)
}2. Collection Extension Plugin
Extend existing collections with additional fields:
export const collectionPlugin = definePlugin({
name: 'collection-extension',
version: '1.0.0',
activate(options) {
return {
collections: {
extend: {
posts: {
fields: [
{
name: 'seoTitle',
type: 'string',
admin: {
label: 'SEO Title',
description: 'Override the page title for SEO',
},
},
{
name: 'metaDescription',
type: 'text',
admin: {
label: 'Meta Description',
description: 'Description for search engines',
},
},
{
name: 'ogImage',
type: 'media',
admin: {
label: 'Open Graph Image',
},
},
],
},
},
},
}
},
})3. Custom Field Type Plugin
Create your own field types:
export const customFieldPlugin = definePlugin({
name: 'custom-fields',
version: '1.0.0',
activate(options) {
return {
fields: [
{
name: 'color-picker',
component: './fields/ColorPicker',
validate: (value) => /^#[0-9A-F]{6}$/i.test(value),
},
{
name: 'markdown',
component: './fields/MarkdownEditor',
sanitize: false, // Allow HTML
},
],
}
},
})Custom Field Component
// fields/ColorPicker.tsx
'use client'
import { useState } from 'react'
interface ColorPickerProps {
value: string
onChange: (value: string) => void
}
export function ColorPicker({ value, onChange }: ColorPickerProps) {
const [color, setColor] = useState(value || '#000000')
return (
<div className="flex gap-2">
<input
type="color"
value={color}
onChange={(e) => {
setColor(e.target.value)
onChange(e.target.value)
}}
className="h-10 w-10"
/>
<input
type="text"
value={color}
onChange={(e) => setColor(e.target.value)}
className="flex-1 border rounded px-2"
pattern="^#[0-9A-F]{6}$"
/>
</div>
)
}4. API Extension Plugin
Add custom API endpoints:
export const apiPlugin = definePlugin({
name: 'api-extension',
version: '1.0.0',
activate(options) {
return {
api: {
routes: [
{
method: 'GET',
path: '/api/analytics',
handler: './api/analytics/route',
},
{
method: 'POST',
path: '/api/webhook',
handler: './api/webhook/route',
},
],
},
}
},
})API Route Handler
// api/analytics/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const stats = await getAnalytics()
return NextResponse.json(stats)
}5. Integration Plugin
Integrate with external services:
interface IntegrationOptions {
apiKey: string
webhookUrl?: string
}
export const integrationPlugin = definePlugin<IntegrationOptions>({
name: 'external-service',
version: '1.0.0',
activate(options) {
// Initialize service client
const client = new ExternalServiceClient(options.apiKey)
return {
hooks: {
afterCreate: async ({ collection, item }) => {
// Sync to external service
await client.createItem(collection, item)
},
afterUpdate: async ({ collection, item }) => {
await client.updateItem(collection, item)
},
afterDelete: async ({ collection, id }) => {
await client.deleteItem(collection, id)
},
},
admin: {
pages: [
{
id: 'integration-settings',
title: 'Integration Settings',
path: '/settings/integration',
component: './admin/IntegrationSettings',
},
],
},
}
},
})Advanced Plugin Features
Plugin Configuration
Create a configuration UI for your plugin:
export const configurablePlugin = definePlugin({
name: 'configurable-plugin',
version: '1.0.0',
activate(options) {
return {
admin: {
settings: [
{
id: 'plugin-config',
label: 'Plugin Configuration',
component: './admin/PluginConfig',
// Save to database
save: async (values) => {
await db.pluginSettings.upsert({
where: { plugin: 'configurable-plugin' },
create: { plugin: 'configurable-plugin', settings: values },
update: { settings: values },
})
},
load: async () => {
const settings = await db.pluginSettings.findUnique({
where: { plugin: 'configurable-plugin' },
})
return settings?.settings || {}
},
},
],
},
}
},
})Plugin State Management
Manage plugin-specific state:
export const statefulPlugin = definePlugin({
name: 'stateful-plugin',
version: '1.0.0',
activate(options) {
// Create state storage
const state = new Map<string, any>()
return {
// Provide state to components
context: {
state,
get: (key: string) => state.get(key),
set: (key: string, value: any) => state.set(key, value),
},
}
},
})Plugin Communication
Allow plugins to communicate with each other:
export const eventPlugin = definePlugin({
name: 'event-plugin',
version: '1.0.0',
activate(options) {
const listeners = new Map<string, Function[]>()
return {
context: {
on: (event: string, callback: Function) => {
if (!listeners.has(event)) {
listeners.set(event, [])
}
listeners.get(event)?.push(callback)
},
emit: (event: string, data: any) => {
listeners.get(event)?.forEach((callback) => callback(data))
},
},
}
},
})Testing Plugins
Unit Tests
// __tests__/plugin.test.ts
import { describe, it, expect } from 'vitest'
import { myPlugin } from '../src'
describe('My Plugin', () => {
it('should activate with options', () => {
const result = myPlugin.activate({ apiKey: 'test' })
expect(result).toBeDefined()
})
it('should extend collections', () => {
const result = myPlugin.activate({})
expect(result.collections).toBeDefined()
})
})Integration Tests
// __tests__/integration.test.ts
import { describe, it, expect } from 'vitest'
import { createTestContext } from '@deessejs/test'
describe('Plugin Integration', () => {
it('should add fields to collections', async () => {
const { db } = await createTestContext({
plugins: [myPlugin()],
})
const post = await db.posts.create({
data: {
title: 'Test',
customField: 'value',
},
})
expect(post.customField).toBe('value')
})
})Publishing Plugins
Package.json
{
"name": "@deessejs/plugin-my-plugin",
"version": "1.0.0",
"description": "My awesome DeesseJS plugin",
"keywords": [
"deessejs",
"plugin",
"cms"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"peerDependencies": {
"@deessejs/core": "^1.0.0"
},
"scripts": {
"build": "tsup",
"test": "vitest",
"lint": "eslint src"
}
}Build Configuration
// tsup.config.ts
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
external: ['@deessejs/core'],
})Publishing
# Build the plugin
npm run build
# Publish to npm
npm publish
# Or use np for safer publishing
npx npBest Practices
Code Organization
- Separate client and server code
- Use TypeScript for type safety
- Follow consistent naming conventions
- Document all public APIs
Performance
- Minimize bundle size
- Use dynamic imports for large components
- Cache expensive operations
- Avoid unnecessary re-renders
Security
- Validate all user inputs
- Sanitize data from external sources
- Use secure defaults
- Document security considerations
Documentation
- Provide clear installation instructions
- Include configuration examples
- Document all options and hooks
- Keep README up to date
Next Steps
- Explore Plugin API Reference
- View Plugin Examples
- Learn about Admin Dashboard