DeesseJS

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-plugin

This 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.md

Manual Setup

Or create the structure manually:

mkdir my-plugin
cd my-plugin
npm init -y
npm install @deessejs/core

Basic 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 np

Best 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

On this page