Files
core/docs/modules/EXTERNAL_MODULE.md
T
hykocx 99a56d2c39 feat(modules): add external module registration and defineModule support
- Add `./modules/define` export path pointing to `defineModule.js`
- Implement `registerExternalModules()` to handle modules passed via `zen.config.js`, with env var gating (`ZEN_MODULE_<NAME>=true`)
- Extract `buildAdminConfig()` helper to consolidate admin navigation/page config building
- Refactor `loadModuleConfig()` to use `buildAdminConfig()` and simplify public routes check
- Improve `initializeModuleTables()` to gracefully skip modules without `db.js` instead of erroring
- Update module discovery JSDoc to reflect external module registration support
2026-04-12 13:39:56 -04:00

6.1 KiB

Créer un module externe

Un module externe est un package npm indépendant qui s'intègre dans une app qui utilise @hykocx/zen. Il n'a pas besoin de modifier le code source du CMS.


Convention de nommage

@scope/zen-nom-du-module

Exemples : @hykocx/zen-invoice, @hykocx/zen-nuage.


Structure du package

zen-invoice/
├── index.js             # Point d'entrée — exporte defineModule()
├── admin/               # Composants React pour l'admin
│   ├── InvoiceListPage.js
│   ├── InvoiceCreatePage.js
│   └── InvoiceEditPage.js
├── db.js                # createTables() et dropTables()
├── actions.js           # Server actions pour pages publiques
├── metadata.js          # Générateurs de métadonnées SEO
├── package.json
└── .env.example

index.js

On utilise defineModule() importé depuis @hykocx/zen/modules/define.

import { lazy } from 'react';
import { defineModule } from '@hykocx/zen/modules/define';

import { createTables, dropTables } from './db.js';
import { getInvoiceByToken } from './actions.js';
import { generatePaymentMetadata } from './metadata.js';

const InvoiceListPage   = lazy(() => import('./admin/InvoiceListPage.js'));
const InvoiceCreatePage = lazy(() => import('./admin/InvoiceCreatePage.js'));
const InvoiceEditPage   = lazy(() => import('./admin/InvoiceEditPage.js'));

export default defineModule({
  name: 'invoice',
  displayName: 'Facturation',
  version: '1.0.0',
  description: 'Gestion des factures et paiements.',

  dependencies: [],
  envVars: ['STRIPE_SECRET_KEY', 'ZEN_INVOICE_TAX_RATE'],

  // Navigation dans le panneau admin
  navigation: [
    {
      id: 'invoice',
      title: 'Facturation',
      icon: 'Invoice03Icon',
      items: [
        { name: 'Factures', href: '/admin/invoice/list', icon: 'Invoice03Icon' },
        { name: 'Nouvelle', href: '/admin/invoice/new',  icon: 'Add01Icon' },
      ],
    },
  ],

  // Pages admin avec leurs composants lazy
  adminPages: {
    '/admin/invoice/list': InvoiceListPage,
    '/admin/invoice/new':  InvoiceCreatePage,
    '/admin/invoice/edit': InvoiceEditPage,
  },

  // Résolveur pour les routes dynamiques (ex: /admin/invoice/edit/123)
  pageResolver(path) {
    const parts = path.split('/').filter(Boolean);
    if (parts[0] !== 'admin' || parts[1] !== 'invoice') return null;
    if (parts[2] === 'list') return InvoiceListPage;
    if (parts[2] === 'new')  return InvoiceCreatePage;
    if (parts[2] === 'edit') return InvoiceEditPage;
    return null;
  },

  publicPages: {},
  publicRoutes: [
    { pattern: ':token',     description: 'Page de paiement' },
    { pattern: ':token/pdf', description: 'Télécharger la facture PDF' },
  ],

  dashboardWidgets: [],

  // Base de données
  db: { createTables, dropTables },

  // Server actions pour les pages publiques (/zen/invoice/...)
  actions: { getInvoiceByToken },

  // Générateurs de métadonnées SEO
  metadata: {
    payment: generatePaymentMetadata,
  },

  // Appelé une fois au démarrage du serveur
  // ctx donne accès aux services du CMS
  async setup(ctx) {
    const stripe = await ctx.payments.then(p => p.stripe);
    // Initialiser des webhooks, vérifier la config, etc.
    console.log('[invoice] Stripe prêt :', !!stripe);
  },
});

L'objet ctx dans setup()

setup(ctx) reçoit un objet qui donne accès aux services du CMS. Chaque propriété retourne une promesse vers le module correspondant.

async setup(ctx) {
  // Base de données PostgreSQL
  const { query, queryOne, queryAll } = await ctx.db;
  const rows = await query('SELECT * FROM zen_auth_users LIMIT 1');

  // Envoi de courriels (Resend)
  const { sendEmail } = await ctx.email;
  await sendEmail({ to: 'test@example.com', subject: 'Test', html: '<p>ok</p>' });

  // Stockage de fichiers (Cloudflare R2)
  const { uploadFile, deleteFile } = await ctx.storage;

  // Stripe
  const { stripe } = await ctx.payments;

  // Variables d'environnement
  const apiKey = ctx.config.get('STRIPE_SECRET_KEY');
}

package.json du module externe

{
  "name": "@hykocx/zen-invoice",
  "version": "1.0.0",
  "type": "module",
  "main": "./index.js",
  "exports": {
    ".": "./index.js"
  },
  "peerDependencies": {
    "@hykocx/zen": ">=1.0.0",
    "react": ">=19.0.0"
  }
}

Intégration dans l'app consommatrice

1. Installer le package

npm install @hykocx/zen-invoice

2. Créer zen.config.js à la racine de l'app

// zen.config.js
import invoiceModule from '@hykocx/zen-invoice';

export default {
  modules: [invoiceModule],
};

3. Passer la config à initializeZen()

// instrumentation.js
import zenConfig from './zen.config.js';

export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { initializeZen } = await import('@hykocx/zen');
    await initializeZen(zenConfig);
  }
}

4. Passer les modules à ZenProvider

// app/layout.js
import zenConfig from './zen.config.js';
import { ZenProvider } from '@hykocx/zen/provider';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ZenProvider modules={zenConfig.modules}>
          {children}
        </ZenProvider>
      </body>
    </html>
  );
}

5. Activer le module via variable d'environnement

# .env.local
ZEN_MODULE_INVOICE=true

La convention est la même que pour les modules internes : tirets en underscores, tout en majuscules.


Initialiser la base de données

Le module déclare ses tables dans db.createTables. Deux façons de les créer.

Au démarrage du serveur (si skipDb: false) :

await initializeZen({ modules: zenConfig.modules, skipDb: false });

Via la CLI :

npx zen-db init

Vérification rapide

  1. Démarrer avec ZEN_MODULE_INVOICE=true.
  2. Ouvrir /admin. La section "Facturation" doit apparaître dans la navigation.
  3. Naviguer vers /admin/invoice/list. La page du module doit se charger.
  4. Appeler getModuleActions('invoice') côté serveur. Les actions du module doivent être retournées.