99a56d2c39
- 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
6.1 KiB
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
- Démarrer avec
ZEN_MODULE_INVOICE=true. - Ouvrir
/admin. La section "Facturation" doit apparaître dans la navigation. - Naviguer vers
/admin/invoice/list. La page du module doit se charger. - Appeler
getModuleActions('invoice')côté serveur. Les actions du module doivent être retournées.