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:
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* defineModule — helper to declare a ZEN module.
|
||||
*
|
||||
* Used for both internal modules (src/modules/) and external npm packages.
|
||||
*
|
||||
* @param {Object} config - Module configuration
|
||||
* @returns {Object} Normalized module configuration
|
||||
*/
|
||||
export function defineModule(config) {
|
||||
if (!config || typeof config !== 'object') {
|
||||
throw new Error('[defineModule] Config must be an object.');
|
||||
}
|
||||
|
||||
if (!config.name || typeof config.name !== 'string') {
|
||||
throw new Error('[defineModule] Field "name" is required (e.g. "invoice").');
|
||||
}
|
||||
|
||||
return {
|
||||
// Identity
|
||||
version: '1.0.0',
|
||||
displayName: config.name.charAt(0).toUpperCase() + config.name.slice(1),
|
||||
description: '',
|
||||
|
||||
// Dependencies and environment variables
|
||||
dependencies: [],
|
||||
envVars: [],
|
||||
|
||||
// Admin UI
|
||||
navigation: null,
|
||||
adminPages: {},
|
||||
pageResolver: null,
|
||||
|
||||
// Public pages
|
||||
publicPages: {},
|
||||
publicRoutes: [],
|
||||
dashboardWidgets: [],
|
||||
|
||||
// Server actions for public pages
|
||||
actions: {},
|
||||
|
||||
// SEO metadata generators
|
||||
metadata: {},
|
||||
|
||||
// Database (optional) — { createTables, dropTables }
|
||||
db: null,
|
||||
|
||||
// Initialization callback (optional) — setup(ctx)
|
||||
setup: null,
|
||||
|
||||
// Spread last so all fields above can be overridden
|
||||
...config,
|
||||
|
||||
// Internal marker — do not override
|
||||
__isZenModule: true,
|
||||
};
|
||||
}
|
||||
+134
-28
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Module Discovery System
|
||||
* Auto-discovers and registers modules from the modules directory
|
||||
* Auto-discovers and registers modules from the modules directory.
|
||||
* Also handles registration of external modules passed via zen.config.js.
|
||||
*/
|
||||
|
||||
import { registerModule, clearRegistry } from './registry.js';
|
||||
@@ -95,27 +96,7 @@ async function loadModuleConfig(moduleName) {
|
||||
try {
|
||||
const config = await import(`../../modules/${moduleName}/module.config.js`);
|
||||
const moduleConfig = config.default || config;
|
||||
|
||||
// Build admin config with navigation and pages
|
||||
let adminConfig = undefined;
|
||||
if (moduleConfig.navigation || moduleConfig.adminPages) {
|
||||
adminConfig = {};
|
||||
if (moduleConfig.navigation) {
|
||||
adminConfig.navigation = moduleConfig.navigation;
|
||||
}
|
||||
// Extract admin page paths (keys only, not the lazy components)
|
||||
// This allows getAdminPage() to know which paths belong to this module
|
||||
if (moduleConfig.adminPages) {
|
||||
adminConfig.pages = {};
|
||||
for (const path of Object.keys(moduleConfig.adminPages)) {
|
||||
// Store true as a marker that this path exists
|
||||
// The actual component is loaded client-side via modules.pages.js
|
||||
adminConfig.pages[path] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract server-side relevant data
|
||||
|
||||
return {
|
||||
name: moduleConfig.name || moduleName,
|
||||
displayName: moduleConfig.displayName || moduleName.charAt(0).toUpperCase() + moduleName.slice(1),
|
||||
@@ -123,12 +104,12 @@ async function loadModuleConfig(moduleName) {
|
||||
description: moduleConfig.description || `${moduleName} module`,
|
||||
dependencies: moduleConfig.dependencies || [],
|
||||
envVars: moduleConfig.envVars || [],
|
||||
// Admin configuration (navigation + page paths)
|
||||
admin: adminConfig,
|
||||
// Public routes metadata (not components)
|
||||
public: moduleConfig.publicRoutes ? {
|
||||
routes: moduleConfig.publicRoutes
|
||||
} : undefined,
|
||||
// Admin config: navigation + page path markers (components loaded client-side)
|
||||
admin: buildAdminConfig(moduleConfig),
|
||||
// Public routes metadata (components loaded client-side)
|
||||
public: moduleConfig.publicRoutes?.length
|
||||
? { routes: moduleConfig.publicRoutes }
|
||||
: undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(`[Module Discovery] No module.config.js for ${moduleName}, using defaults`);
|
||||
@@ -182,6 +163,131 @@ async function loadModuleComponents(moduleName) {
|
||||
return components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register external modules provided via zen.config.js.
|
||||
* Skips any module whose ZEN_MODULE_<NAME>=true env var is not set.
|
||||
*
|
||||
* @param {Array} modules - Array of module configs created with defineModule()
|
||||
* @returns {Promise<Object>} { registered, skipped, errors }
|
||||
*/
|
||||
export async function registerExternalModules(modules = []) {
|
||||
const registered = [];
|
||||
const skipped = [];
|
||||
const errors = [];
|
||||
|
||||
for (const moduleConfig of modules) {
|
||||
const moduleName = moduleConfig?.name;
|
||||
|
||||
if (!moduleName) {
|
||||
errors.push({ module: '(unknown)', error: 'Missing "name" field.' });
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isModuleEnabledInEnv(moduleName)) {
|
||||
skipped.push(moduleName);
|
||||
console.log(`[External Modules] Skipped ${moduleName} (not enabled)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build registry entry from the external module config
|
||||
const adminConfig = buildAdminConfig(moduleConfig);
|
||||
|
||||
const moduleData = {
|
||||
name: moduleName,
|
||||
displayName: moduleConfig.displayName || moduleName,
|
||||
version: moduleConfig.version || '1.0.0',
|
||||
description: moduleConfig.description || '',
|
||||
dependencies: moduleConfig.dependencies || [],
|
||||
envVars: moduleConfig.envVars || [],
|
||||
admin: adminConfig,
|
||||
public: moduleConfig.publicRoutes?.length
|
||||
? { routes: moduleConfig.publicRoutes }
|
||||
: undefined,
|
||||
actions: moduleConfig.actions || {},
|
||||
metadata: moduleConfig.metadata || {},
|
||||
db: moduleConfig.db
|
||||
? { init: moduleConfig.db.createTables, drop: moduleConfig.db.dropTables }
|
||||
: undefined,
|
||||
cron: moduleConfig.cron || undefined,
|
||||
api: moduleConfig.api || undefined,
|
||||
enabled: true,
|
||||
external: true,
|
||||
};
|
||||
|
||||
registerModule(moduleName, moduleData);
|
||||
|
||||
// Call setup(ctx) if provided
|
||||
if (typeof moduleConfig.setup === 'function') {
|
||||
const ctx = await buildModuleContext();
|
||||
await moduleConfig.setup(ctx);
|
||||
}
|
||||
|
||||
registered.push(moduleName);
|
||||
console.log(`[External Modules] Registered ${moduleName}`);
|
||||
} catch (error) {
|
||||
errors.push({ module: moduleName, error: error.message });
|
||||
console.error(`[External Modules] Error registering ${moduleName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (registered.length > 0 || skipped.length > 0) {
|
||||
console.log(
|
||||
`[External Modules] Done. Registered: ${registered.length}, Skipped: ${skipped.length}, Errors: ${errors.length}`
|
||||
);
|
||||
}
|
||||
|
||||
return { registered, skipped, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build admin config object from a module config (shared with loadModuleConfig).
|
||||
* @param {Object} moduleConfig
|
||||
* @returns {Object|undefined}
|
||||
*/
|
||||
function buildAdminConfig(moduleConfig) {
|
||||
if (!moduleConfig.navigation && !moduleConfig.adminPages) return undefined;
|
||||
|
||||
const adminConfig = {};
|
||||
|
||||
if (moduleConfig.navigation) {
|
||||
adminConfig.navigation = moduleConfig.navigation;
|
||||
}
|
||||
|
||||
if (moduleConfig.adminPages) {
|
||||
adminConfig.pages = {};
|
||||
for (const path of Object.keys(moduleConfig.adminPages)) {
|
||||
adminConfig.pages[path] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return adminConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the context object injected into module setup() callbacks.
|
||||
* All services are lazy-initialized internally — importing them is safe.
|
||||
* @returns {Promise<Object>} ctx
|
||||
*/
|
||||
async function buildModuleContext() {
|
||||
const [db, email, storage, payments] = await Promise.all([
|
||||
import('../../core/database/index.js'),
|
||||
import('../../core/email/index.js'),
|
||||
import('../../core/storage/index.js'),
|
||||
import('../../core/payments/index.js'),
|
||||
]);
|
||||
|
||||
return {
|
||||
db,
|
||||
email,
|
||||
storage,
|
||||
payments,
|
||||
config: {
|
||||
get: (key) => process.env[key],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset module discovery (useful for testing)
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// Discovery
|
||||
export {
|
||||
discoverModules,
|
||||
registerExternalModules,
|
||||
isModuleEnabledInEnv,
|
||||
resetDiscovery
|
||||
} from './discovery.js';
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ToastProvider, ToastContainer } from '@hykocx/zen/toast';
|
||||
import { registerExternalModulePages } from '../../modules/modules.pages.js';
|
||||
|
||||
export function ZenProvider({ children }) {
|
||||
return (
|
||||
<ToastProvider>
|
||||
{children}
|
||||
<ToastContainer />
|
||||
</ToastProvider>
|
||||
);
|
||||
/**
|
||||
* ZenProvider — root client provider for the ZEN CMS.
|
||||
*
|
||||
* Pass external module configs via the `modules` prop so their
|
||||
* admin pages and public pages are available to the client router.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Array} props.modules - External module configs from zen.config.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(() => {
|
||||
if (modules.length > 0) {
|
||||
registerExternalModulePages(modules);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{children}
|
||||
<ToastContainer />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+32
-18
@@ -3,7 +3,7 @@
|
||||
* Initialize all ZEN services and modules using dynamic module discovery
|
||||
*/
|
||||
|
||||
import { discoverModules, startModuleCronJobs, stopModuleCronJobs } from '../../core/modules/index.js';
|
||||
import { discoverModules, registerExternalModules, startModuleCronJobs, stopModuleCronJobs } from '../../core/modules/index.js';
|
||||
|
||||
// Use globalThis to persist initialization flag across module reloads
|
||||
const ZEN_INIT_KEY = Symbol.for('__ZEN_INITIALIZED__');
|
||||
@@ -16,26 +16,32 @@ const ZEN_INIT_KEY = Symbol.for('__ZEN_INITIALIZED__');
|
||||
* Alternative: Call this function manually in your root layout
|
||||
*
|
||||
* @example
|
||||
* // instrumentation.js (Recommended)
|
||||
* // instrumentation.js (Recommended) — internal modules only
|
||||
* export async function register() {
|
||||
* if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
* const { initializeZen } = await import('@hykocx/zen');
|
||||
* await initializeZen();
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // app/layout.js (Alternative)
|
||||
* import { initializeZen } from '@hykocx/zen';
|
||||
* initializeZen();
|
||||
*
|
||||
* @param {Object} options - Initialization options
|
||||
* @param {boolean} options.skipCron - Skip cron job initialization
|
||||
* @param {boolean} options.skipDb - Skip database initialization
|
||||
* // instrumentation.js — with external modules from zen.config.js
|
||||
* import zenConfig from './zen.config.js';
|
||||
* export async function register() {
|
||||
* if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
* const { initializeZen } = await import('@hykocx/zen');
|
||||
* await initializeZen(zenConfig);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {Array} config.modules - External module configs (from zen.config.js)
|
||||
* @param {boolean} config.skipCron - Skip cron job initialization
|
||||
* @param {boolean} config.skipDb - Skip database initialization
|
||||
* @returns {Promise<Object>} Initialization result
|
||||
*/
|
||||
export async function initializeZen(options = {}) {
|
||||
const { skipCron = false, skipDb = true } = options;
|
||||
export async function initializeZen(config = {}) {
|
||||
const { modules: externalModules = [], skipCron = false, skipDb = true } = config;
|
||||
|
||||
// Only run on server-side
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -57,21 +63,29 @@ export async function initializeZen(options = {}) {
|
||||
};
|
||||
|
||||
try {
|
||||
// Step 1: Discover and register all enabled modules
|
||||
// This reads from modules.registry.js and loads each module's config files
|
||||
// Step 1: Discover and register internal modules (from modules.registry.js)
|
||||
result.discovery = await discoverModules();
|
||||
|
||||
|
||||
const enabledCount = result.discovery.enabled?.length || 0;
|
||||
const skippedCount = result.discovery.skipped?.length || 0;
|
||||
|
||||
|
||||
if (enabledCount > 0) {
|
||||
console.log(`✓ ZEN: Discovered ${enabledCount} enabled module(s): ${result.discovery.enabled.join(', ')}`);
|
||||
console.log(`✓ ZEN: Discovered ${enabledCount} internal module(s): ${result.discovery.enabled.join(', ')}`);
|
||||
}
|
||||
if (skippedCount > 0) {
|
||||
console.log(`⚠ ZEN: Skipped ${skippedCount} disabled module(s): ${result.discovery.skipped.join(', ')}`);
|
||||
}
|
||||
|
||||
// Step 2: Register external modules from zen.config.js (if any)
|
||||
if (externalModules.length > 0) {
|
||||
result.external = await registerExternalModules(externalModules);
|
||||
|
||||
if (result.external.registered.length > 0) {
|
||||
console.log(`✓ ZEN: Registered ${result.external.registered.length} external module(s): ${result.external.registered.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Start cron jobs for all enabled modules
|
||||
// Step 3: Start cron jobs for all enabled modules (internal + external)
|
||||
if (!skipCron) {
|
||||
result.cron = await startModuleCronJobs();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user