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 { export default {
routes: [ routes: [
{ { path: '/admin/mon-module/list', method: 'GET', handler: handleList, auth: 'admin' },
path: 'mon-module/list', { path: '/mon-module/list', method: 'GET', handler: handleList, auth: 'public' },
method: 'GET',
handler: handleList,
requireAuth: true,
requireAdmin: false,
},
], ],
}; };
``` ```
Valeurs acceptées pour `auth` : `'admin'` (JWT admin requis), `'user'` (JWT utilisateur requis), `'public'` (aucune auth).
--- ---
## cron.config.js ## cron.config.js
+1 -1
View File
@@ -23,7 +23,7 @@ export {
getAllCronJobs, getAllCronJobs,
getAllPublicRoutes, getAllPublicRoutes,
getAllDatabaseSchemas, getAllDatabaseSchemas,
getModuleMetadata, getModuleMetadataGenerator,
getAllModuleMetadata, getAllModuleMetadata,
} from './registry.js'; } from './registry.js';
+1 -1
View File
@@ -13,7 +13,7 @@ import { getAvailableModules } from '../../modules/modules.registry.js';
* @returns {boolean} * @returns {boolean}
*/ */
export function isModuleEnabledInEnv(moduleName) { export function isModuleEnabledInEnv(moduleName) {
const envVar = `ZEN_MODULE_${moduleName.toUpperCase()}`; const envVar = `ZEN_MODULE_${moduleName.toUpperCase().replace(/-/g, '_')}`;
return process.env[envVar] === 'true'; return process.env[envVar] === 'true';
} }
+1 -1
View File
@@ -26,7 +26,7 @@ export {
getAllCronJobs, getAllCronJobs,
getAllPublicRoutes, getAllPublicRoutes,
getAllDatabaseSchemas, getAllDatabaseSchemas,
getModuleMetadata, getModuleMetadataGenerator,
getAllModuleMetadata, getAllModuleMetadata,
getModulePublicPages // returns route metadata only, use modules.pages.js for components getModulePublicPages // returns route metadata only, use modules.pages.js for components
} from './registry.js'; } 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} 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 * @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); const module = getModule(moduleName);
if (module?.enabled && module?.metadata) { if (module?.enabled && module?.metadata) {
+8 -5
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState } from 'react'; import { useEffect, useRef } from 'react';
import { ToastProvider, ToastContainer } from '@zen/core/toast'; import { ToastProvider, ToastContainer } from '@zen/core/toast';
import { registerExternalModulePages } from '../../modules/modules.pages.js'; import { registerExternalModulePages } from '../../modules/modules.pages.js';
@@ -15,13 +15,16 @@ import { registerExternalModulePages } from '../../modules/modules.pages.js';
* @param {ReactNode} props.children * @param {ReactNode} props.children
*/ */
export function ZenProvider({ modules = [], children }) { export function ZenProvider({ modules = [], children }) {
// Register external module pages once, synchronously, before first render. const registered = useRef(false);
// useState initializer runs exactly once and does not cause a re-render.
useState(() => { if (!registered.current) {
// Register synchronously on first render so pages are available
// before any child component resolves a module route.
if (modules.length > 0) { if (modules.length > 0) {
registerExternalModulePages(modules); registerExternalModulePages(modules);
} }
}); registered.current = true;
}
return ( return (
<ToastProvider> <ToastProvider>
+4 -1
View File
@@ -37,7 +37,7 @@ export {
getEnabledModules, getEnabledModules,
// Module-specific getters // Module-specific getters
getModuleMetadata, getModuleMetadataGenerator,
getAllModuleMetadata, getAllModuleMetadata,
getModulePublicPages, getModulePublicPages,
@@ -46,6 +46,9 @@ export {
isModuleRegistered, isModuleRegistered,
} from '../core/modules/index.js'; } from '../core/modules/index.js';
// Module metadata (server-side object getters)
export { getModuleMetadata, getMetadataGenerator } from './modules.metadata.js';
// Client-side module pages registry // Client-side module pages registry
export { registerExternalModulePages } from './modules.pages.js'; export { registerExternalModulePages } from './modules.pages.js';
+18 -3
View File
@@ -9,7 +9,7 @@
* const { getInvoiceByToken } = getModuleActions('invoice'); * 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) // Static actions for internal modules (add entries here for new internal modules)
export const MODULE_ACTIONS = { 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 * @returns {Promise<Object>} Object with module names as keys and stats as values
*/ */
export async function getAllModuleDashboardStats() { export async function getAllModuleDashboardStats() {
const stats = {}; const stats = {};
// Internal modules — static action map
for (const [moduleName, getStats] of Object.entries(MODULE_DASHBOARD_ACTIONS)) { 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; if (process.env[envKey] !== 'true') continue;
try { 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; 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 * @returns {Object} Object with module names as keys and arrays of lazy widgets
*/ */
export function getModuleDashboardWidgets() { 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 // Legacy export for backward compatibility
+11 -10
View File
@@ -1,15 +1,16 @@
/** /**
* Available Modules Registry * Available Modules Registry
* *
* Add your module name here to enable discovery. * Add the module name here to make it discoverable by the server.
* See MODULES.md for the full module creation guide. * See docs/modules/INTERNAL_MODULE.md for the full creation guide.
* *
* Files to update when adding a module: * Required steps when adding an internal module:
* 1. modules.registry.js → Add to AVAILABLE_MODULES * 1. modules.registry.js → Add name to AVAILABLE_MODULES (this file)
* 2. modules.pages.js → Import config, add to MODULE_CONFIGS * 2. modules.pages.js → Import module.config.js, add to MODULE_CONFIGS
* 3. modules.actions.js → Import actions (if public pages) *
* 4. modules.metadata.js → Import metadata (if SEO needed) * Optional steps (only when the capability is needed):
* 5. modules/init.js → Import createTables for database CLI * 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 = [ export const AVAILABLE_MODULES = [
'posts', 'posts',
+5 -4
View File
@@ -1,9 +1,10 @@
/** /**
* Posts Module Configuration * 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 { lazy } from 'react';
import { defineModule } from '../../core/modules/defineModule.js';
import { getPostsConfig } from './config.js'; import { getPostsConfig } from './config.js';
// Lazy components — shared across all post types // Lazy components — shared across all post types
@@ -73,7 +74,7 @@ function pageResolver(path) {
return null; return null;
} }
export default { export default defineModule({
name: 'posts', name: 'posts',
displayName: 'Posts', displayName: 'Posts',
version: '1.0.0', version: '1.0.0',
@@ -81,7 +82,7 @@ export default {
dependencies: [], 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) // Array of sections — one per post type (server-side, env vars available)
navigation: navigationSections, navigation: navigationSections,
@@ -95,4 +96,4 @@ export default {
publicPages: {}, publicPages: {},
publicRoutes: [], publicRoutes: [],
dashboardWidgets: [], dashboardWidgets: [],
}; });