Aller au contenu

Création de plugins

Écrivez des plugins personnalisés pour étendre Silex avec de nouvelles fonctionnalités.

Aperçu

Un plugin Silex est un module JavaScript qui suit le patron de plugin : il exporte un objet avec une fonction init. Les plugins peuvent être côté client (extensions de l'éditeur), côté serveur (connecteurs et routes), ou les deux.

Structure d'un plugin :

// my-plugin.js
export default {
  name: 'MyPlugin',
  async init(editor, options) {
    console.log('Plugin initialized with options:', options)
  },
}

Les plugins reçoivent : - Plugin client : (editor, options) — l'instance de l'éditeur GrapesJS - Plugin serveur : (config, options) — l'instance de ServerConfig

Licence : Les plugins ne sont pas considérés comme des oeuvres dérivées de Silex au regard de la licence AGPL. Vous pouvez utiliser n'importe quelle licence pour vos plugins.

Contribuer au coeur de Silex : Si vous souhaitez corriger des bugs ou ajouter des fonctionnalités à Silex lui-même (pas seulement des plugins), consultez le guide de contribution pour les instructions d'installation et les premières tâches à réaliser.

Prérequis

Plugins côté client

Étendez l'éditeur GrapesJS avec une interface personnalisée, des blocs ou des commandes.

Structure de base

// plugins/my-editor-plugin.js
export default {
  name: 'MyEditorPlugin',
  async init(editor, options) {
    console.log('Editor plugin initialized')

    // Access the editor
    const canvas = editor.Canvas
    const blocks = editor.Blocks
    const commands = editor.Commands

    // Add a custom block
    blocks.add('my-block', {
      label: 'My Custom Block',
      content: '<div class="my-block">Custom content</div>',
      category: 'Custom',
      media: '<svg>...</svg>',
      attributes: {
        class: 'gjs-fonts gjs-f-h1',
      },
    })

    // Add a custom command
    commands.add('my-command', {
      run(editor) {
        console.log('Command executed')
      },
      stop(editor) {
        console.log('Command stopped')
      },
    })

    // Listen to editor events
    editor.on('component:create', (component) => {
      console.log('Component created:', component)
    })
  },
}

Ajouter des panneaux d'interface

Ajoutez un panneau personnalisé à l'éditeur :

export default {
  name: 'MyPanel',
  async init(editor, options) {
    const pnm = editor.Panels

    // Create a new panel
    pnm.addPanel({
      id: 'my-panel',
      buttons: [
        {
          id: 'my-button',
          label: 'My Button',
          command: 'my-command',
          className: 'btn-icon-blank',
        },
      ],
    })

    // Add button to existing toolbar
    const topPanel = pnm.getPanel('options')
    topPanel.buttons.add([
      {
        id: 'another-button',
        label: 'Another Button',
        active: false,
        togglable: true,
        command: 'another-command',
      },
    ])
  },
}

Interagir avec les éléments sélectionnés

export default {
  name: 'ElementModifier',
  async init(editor, options) {
    editor.Commands.add('bold-selected', {
      run(editor) {
        const selected = editor.getSelected()
        if (selected) {
          selected.addClasses('font-weight-bold')
        }
      },
    })
  },
}

Chargement dans la configuration client

// client-config.js
import myEditorPlugin from './plugins/my-editor-plugin'

export default async function (config) {
  config.grapesJsConfig.plugins = [
    ...config.grapesJsConfig.plugins,
    myEditorPlugin,
  ]

  config.grapesJsConfig.pluginsOpts = {
    ...config.grapesJsConfig.pluginsOpts,
    MyEditorPlugin: {
      customOption: 'value',
    },
  }
}

Plugins côté serveur

Étendez le backend avec des connecteurs, des routes ou des écouteurs d'événements.

Structure de base

// plugins/my-server-plugin.js
module.exports = {
  name: 'MyServerPlugin',
  async init(config, options) {
    console.log('Server plugin initialized with options:', options)

    // Add routes to the Express app
    // config.app is available after plugins are loaded
  },
}

Ajouter des connecteurs

Créez un connecteur de stockage ou d'hébergement personnalisé :

// plugins/my-connector.js
const { Connector, StorageConnector, ConnectorType } = require('@silexlabs/silex-plugins')

class MyStorageConnector extends StorageConnector {
  connectorId = 'my-storage'
  displayName = 'My Storage'
  connectorType = ConnectorType.STORAGE
  color = '#ff0000'
  background = '#ffeeee'
  icon = 'data:image/svg+xml,...'

  async isLoggedIn(session) {
    // Check if user is logged in
    return !!session.myStorageToken
  }

  async getOAuthUrl(session) {
    // Return OAuth login URL
    return `https://my-service.com/oauth?client_id=${this.options.clientId}`
  }

  async listWebsites(session) {
    // Return array of websites
    return []
  }

  async createWebsite(session, data) {
    // Create a new website
    return 'website-id'
  }

  // ... implement other StorageConnector methods
}

module.exports = {
  name: 'MyStorageConnectorPlugin',
  async init(config, options) {
    config.addStorageConnector(
      new MyStorageConnector(config, {
        clientId: options.clientId,
        clientSecret: options.clientSecret,
      })
    )
  },
}

Ajouter des routes Express

module.exports = {
  name: 'MyApiPlugin',
  async init(config, options) {
    // Note: config.addRoutes is called after plugin init
    // Store the route setup for later
    const originalAddRoutes = config.addRoutes.bind(config)

    config.addRoutes = async (app) => {
      await originalAddRoutes(app)

      // Add custom routes
      app.get('/api/my-endpoint', (req, res) => {
        res.json({ message: 'Hello from my plugin' })
      })

      app.post('/api/my-endpoint', (req, res) => {
        const data = req.body
        // Process data
        res.json({ success: true })
      })
    }
  },
}

Écouter les événements du serveur

const { ServerEvent } = require('@silexlabs/silex/dist/server/events')

module.exports = {
  name: 'MyLoggerPlugin',
  async init(config, options) {
    config.on(ServerEvent.STARTUP_END, () => {
      console.log(`Silex is running at ${config.url}`)
    })

    config.on(ServerEvent.PUBLISH_START, async (publicationData) => {
      console.log(`Publishing: ${publicationData.siteSettings.name}`)
    })

    config.on(ServerEvent.PUBLISH_END, async (error) => {
      if (error) {
        console.error(`Publication failed: ${error.message}`)
      } else {
        console.log('Publication succeeded')
      }
    })
  },
}

Chargement dans la configuration serveur

// .silex.js
const myServerPlugin = require('./plugins/my-server-plugin')

module.exports = async function (config) {
  await config.addPlugin(myServerPlugin, {
    apiKey: process.env.MY_API_KEY,
    debug: config.debug,
  })
}

Transformateurs de publication

Personnalisez la compilation et la publication des sites web. Consultez Transformateurs de publication pour plus de détails.

Exemple de plugin de transformateur de publication :

// plugins/minify-plugin.js
export default {
  name: 'MinifyPlugin',
  async init(config, options) {
    config.addPublicationTransformers({
      transformFile(file) {
        // Minify HTML files
        if (file.type === 'html') {
          // Use a minification library
          const minified = minifyHtml(file.content)
          return { ...file, content: minified }
        }
        return file
      },

      transformPath(path, type) {
        // Change .css to .min.css
        if (type === 'css') {
          return path.replace(/\.css$/, '.min.css')
        }
        return path
      },
    })
  },
}

Chargez dans la configuration client :

import minifyPlugin from './plugins/minify-plugin'

export default async function (config) {
  config.grapesJsConfig.plugins.push(minifyPlugin)
}

Plugins full-stack

Les plugins peuvent s'exécuter à la fois côté client et serveur :

// plugins/full-stack-plugin.js

// Export two different modules
module.exports.client = {
  name: 'MyPlugin-Client',
  async init(editor, options) {
    // Client-side code
    editor.Commands.add('my-command', { /* ... */ })
  },
}

module.exports.server = {
  name: 'MyPlugin-Server',
  async init(config, options) {
    // Server-side code
    config.addStorageConnector(/* ... */)
  },
}

Chargez séparément :

// client-config.js
import { client } from './plugins/full-stack-plugin'
config.grapesJsConfig.plugins.push(client)

// .silex.js
const { server } = require('./plugins/full-stack-plugin')
await config.addPlugin(server, {})

Packager des plugins

Pour la publication sur npm

Créez une structure de paquet :

my-silex-plugin/
├── package.json
├── dist/
│   ├── client.js
│   ├── server.js
│   └── index.js
├── src/
│   ├── client.js
│   ├── server.js
│   └── index.js
└── README.md

package.json :

{
  "name": "my-silex-plugin",
  "version": "1.0.0",
  "main": "dist/index.js",
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch"
  },
  "peerDependencies": {
    "@silexlabs/silex": "^3.0.0"
  }
}

Publiez sur npm :

npm publish

Les utilisateurs installent avec :

npm install my-silex-plugin

Pour un usage privé

Conservez les plugins dans le répertoire de votre instance Silex :

my-silex/
├── .silex.js
├── client-config.js
└── plugins/
    ├── my-storage-connector.js
    ├── my-editor-plugin.js
    └── my-transformer.js

Chargez avec des chemins relatifs :

// .silex.js
const myConnector = require('./plugins/my-storage-connector')
await config.addPlugin(myConnector, {})

// client-config.js
import myEditorPlugin from './plugins/my-editor-plugin'

Support TypeScript

Écrivez des plugins en TypeScript pour un meilleur support IDE :

// plugins/my-plugin.ts
import { Editor, EditorConfig } from 'grapesjs'

interface MyPluginOptions {
  apiKey: string
  debug?: boolean
}

export default {
  name: 'MyPlugin',
  async init(editor: Editor, options: MyPluginOptions) {
    // Fully typed
    editor.Blocks.add('my-block', { /* ... */ })
  },
}

Compilez en JavaScript :

tsc --target es2020 --module commonjs plugins/*.ts

Tester les plugins

Testez avec Jest ou un autre framework de test :

// my-plugin.test.js
describe('MyPlugin', () => {
  it('should initialize', async () => {
    const mockEditor = {
      Commands: { add: jest.fn() },
      Blocks: { add: jest.fn() },
    }

    const plugin = require('./my-plugin')
    await plugin.init(mockEditor, { })

    expect(mockEditor.Commands.add).toHaveBeenCalled()
  })
})

Dépannage

Le plugin ne s'initialise pas

Vérifiez :

  1. Le plugin est chargé dans le fichier de configuration
  2. Le plugin exporte la bonne structure avec name et init
  3. Pas d'erreurs de syntaxe dans le code du plugin
  4. Les options sont passées correctement

Activez le débogage :

export default {
  name: 'MyPlugin',
  async init(editor, options) {
    console.log('Plugin init called with:', options)
  },
}

Conflits du plugin avec GrapesJS

Certains plugins modifient le comportement de base de GrapesJS. En cas de conflits :

  1. Changez l'ordre de chargement
  2. Vérifiez le code source du plugin pour les surcharges
  3. Utilisez un plugin différent ou corrigez les conflits

Les routes du plugin serveur ne sont pas accessibles

Assurez-vous que les routes sont ajoutées dans config.addRoutes :

config.addRoutes = async (app) => {
  await originalAddRoutes(app)
  app.get('/my-route', (req, res) => { /* ... */ })
}

Les routes ajoutées en dehors de cette fonction peuvent ne pas fonctionner.

Voir aussi

Éditer cette page sur GitLab