# Modules externes `@zen/module-*` Un **module** est un package npm distinct qui ajoute des fonctionnalités à un projet construit avec `@zen/core` — sans aucune modification de code dans le projet consommateur. ```bash npm install @zen/module-billing # ajouter les variables d'env documentées dans le README du module npx zen-db init # crée les tables du module et seed ses permissions npm run dev # tout est câblé : pages admin, sidebar, widgets, API, /zen//... ``` Aucun fichier de configuration manuelle. La plateforme découvre les modules par scan des dépendances `package.json`. --- ## Découverte Au boot et au lancement de `zen-db init`, le core scanne `dependencies` + `devDependencies` du `package.json` du projet consommateur et charge tout package matchant : - **Préfixe officiel** : `@zen/module-*` - **Préfixe non-scopé** : `zen-module-*` - **Tiers** : tout package dont le `package.json` contient `"keywords": ["zen-module"]` Pour chaque module trouvé, le core vérifie qu'il exporte les bons symboles, puis l'enregistre. --- ## Forme d'un module Le point d'entrée du package (`main` ou `exports["."]`) doit exporter : ```js // @zen/module-blog/index.js export const manifest = { name: '@zen/module-blog', version: '1.0.0', permissions: [ { key: 'blog.view', name: 'Voir les billets', description: 'Consultation', group_name: 'Blog' }, { key: 'blog.manage', name: 'Gérer les billets', description: 'CRUD', group_name: 'Blog' }, ], envVars: [ { key: 'BLOG_UPLOAD_DIR', required: false, description: 'Répertoire des médias' }, ], }; export async function register() { // Tous les enregistrements runtime se font ici (voir API ci-dessous). await import('./register-server.js'); } export { createTables, dropTables } from './db.js'; ``` | Export | Type | Obligatoire | |--------|------|-------------| | `manifest` | objet (voir ci-dessous) | oui | | `register` | `() => void \| Promise` | oui | | `createTables` | `async () => { created?: string[], skipped?: string[] }` | si le module a des tables | | `dropTables` | `async () => void` | si le module a des tables | ### Manifest | Champ | Type | Description | |-------|------|-------------| | `name` | `string` | Nom du package (utilisé comme identifiant unique). | | `version` | `string` | Version du module (logguée au boot). | | `permissions` | `Array` | Permissions ajoutées au catalogue. Auto-attribuées au rôle `admin` au prochain `zen-db init`. | | `envVars` | `Array` | Variables d'env du module ; les `required: true` absentes émettent un warning au boot. | --- ## API d'enregistrement Toutes ces fonctions s'utilisent depuis le hook `register()` du module. ### Permissions Déclarées dans `manifest.permissions`. Le core les enregistre automatiquement avant le seed BD et les attribue au rôle `admin`. À la connexion, l'admin peut les distribuer à d'autres rôles via `/admin/roles`. ### Sidebar admin ```js import { registerNavSection, registerNavItem } from '@zen/core/features/admin'; registerNavSection({ id: 'blog', title: 'Blog', icon: 'Notebook01Icon', order: 40 }); registerNavItem({ id: 'blog-posts', label: 'Billets', icon: 'Notebook01Icon', href: '/admin/blog', sectionId: 'blog', permission: 'blog.view', }); ``` ### Pages admin ```js import { registerPage } from '@zen/core/features/admin'; import BlogAdminPage from './admin/BlogAdminPage.client.js'; registerPage({ slug: 'blog', Component: BlogAdminPage, title: 'Blog' }); ``` Rendue sous `/admin/blog`. ### Widgets dashboard ```js // côté serveur import { registerWidgetFetcher } from '@zen/core/features/admin'; registerWidgetFetcher('blog-posts', async () => ({ count: await countPosts() })); // côté client 'use client'; import { registerWidget } from '@zen/core/features/admin'; registerWidget({ id: 'blog-posts', Component: BlogWidget, order: 40, permission: 'blog.view' }); ``` ### Routes API ```js import { registerApiRoutes } from '@zen/core/api'; import { defineApiRoutes, apiSuccess } from '@zen/core/api'; const routes = defineApiRoutes([ { path: '/blog/posts', method: 'GET', handler: handleListPosts, auth: 'admin', permission: 'blog.view' }, { path: '/blog/posts', method: 'POST', handler: handleCreatePost, auth: 'admin', permission: 'blog.manage' }, { path: '/blog/posts/:id', method: 'GET', handler: handleGetPost, auth: 'public' }, ]); registerApiRoutes(routes); ``` Le router applique automatiquement la session, le rate-limit et la vérification de permission. Les routes sont accessibles sous `/zen/api/*`. | Champ `auth` | Comportement | |--------------|--------------| | `'public'` | Aucune session requise. | | `'user'` | Session valide. | | `'admin'` | Session avec permission `admin.access`. La permission granulaire `permission` est aussi vérifiée si fournie. | ### Pages publiques `/zen//...` ```js import { registerPublicModulePage } from '@zen/core/public-pages'; import BlogPublicPage from './public/BlogPublicPage.js'; registerPublicModulePage({ moduleName: 'blog', Component: BlogPublicPage }); ``` URL : `/zen/blog/<...>`. Le composant reçoit `{ params, segments }` : ```js function BlogPublicPage({ params, segments }) { // /zen/blog/post/abc-123 → segments = ['post', 'abc-123'] if (segments[0] === 'post') return ; return ; } ``` Le namespace `api` est réservé aux routes API et ne peut être utilisé comme `moduleName`. ### Migrations BD ```js // db.js import { query, tableExists } from '@zen/core/database'; const TABLES = [ { name: 'zen_blog_posts', sql: `CREATE TABLE zen_blog_posts (...)` }, ]; export async function createTables() { const created = []; const skipped = []; for (const t of TABLES) { if (await tableExists(t.name)) { skipped.push(t.name); continue; } await query(t.sql); created.push(t.name); } return { created, skipped }; } export async function dropTables() { for (const t of [...TABLES].reverse()) { await query(`DROP TABLE IF EXISTS "${t.name}" CASCADE`); } } ``` Convention : préfixer toutes les tables par `zen__` pour éviter les collisions. --- ## Frontières serveur/client Comme pour le core (voir [DEV.md](DEV.md)), les fichiers du module portent les suffixes `.server.js` / `.client.js`. Le hook `register()` côté serveur est appelé par le core ; les enregistrements client (widgets, par ex.) doivent être triggés par un import dans le bundle client — typiquement via le composant client lui-même qui appelle `registerWidget()` à l'import. ```js // Pattern recommandé pour un module avec partie client : // src/register-server.js (importé par register()) import './admin/BlogAdminPage.client.js'; // chaîne d'imports vers client import './widgets/BlogWidget.server.js'; // ... registerNavItem, registerPage, registerApiRoutes, etc. ``` Le bundle Next.js du projet consommateur traverse le graphe d'import et inclut les composants client dans le bundle client. Côté serveur, seules les fonctions et fetchers serveur sont chargés. --- ## Variables d'environnement Toute variable requise par le module doit être déclarée dans `manifest.envVars` et documentée dans le `README.md` du module. Les variables `required: true` absentes génèrent un warning au boot — elles ne crashent pas le serveur, le module gère son propre fallback. --- ## Squelette minimal d'un module ``` @zen/module-blog/ ├── package.json # name: "@zen/module-blog", main: "./index.js" ├── README.md # documente les env vars et la configuration ├── index.js # exporte manifest, register, createTables, dropTables └── src/ ├── db.server.js # createTables/dropTables ├── register-server.js # imports déclencheurs (chargé par register()) ├── api.server.js # routes API (registerApiRoutes) ├── admin/ │ ├── BlogAdminPage.client.js # registerPage + composant │ └── widgets/... # registerWidgetFetcher + registerWidget └── public/ └── BlogPublicPage.js # registerPublicModulePage ``` Tout le code du module vit dans `src/`. Seul `index.js` reste à la racine — c'est le point d'entrée public lu par le core via `main: "./index.js"`. Il importe depuis `./src/` : ```js // index.js export async function register() { await import('./src/register-server.js'); } export { createTables, dropTables } from './src/db.server.js'; ``` Le champ `files` dans `package.json` publie uniquement `index.js`, `src/`, `README.md` et `LICENSE` : ```json "files": ["index.js", "src", "README.md", "LICENSE"] ``` --- ## Cycle de vie complet | Étape | Côté core | Côté module | |-------|-----------|-------------| | Install | — | `npm install @zen/module-X` | | Configuration | — | Ajout des `envVars` au `.env` | | Migration BD | `zen-db init` scanne, charge le module, registerPermissions(), seed, createTables() | `createTables()` exécuté | | Boot serveur | `instrumentation.js` → `initializeZen()` scanne, charge, registerPermissions(), `register()` | `register()` exécuté côté serveur | | Premier render client | bundle client traverse les imports → composants client enregistrés | `registerWidget()` exécuté côté client | | Runtime | router dispatch les requêtes, admin résout les pages/widgets via le registre | aucun travail supplémentaire |