Files
core/docs/modules/INTERNAL_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

5.2 KiB

Créer un module interne

Un module interne vit dans src/modules/ et fait partie du package @hykocx/zen. 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: 'mon-module/list',
      method: 'GET',
      handler: handleList,
      requireAuth: true,
      requireAdmin: false,
    },
  ],
};

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

  1. Démarrer le serveur avec ZEN_MODULE_MON_MODULE=true.
  2. Ouvrir /admin. La section de navigation du module doit apparaître.
  3. Naviguer vers /admin/mon-module/list. La page doit se charger.
  4. Lancer npx zen-db init. La table zen_mon_module doit être créée.