feat(modules): add external module system with auto-discovery and public pages support

- add `src/core/modules/` with registry, discovery (server), and public index
- add `src/core/public-pages/` with registry, server component, and public index
- add `src/core/users/permissions-registry.js` for runtime permission registration
- expose `./modules`, `./public-pages`, and `./public-pages/server` package exports
- rename `registerFeatureRoutes` to `registerApiRoutes` with backward-compatible alias
- extend `seedDefaultRolesAndPermissions` to include module-registered permissions
- update `initializeZen` and shared init to wire module discovery and registration
- add `docs/MODULES.md` documenting the `@zen/module-*` authoring contract
- update `docs/DEV.md` with references to module system docs
This commit is contained in:
2026-04-25 10:50:13 -04:00
parent 3098940905
commit a3aff9fa49
23 changed files with 776 additions and 33 deletions
+59 -7
View File
@@ -1,25 +1,66 @@
/**
* Core Feature Database Initialization (CLI)
* Database initialization for features and modules.
*
* Initialise et supprime les tables des features core. La liste est aujourd'hui
* limitée à auth — le seul feature avec un schéma. Ajouter ici si un autre
* feature gagne un db.js avec createTables()/dropTables().
* - Features core : auth (et tout futur core ayant un db.js).
* - Modules externes : découverts via discoverModules() ; chaque module
* exporte ses propres createTables/dropTables.
*
* Les permissions ajoutées par les modules doivent être enregistrées AVANT
* le seed de la BD pour qu'elles soient persistées et auto-attribuées au
* rôle admin. C'est pour cela qu'on appelle register() de chaque module
* avant initFeatures().
*/
import { createTables as authCreate, dropTables as authDrop } from './auth/db.js';
import { done, fail, info, step } from '@zen/core/shared/logger';
import { discoverModules, validateModuleEnvVars } from '../core/modules/discover.server.js';
import { getRegisteredModules } from '../core/modules/registry.js';
import { registerPermissions } from '../core/users/permissions-registry.js';
const FEATURES = [
const CORE_FEATURES = [
{ name: 'auth', createTables: authCreate, dropTables: authDrop },
];
async function loadModules() {
await discoverModules();
const modules = getRegisteredModules();
validateModuleEnvVars(modules);
// Enregistre les permissions du module et exécute son register() pour que
// tous les hooks runtime soient en place avant le seed.
for (const mod of modules) {
if (Array.isArray(mod.manifest?.permissions)) {
registerPermissions(mod.manifest.permissions);
}
if (typeof mod.register === 'function') {
try {
await mod.register();
} catch (error) {
fail(`zen-modules: ${mod.manifest.name} register() threw — ${error.message}`);
}
}
}
return modules;
}
export async function initFeatures() {
const created = [];
const skipped = [];
step('Initializing feature databases...');
for (const { name, createTables } of FEATURES) {
// Charger les modules d'abord pour que leurs permissions soient connues
// au moment du seed (et donc auto-attribuées au rôle admin).
const modules = await loadModules();
const targets = [
...CORE_FEATURES,
...modules
.filter(m => typeof m.createTables === 'function')
.map(m => ({ name: m.manifest.name, createTables: m.createTables, dropTables: m.dropTables })),
];
for (const { name, createTables } of targets) {
try {
step(`Initializing ${name}...`);
if (typeof createTables !== 'function') {
@@ -40,7 +81,18 @@ export async function initFeatures() {
}
export async function dropFeatures() {
for (const { name, dropTables } of [...FEATURES].reverse()) {
const modules = await loadModules();
// Ordre de création : core, puis modules. Drop = ordre inverse pour que
// les tables modules (qui peuvent avoir des FK vers core) tombent d'abord.
const targets = [
...CORE_FEATURES,
...modules
.filter(m => typeof m.dropTables === 'function')
.map(m => ({ name: m.manifest.name, dropTables: m.dropTables })),
];
for (const { name, dropTables } of [...targets].reverse()) {
try {
if (typeof dropTables !== 'function') {
info(`${name} has no dropTables function`);