- Rename `getModuleMetadata` to `getModuleMetadataGenerator` in registry, index, and client exports to clarify its purpose (returns a generator function, not a metadata object) - Add new `getModuleMetadata` and `getMetadataGenerator` exports from `modules.metadata.js` for server-side metadata object retrieval - Update route auth format in docs from `requireAuth`/`requireAdmin` flags to a single `auth` field with values: `'admin'`, `'user'`, or `'public'` - Fix `isModuleEnabledInEnv` to replace hyphens with underscores in env var names (e.g. `my-module` → `ZEN_MODULE_MY_MODULE`) - Replace `useState` initializer in `ZenProvider` with `useRef` guard to avoid React strict mode double-invocation issues
5.4 KiB
Créer un module interne
Un module interne vit dans src/modules/ et fait partie du package @zen/core. Il a accès direct aux services du CMS sans passer par un contexte injecté.
Structure d'un module
src/modules/mon-module/
├── module.config.js # Obligatoire — navigation, pages, routes, cron, etc.
├── db.js # Optionnel — createTables() et dropTables()
├── api.js # Optionnel — routes REST
├── cron.config.js # Optionnel — tâches planifiées
├── crud.js # Optionnel — accès aux données
├── admin/ # Composants React pour l'admin
└── .env.example # Variables d'environnement requises
module.config.js
C'est la source de vérité du module. On utilise defineModule() pour déclarer la configuration.
import { lazy } from 'react';
import { defineModule } from '../../core/modules/defineModule.js';
const ListPage = lazy(() => import('./admin/ListPage.js'));
const CreatePage = lazy(() => import('./admin/CreatePage.js'));
const EditPage = lazy(() => import('./admin/EditPage.js'));
export default defineModule({
name: 'mon-module',
displayName: 'Mon module',
version: '1.0.0',
description: 'Description courte.',
// Modules dont celui-ci dépend (vérification au démarrage)
dependencies: [],
// Variables d'environnement que ce module lit
envVars: ['ZEN_MON_MODULE_OPTION'],
// Navigation admin — un ou plusieurs objets de section
navigation: [
{
id: 'mon-module',
title: 'Mon module',
icon: 'SomeIcon',
items: [
{ name: 'Liste', href: '/admin/mon-module/list', icon: 'SomeIcon' },
{ name: 'Nouveau', href: '/admin/mon-module/new', icon: 'AddIcon' },
],
},
],
// Pages admin — chemin exact vers composant lazy
adminPages: {
'/admin/mon-module/list': ListPage,
'/admin/mon-module/new': CreatePage,
'/admin/mon-module/edit': EditPage,
},
// Résolveur pour les routes dynamiques (non connues à la compilation)
// Retourner null si le chemin ne correspond pas
pageResolver(path) {
const parts = path.split('/').filter(Boolean);
if (parts[0] !== 'admin' || parts[1] !== 'mon-module') return null;
if (parts[2] === 'list') return ListPage;
if (parts[2] === 'new') return CreatePage;
if (parts[2] === 'edit') return EditPage;
return null;
},
// Pages publiques accessibles sans authentification (/zen/mon-module/...)
publicPages: {},
publicRoutes: [],
// Widgets affichés sur le dashboard admin
dashboardWidgets: [],
});
db.js
Si le module crée des tables, on exporte createTables et dropTables.
import { query } from '../../core/database/index.js';
import { tableExists } from '../../core/database/helpers.js';
export async function createTables() {
const created = [];
const skipped = [];
if (!(await tableExists('zen_mon_module'))) {
await query(`
CREATE TABLE zen_mon_module (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
titre TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
)
`);
created.push('zen_mon_module');
} else {
skipped.push('zen_mon_module');
}
return { created, skipped };
}
export async function dropTables() {
await query('DROP TABLE IF EXISTS zen_mon_module CASCADE');
}
api.js
Les routes API sont montées automatiquement sous /api/zen/mon-module/....
async function handleList(request) {
// ...
return Response.json({ items });
}
export default {
routes: [
{ path: '/admin/mon-module/list', method: 'GET', handler: handleList, auth: 'admin' },
{ path: '/mon-module/list', method: 'GET', handler: handleList, auth: 'public' },
],
};
Valeurs acceptées pour auth : 'admin' (JWT admin requis), 'user' (JWT utilisateur requis), 'public' (aucune auth).
cron.config.js
import { maFonction } from './crud.js';
export default {
jobs: [
{
name: 'mon-module:nettoyage',
schedule: '0 3 * * *', // Tous les jours à 3h
handler: maFonction,
timezone: 'America/Toronto',
},
],
};
Enregistrement — 2 étapes
Après avoir créé les fichiers, on enregistre le module dans deux endroits.
1. src/modules/modules.registry.js
Ajouter le nom du module à AVAILABLE_MODULES :
export const AVAILABLE_MODULES = [
'posts',
'mon-module', // ajout
];
2. src/modules/modules.pages.js
Importer la config et l'ajouter à MODULE_CONFIGS :
import postsConfig from './posts/module.config.js';
import monModuleConfig from './mon-module/module.config.js'; // ajout
const MODULE_CONFIGS = {
posts: postsConfig,
'mon-module': monModuleConfig, // ajout
};
Activation
Le module est ignoré au démarrage tant que sa variable d'environnement n'est pas définie :
# .env.local
ZEN_MODULE_MON_MODULE=true
La règle de conversion : tirets et espaces deviennent des underscores, tout en majuscules.
mon-module → ZEN_MODULE_MON_MODULE
post-types → ZEN_MODULE_POST_TYPES
Vérification rapide
- Démarrer le serveur avec
ZEN_MODULE_MON_MODULE=true. - Ouvrir
/admin. La section de navigation du module doit apparaître. - Naviguer vers
/admin/mon-module/list. La page doit se charger. - Lancer
npx zen-db init. La tablezen_mon_moduledoit être créée.