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
This commit is contained in:
@@ -18,6 +18,7 @@ export {
|
||||
export {
|
||||
// Discovery & initialization
|
||||
discoverModules,
|
||||
registerExternalModules,
|
||||
initializeModules,
|
||||
initializeModuleDatabases,
|
||||
startModuleCronJobs,
|
||||
@@ -45,5 +46,8 @@ export {
|
||||
isModuleRegistered,
|
||||
} from '../core/modules/index.js';
|
||||
|
||||
// Client-side module pages registry
|
||||
export { registerExternalModulePages } from './modules.pages.js';
|
||||
|
||||
// Public pages system
|
||||
export * from './pages.js';
|
||||
|
||||
+27
-37
@@ -1,32 +1,20 @@
|
||||
/**
|
||||
* Module Database Initialization
|
||||
* Initializes enabled module database tables
|
||||
*
|
||||
* IMPORTANT: When creating a new module, add its createTables import below
|
||||
* and add it to MODULE_DB_INITIALIZERS.
|
||||
*/
|
||||
|
||||
// Import createTables functions from each module
|
||||
// These are bundled together so they're available at runtime
|
||||
import { createTables as createPostsTables } from './posts/db.js';
|
||||
|
||||
/**
|
||||
* Module database initializers
|
||||
* Maps module names to their createTables functions
|
||||
* Module Database Initialization (CLI)
|
||||
*
|
||||
* Add new modules here:
|
||||
* Initializes DB tables for each enabled module.
|
||||
* Modules are auto-discovered from AVAILABLE_MODULES —
|
||||
* no manual registration needed when adding a new module.
|
||||
*/
|
||||
const MODULE_DB_INITIALIZERS = {
|
||||
posts: createPostsTables,
|
||||
};
|
||||
|
||||
import { AVAILABLE_MODULES } from './modules.registry.js';
|
||||
|
||||
/**
|
||||
* Check if a module is enabled in the environment
|
||||
* Check if a module is enabled via environment variable
|
||||
* @param {string} moduleName - Module name
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isModuleEnabled(moduleName) {
|
||||
const envKey = `ZEN_MODULE_${moduleName.toUpperCase()}`;
|
||||
const envKey = `ZEN_MODULE_${moduleName.toUpperCase().replace(/-/g, '_')}`;
|
||||
return process.env[envKey] === 'true';
|
||||
}
|
||||
|
||||
@@ -37,36 +25,38 @@ function isModuleEnabled(moduleName) {
|
||||
export async function initModules() {
|
||||
const created = [];
|
||||
const skipped = [];
|
||||
|
||||
|
||||
console.log('\nInitializing module databases...');
|
||||
|
||||
for (const [moduleName, createTables] of Object.entries(MODULE_DB_INITIALIZERS)) {
|
||||
|
||||
for (const moduleName of AVAILABLE_MODULES) {
|
||||
if (!isModuleEnabled(moduleName)) {
|
||||
console.log(`- Skipped ${moduleName} (not enabled)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
if (typeof createTables === 'function') {
|
||||
console.log(`\nInitializing ${moduleName} module tables...`);
|
||||
const result = await createTables();
|
||||
|
||||
if (result?.created) {
|
||||
created.push(...result.created);
|
||||
}
|
||||
if (result?.skipped) {
|
||||
skipped.push(...result.skipped);
|
||||
}
|
||||
|
||||
console.log(`\nInitializing ${moduleName} module tables...`);
|
||||
const db = await import(`./${moduleName}/db.js`);
|
||||
|
||||
if (typeof db.createTables === 'function') {
|
||||
const result = await db.createTables();
|
||||
|
||||
if (result?.created) created.push(...result.created);
|
||||
if (result?.skipped) skipped.push(...result.skipped);
|
||||
|
||||
console.log(`✓ ${moduleName} module initialized`);
|
||||
} else {
|
||||
console.log(`- ${moduleName} has no createTables function`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error initializing ${moduleName}:`, error.message);
|
||||
if (error.code === 'ERR_MODULE_NOT_FOUND' || error.message?.includes('Cannot find module')) {
|
||||
console.log(`- ${moduleName} has no db.js (skipped)`);
|
||||
} else {
|
||||
console.error(`Error initializing ${moduleName}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return { created, skipped };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
/**
|
||||
* Module Actions Registry (Server-Side)
|
||||
*
|
||||
* Import server actions for public pages (/zen/*) and dashboard.
|
||||
* Admin pages import actions directly from modules.
|
||||
* See modules.registry.js for full module creation guide.
|
||||
*
|
||||
* Usage in consuming Next.js app:
|
||||
* ```
|
||||
* import { MODULE_ACTIONS, MODULE_DASHBOARD_ACTIONS } from '@hykocx/zen/modules/actions';
|
||||
*
|
||||
* // Access module actions
|
||||
* const { getInvoiceByTokenAction } = MODULE_ACTIONS.invoice;
|
||||
*
|
||||
* // Get dashboard stats
|
||||
* const stats = await MODULE_DASHBOARD_ACTIONS.invoice();
|
||||
* ```
|
||||
*
|
||||
* Static registry for internal module server actions.
|
||||
* External modules registered via zen.config.js are resolved through the runtime registry.
|
||||
*
|
||||
* Usage:
|
||||
* import { getModuleActions } from '@hykocx/zen/modules/actions';
|
||||
* const { getInvoiceByToken } = getModuleActions('invoice');
|
||||
*/
|
||||
|
||||
// Register module actions (for public pages)
|
||||
import { getModule } from '../core/modules/registry.js';
|
||||
|
||||
// Static actions for internal modules (add entries here for new internal modules)
|
||||
export const MODULE_ACTIONS = {
|
||||
posts: {},
|
||||
};
|
||||
|
||||
// Register dashboard stats actions (for admin dashboard)
|
||||
// Static dashboard stats actions for internal modules
|
||||
export const MODULE_DASHBOARD_ACTIONS = {};
|
||||
|
||||
/**
|
||||
* Get actions for a specific module
|
||||
* Get actions for a specific module.
|
||||
* Checks the static registry first, then the runtime registry for external modules.
|
||||
*
|
||||
* @param {string} moduleName - Module name
|
||||
* @returns {Object} Module actions object or empty object
|
||||
*/
|
||||
export function getModuleActions(moduleName) {
|
||||
return MODULE_ACTIONS[moduleName] || {};
|
||||
if (MODULE_ACTIONS[moduleName]) return MODULE_ACTIONS[moduleName];
|
||||
|
||||
// External modules declare their actions in their defineModule() config
|
||||
return getModule(moduleName)?.actions ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,51 +1,57 @@
|
||||
/**
|
||||
* Module Metadata Registry (Server-Side)
|
||||
*
|
||||
* Import metadata generators from modules for SEO/dynamic metadata.
|
||||
* See modules.registry.js for full module creation guide.
|
||||
*
|
||||
* Usage in Next.js page.js:
|
||||
* ```
|
||||
* import { MODULE_METADATA } from '@hykocx/zen/modules/metadata';
|
||||
*
|
||||
* export async function generateMetadata({ params }) {
|
||||
* return await MODULE_METADATA.invoice.generateInvoicePaymentMetadata(params.token);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Static registry for internal module SEO metadata generators.
|
||||
* External modules registered via zen.config.js are resolved through the runtime registry.
|
||||
*
|
||||
* Usage:
|
||||
* import { getMetadataGenerator } from '@hykocx/zen/modules/metadata';
|
||||
* const fn = getMetadataGenerator('invoice', 'payment');
|
||||
* const meta = await fn(params.token);
|
||||
*/
|
||||
|
||||
// Register module metadata
|
||||
import { getModule } from '../core/modules/registry.js';
|
||||
|
||||
// Static metadata for internal modules (add entries here for new internal modules)
|
||||
export const MODULE_METADATA = {};
|
||||
|
||||
/**
|
||||
* Get metadata generators for a specific module
|
||||
* Get metadata generators for a specific module.
|
||||
* Checks the static registry first, then the runtime registry for external modules.
|
||||
*
|
||||
* @param {string} moduleName - Module name
|
||||
* @returns {Object|null} Module metadata object or null
|
||||
*/
|
||||
export function getModuleMetadata(moduleName) {
|
||||
return MODULE_METADATA[moduleName] || null;
|
||||
return MODULE_METADATA[moduleName] || getModule(moduleName)?.metadata || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific metadata generator function
|
||||
* Get a specific metadata generator function.
|
||||
* Checks the static registry first, then the runtime registry for external modules.
|
||||
*
|
||||
* @param {string} moduleName - Module name
|
||||
* @param {string} generatorName - Name of the metadata generator function (e.g., 'payment', 'pdf', 'receipt')
|
||||
* @param {string} generatorName - Metadata generator key (e.g., 'payment', 'pdf')
|
||||
* @returns {Function|null} Metadata generator function or null
|
||||
*/
|
||||
export function getMetadataGenerator(moduleName, generatorName) {
|
||||
const metadata = MODULE_METADATA[moduleName];
|
||||
if (!metadata) return null;
|
||||
|
||||
// Check the default export first (where the route type mapping is)
|
||||
if (metadata.default && typeof metadata.default[generatorName] === 'function') {
|
||||
return metadata.default[generatorName];
|
||||
|
||||
if (metadata) {
|
||||
if (metadata.default && typeof metadata.default[generatorName] === 'function') {
|
||||
return metadata.default[generatorName];
|
||||
}
|
||||
if (typeof metadata[generatorName] === 'function') {
|
||||
return metadata[generatorName];
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to direct named export
|
||||
if (typeof metadata[generatorName] === 'function') {
|
||||
return metadata[generatorName];
|
||||
|
||||
// External modules declare metadata in their defineModule() config
|
||||
const externalMetadata = getModule(moduleName)?.metadata;
|
||||
if (externalMetadata && typeof externalMetadata[generatorName] === 'function') {
|
||||
return externalMetadata[generatorName];
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,41 @@
|
||||
|
||||
/**
|
||||
* Module Pages Registry (Client-Side)
|
||||
*
|
||||
* Import module configs and register them here.
|
||||
* See modules.registry.js for full module creation guide.
|
||||
*
|
||||
* Static registry for internal modules (imported explicitly for proper code splitting).
|
||||
* External modules are registered at runtime via registerExternalModulePages().
|
||||
*
|
||||
* To add an internal module:
|
||||
* 1. Import its config below
|
||||
* 2. Add it to MODULE_CONFIGS
|
||||
*/
|
||||
|
||||
// Import module configs
|
||||
// Import module configs — add new internal modules here
|
||||
import postsConfig from './posts/module.config.js';
|
||||
|
||||
// Register module configs
|
||||
// Internal module configs — add new modules here
|
||||
const MODULE_CONFIGS = {
|
||||
posts: postsConfig,
|
||||
};
|
||||
|
||||
// Runtime registry for external modules (populated by ZenProvider via registerExternalModulePages)
|
||||
const EXTERNAL_MODULE_CONFIGS = new Map();
|
||||
|
||||
/**
|
||||
* Register external module configs at runtime.
|
||||
* Called by ZenProvider when the app starts.
|
||||
* Idempotent — safe to call multiple times with the same modules.
|
||||
*
|
||||
* @param {Array} modules - Array of module configs created with defineModule()
|
||||
*/
|
||||
export function registerExternalModulePages(modules = []) {
|
||||
for (const mod of modules) {
|
||||
if (mod?.name) {
|
||||
EXTERNAL_MODULE_CONFIGS.set(mod.name, mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build admin pages from all module configs
|
||||
*/
|
||||
@@ -46,13 +68,27 @@ export const MODULE_DASHBOARD_WIDGETS = Object.entries(MODULE_CONFIGS).reduce((a
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* Get admin page loader for a specific module and path
|
||||
* Get admin page loader for a specific module and path.
|
||||
* Checks external modules first, then internal modules.
|
||||
*
|
||||
* @param {string} moduleName - Module name
|
||||
* @param {string} path - Admin path
|
||||
* @returns {React.LazyExoticComponent|null} Lazy component or null
|
||||
*/
|
||||
export function getModulePageLoader(moduleName, path) {
|
||||
// Use custom resolver first (for dynamic paths not known at build time)
|
||||
// Check external modules first
|
||||
const externalConfig = EXTERNAL_MODULE_CONFIGS.get(moduleName);
|
||||
if (externalConfig) {
|
||||
if (typeof externalConfig.pageResolver === 'function') {
|
||||
const resolved = externalConfig.pageResolver(path);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
if (externalConfig.adminPages?.[path]) {
|
||||
return externalConfig.adminPages[path];
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to internal modules
|
||||
const config = MODULE_CONFIGS[moduleName];
|
||||
if (config?.pageResolver) {
|
||||
const resolved = config.pageResolver(path);
|
||||
@@ -60,31 +96,45 @@ export function getModulePageLoader(moduleName, path) {
|
||||
}
|
||||
|
||||
const modulePages = MODULE_ADMIN_PAGES[moduleName];
|
||||
if (modulePages && modulePages[path]) {
|
||||
if (modulePages?.[path]) {
|
||||
return modulePages[path];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public page loader for a specific module
|
||||
* Get public page loader for a specific module.
|
||||
* Checks external modules first, then internal modules.
|
||||
*
|
||||
* @param {string} moduleName - Module name
|
||||
* @returns {React.LazyExoticComponent|null} Lazy component or null
|
||||
*/
|
||||
export function getModulePublicPageLoader(moduleName) {
|
||||
const externalConfig = EXTERNAL_MODULE_CONFIGS.get(moduleName);
|
||||
if (externalConfig?.publicPages?.default) {
|
||||
return externalConfig.publicPages.default;
|
||||
}
|
||||
|
||||
const modulePages = MODULE_PUBLIC_PAGES[moduleName];
|
||||
if (modulePages?.default) {
|
||||
return modulePages.default;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get module navigation config
|
||||
* Get module navigation config.
|
||||
* Checks external modules first, then internal modules.
|
||||
*
|
||||
* @param {string} moduleName - Module name
|
||||
* @returns {Object|null} Navigation config or null
|
||||
*/
|
||||
export function getModuleNavigation(moduleName) {
|
||||
const externalConfig = EXTERNAL_MODULE_CONFIGS.get(moduleName);
|
||||
if (externalConfig?.navigation) return externalConfig.navigation;
|
||||
|
||||
const config = MODULE_CONFIGS[moduleName];
|
||||
return config?.navigation || null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user