docs/refactor: rename getModuleMetadata and update route auth format

- 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
This commit is contained in:
2026-04-12 18:58:01 -04:00
parent c806c8d8d4
commit 3e633e981a
11 changed files with 68 additions and 38 deletions
+4 -7
View File
@@ -132,17 +132,14 @@ async function handleList(request) {
export default {
routes: [
{
path: 'mon-module/list',
method: 'GET',
handler: handleList,
requireAuth: true,
requireAdmin: false,
},
{ 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
+1 -1
View File
@@ -23,7 +23,7 @@ export {
getAllCronJobs,
getAllPublicRoutes,
getAllDatabaseSchemas,
getModuleMetadata,
getModuleMetadataGenerator,
getAllModuleMetadata,
} from './registry.js';
+1 -1
View File
@@ -13,7 +13,7 @@ import { getAvailableModules } from '../../modules/modules.registry.js';
* @returns {boolean}
*/
export function isModuleEnabledInEnv(moduleName) {
const envVar = `ZEN_MODULE_${moduleName.toUpperCase()}`;
const envVar = `ZEN_MODULE_${moduleName.toUpperCase().replace(/-/g, '_')}`;
return process.env[envVar] === 'true';
}
+1 -1
View File
@@ -26,7 +26,7 @@ export {
getAllCronJobs,
getAllPublicRoutes,
getAllDatabaseSchemas,
getModuleMetadata,
getModuleMetadataGenerator,
getAllModuleMetadata,
getModulePublicPages // returns route metadata only, use modules.pages.js for components
} from './registry.js';
+7 -3
View File
@@ -232,12 +232,16 @@ export function getAllDatabaseSchemas() {
}
/**
* Get metadata generator function from a module
* Get a specific metadata generator function from a module.
* Use this when you need to call a generator directly (e.g. for Next.js generateMetadata).
*
* To get the full metadata object for a module, use getModuleMetadata() from modules.metadata.js.
*
* @param {string} moduleName - Module name (e.g., 'invoice')
* @param {string} type - Metadata type (e.g., 'payment', 'pdf', 'receipt')
* @param {string} type - Metadata type key (e.g., 'payment', 'pdf', 'receipt')
* @returns {Function|null} Metadata generator function or null if not found
*/
export function getModuleMetadata(moduleName, type) {
export function getModuleMetadataGenerator(moduleName, type) {
const module = getModule(moduleName);
if (module?.enabled && module?.metadata) {
+8 -5
View File
@@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useEffect, useRef } from 'react';
import { ToastProvider, ToastContainer } from '@zen/core/toast';
import { registerExternalModulePages } from '../../modules/modules.pages.js';
@@ -15,13 +15,16 @@ import { registerExternalModulePages } from '../../modules/modules.pages.js';
* @param {ReactNode} props.children
*/
export function ZenProvider({ modules = [], children }) {
// Register external module pages once, synchronously, before first render.
// useState initializer runs exactly once and does not cause a re-render.
useState(() => {
const registered = useRef(false);
if (!registered.current) {
// Register synchronously on first render so pages are available
// before any child component resolves a module route.
if (modules.length > 0) {
registerExternalModulePages(modules);
}
});
registered.current = true;
}
return (
<ToastProvider>
+4 -1
View File
@@ -37,7 +37,7 @@ export {
getEnabledModules,
// Module-specific getters
getModuleMetadata,
getModuleMetadataGenerator,
getAllModuleMetadata,
getModulePublicPages,
@@ -46,6 +46,9 @@ export {
isModuleRegistered,
} from '../core/modules/index.js';
// Module metadata (server-side object getters)
export { getModuleMetadata, getMetadataGenerator } from './modules.metadata.js';
// Client-side module pages registry
export { registerExternalModulePages } from './modules.pages.js';
+18 -3
View File
@@ -9,7 +9,7 @@
* const { getInvoiceByToken } = getModuleActions('invoice');
*/
import { getModule } from '../core/modules/registry.js';
import { getModule, getEnabledModules } from '../core/modules/registry.js';
// Static actions for internal modules (add entries here for new internal modules)
export const MODULE_ACTIONS = {
@@ -43,14 +43,15 @@ export function getModuleDashboardAction(moduleName) {
}
/**
* Get all dashboard stats from all modules
* Get all dashboard stats from all modules (internal static + external runtime).
* @returns {Promise<Object>} Object with module names as keys and stats as values
*/
export async function getAllModuleDashboardStats() {
const stats = {};
// Internal modules — static action map
for (const [moduleName, getStats] of Object.entries(MODULE_DASHBOARD_ACTIONS)) {
const envKey = `ZEN_MODULE_${moduleName.toUpperCase()}`;
const envKey = `ZEN_MODULE_${moduleName.toUpperCase().replace(/-/g, '_')}`;
if (process.env[envKey] !== 'true') continue;
try {
@@ -63,6 +64,20 @@ export async function getAllModuleDashboardStats() {
}
}
// External modules — runtime registry
for (const mod of getEnabledModules()) {
if (mod.external && typeof mod.actions?.getDashboardStats === 'function') {
try {
const result = await mod.actions.getDashboardStats();
if (result.success) {
stats[mod.name] = result.stats;
}
} catch (error) {
console.error(`Error getting dashboard stats for ${mod.name}:`, error);
}
}
}
return stats;
}
+8 -2
View File
@@ -148,11 +148,17 @@ export function getAllModuleConfigs() {
}
/**
* Get all dashboard widgets from all modules
* Get all dashboard widgets from all modules (internal + external).
* @returns {Object} Object with module names as keys and arrays of lazy widgets
*/
export function getModuleDashboardWidgets() {
return MODULE_DASHBOARD_WIDGETS;
const widgets = { ...MODULE_DASHBOARD_WIDGETS };
for (const [name, config] of EXTERNAL_MODULE_CONFIGS) {
if (config.dashboardWidgets?.length) {
widgets[name] = config.dashboardWidgets;
}
}
return widgets;
}
// Legacy export for backward compatibility
+11 -10
View File
@@ -1,15 +1,16 @@
/**
* Available Modules Registry
*
* Add your module name here to enable discovery.
* See MODULES.md for the full module creation guide.
*
* Files to update when adding a module:
* 1. modules.registry.js → Add to AVAILABLE_MODULES
* 2. modules.pages.js → Import config, add to MODULE_CONFIGS
* 3. modules.actions.js → Import actions (if public pages)
* 4. modules.metadata.js → Import metadata (if SEO needed)
* 5. modules/init.js → Import createTables for database CLI
*
* Add the module name here to make it discoverable by the server.
* See docs/modules/INTERNAL_MODULE.md for the full creation guide.
*
* Required steps when adding an internal module:
* 1. modules.registry.js → Add name to AVAILABLE_MODULES (this file)
* 2. modules.pages.js → Import module.config.js, add to MODULE_CONFIGS
*
* Optional steps (only when the capability is needed):
* 3. modules.actions.js → Add to MODULE_ACTIONS (public page server actions)
* 4. modules.metadata.js → Add to MODULE_METADATA (Next.js SEO generators)
*/
export const AVAILABLE_MODULES = [
'posts',
+5 -4
View File
@@ -1,9 +1,10 @@
/**
* Posts Module Configuration
* Navigation and adminPages are generated dynamically from ZEN_MODULE_ZEN_MODULE_POSTS_TYPES env var.
* Navigation and adminPages are generated dynamically from ZEN_MODULE_POSTS_TYPES env var.
*/
import { lazy } from 'react';
import { defineModule } from '../../core/modules/defineModule.js';
import { getPostsConfig } from './config.js';
// Lazy components — shared across all post types
@@ -73,7 +74,7 @@ function pageResolver(path) {
return null;
}
export default {
export default defineModule({
name: 'posts',
displayName: 'Posts',
version: '1.0.0',
@@ -81,7 +82,7 @@ export default {
dependencies: [],
envVars: ['ZEN_MODULE_ZEN_MODULE_POSTS_TYPES'],
envVars: ['ZEN_MODULE_POSTS_TYPES'],
// Array of sections — one per post type (server-side, env vars available)
navigation: navigationSections,
@@ -95,4 +96,4 @@ export default {
publicPages: {},
publicRoutes: [],
dashboardWidgets: [],
};
});