UNPKG

veko

Version:

Ultra-lightweight Node.js framework with ZERO dependencies - VSV components, Tailwind CSS, asset imports, SSR

538 lines (455 loc) 15 kB
/** * Veko Module System * CMS-style modular architecture for themes and modules * * @module veko/core/module-system */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); /** * Module types */ const ModuleTypes = { THEME: 'theme', // Visual themes MODULE: 'module', // Functional modules PLUGIN: 'plugin', // Plugins WIDGET: 'widget' // Reusable widgets }; /** * Module manifest interface * @typedef {Object} ModuleManifest * @property {string} name - Module name * @property {string} version - Semantic version * @property {string} type - Module type * @property {string} [description] - Description * @property {string} [author] - Author * @property {string[]} [dependencies] - Required modules * @property {Object} [config] - Default configuration * @property {string[]} [components] - Provided components * @property {string[]} [hooks] - Provided hooks */ /** * Module Manager * Handles loading, resolving, and managing modules/themes */ class ModuleManager { constructor(vsv, options = {}) { this.vsv = vsv; this.options = { themesDir: options.themesDir || 'themes', modulesDir: options.modulesDir || 'modules', pluginsDir: options.pluginsDir || 'plugins', widgetsDir: options.widgetsDir || 'widgets', cacheDir: options.cacheDir || '.veko/modules', ...options }; // Module registries this.themes = new Map(); this.modules = new Map(); this.plugins = new Map(); this.widgets = new Map(); // Active selections this.activeTheme = null; this.activeModules = new Set(); // Component resolution cache this.componentResolution = new Map(); // Hooks system this.hooks = new Map(); } /** * Initialize module system */ async init() { await this.scanAllModules(); return this; } /** * Scan all module directories */ async scanAllModules() { const dirs = [ { dir: this.options.themesDir, type: ModuleTypes.THEME, registry: this.themes }, { dir: this.options.modulesDir, type: ModuleTypes.MODULE, registry: this.modules }, { dir: this.options.pluginsDir, type: ModuleTypes.PLUGIN, registry: this.plugins }, { dir: this.options.widgetsDir, type: ModuleTypes.WIDGET, registry: this.widgets } ]; for (const { dir, type, registry } of dirs) { const fullDir = path.join(process.cwd(), dir); if (!fs.existsSync(fullDir)) continue; const items = fs.readdirSync(fullDir, { withFileTypes: true }); for (const item of items) { if (!item.isDirectory()) continue; const modulePath = path.join(fullDir, item.name); const manifest = await this.loadManifest(modulePath, item.name, type); if (manifest) { registry.set(manifest.name, { manifest, path: modulePath, loaded: false }); } } } } /** * Load module manifest */ async loadManifest(modulePath, defaultName, type) { // Try different manifest file names const manifestFiles = ['veko.json', 'module.json', 'theme.json', 'package.json']; for (const file of manifestFiles) { const manifestPath = path.join(modulePath, file); if (!fs.existsSync(manifestPath)) continue; try { const content = fs.readFileSync(manifestPath, 'utf-8'); const manifest = JSON.parse(content); return { name: manifest.name || defaultName, version: manifest.version || '1.0.0', type: manifest.type || type, description: manifest.description || '', author: manifest.author || '', dependencies: manifest.dependencies || manifest.veko?.dependencies || [], config: manifest.config || manifest.veko?.config || {}, components: manifest.components || manifest.veko?.components || [], hooks: manifest.hooks || manifest.veko?.hooks || [], entry: manifest.main || manifest.entry || 'index.jsv', ...manifest.veko }; } catch (error) { console.warn(`Failed to parse manifest in ${modulePath}: ${error.message}`); } } // Create default manifest if none found return { name: defaultName, version: '1.0.0', type, description: '', dependencies: [], config: {}, components: this.scanComponents(modulePath), hooks: [], entry: 'index.jsv' }; } /** * Scan for components in a module directory */ scanComponents(modulePath) { const components = []; const extensions = ['.jsv', '.tsv', '.jsx', '.tsx']; const scan = (dir, prefix = '') => { if (!fs.existsSync(dir)) return; const items = fs.readdirSync(dir, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(dir, item.name); if (item.isDirectory()) { scan(fullPath, prefix + item.name + '/'); } else if (extensions.some(ext => item.name.endsWith(ext))) { const name = item.name.replace(/\.(jsv|tsv|jsx|tsx)$/, ''); components.push(prefix + name); } } }; scan(modulePath); return components; } /** * Set active theme */ async setTheme(themeName, config = {}) { if (!this.themes.has(themeName)) { throw new Error(`Theme not found: ${themeName}`); } const theme = this.themes.get(themeName); // Load theme if not already loaded if (!theme.loaded) { await this.loadModule(theme); } // Resolve dependencies await this.resolveDependencies(theme.manifest); this.activeTheme = { name: themeName, config: { ...theme.manifest.config, ...config }, module: theme }; // Clear component resolution cache this.componentResolution.clear(); return this.activeTheme; } /** * Enable a module */ async enableModule(moduleName, config = {}) { const registries = [this.modules, this.plugins, this.widgets]; let moduleInfo = null; for (const registry of registries) { if (registry.has(moduleName)) { moduleInfo = registry.get(moduleName); break; } } if (!moduleInfo) { throw new Error(`Module not found: ${moduleName}`); } // Load module if not already loaded if (!moduleInfo.loaded) { await this.loadModule(moduleInfo); } // Resolve dependencies await this.resolveDependencies(moduleInfo.manifest); this.activeModules.add({ name: moduleName, config: { ...moduleInfo.manifest.config, ...config }, module: moduleInfo }); // Clear component resolution cache this.componentResolution.clear(); return moduleInfo; } /** * Disable a module */ disableModule(moduleName) { for (const mod of this.activeModules) { if (mod.name === moduleName) { this.activeModules.delete(mod); this.componentResolution.clear(); return true; } } return false; } /** * Load a module */ async loadModule(moduleInfo) { const entryPath = path.join(moduleInfo.path, moduleInfo.manifest.entry); if (fs.existsSync(entryPath)) { // Register components with VSV const components = this.scanComponents(moduleInfo.path); for (const component of components) { const componentPath = this.findComponentFile(moduleInfo.path, component); if (componentPath) { await this.vsv.registerComponent(component, componentPath, { module: moduleInfo.manifest.name }); } } // Register hooks if (moduleInfo.manifest.hooks) { for (const hookName of moduleInfo.manifest.hooks) { this.registerHook(hookName, moduleInfo.manifest.name); } } } moduleInfo.loaded = true; moduleInfo.loadedAt = Date.now(); } /** * Find component file */ findComponentFile(basePath, componentName) { const extensions = ['.jsv', '.tsv', '.jsx', '.tsx']; const parts = componentName.split('/'); for (const ext of extensions) { const filePath = path.join(basePath, ...parts) + ext; if (fs.existsSync(filePath)) { return filePath; } // Try index file in directory const indexPath = path.join(basePath, ...parts, `index${ext}`); if (fs.existsSync(indexPath)) { return indexPath; } } return null; } /** * Resolve module dependencies */ async resolveDependencies(manifest) { if (!manifest.dependencies || manifest.dependencies.length === 0) { return; } for (const dep of manifest.dependencies) { const depName = typeof dep === 'string' ? dep : dep.name; // Check if already active const isActive = Array.from(this.activeModules).some(m => m.name === depName); if (isActive) continue; // Try to enable the dependency try { await this.enableModule(depName); } catch (error) { throw new Error(`Failed to resolve dependency ${depName} for ${manifest.name}: ${error.message}`); } } } /** * Resolve component path with module/theme override support * Priority: Theme > Active Modules (in order) > Default */ resolveComponent(componentName) { // Check cache if (this.componentResolution.has(componentName)) { return this.componentResolution.get(componentName); } let resolved = null; let source = 'default'; // Check active theme first if (this.activeTheme) { const themePath = this.findComponentFile( this.activeTheme.module.path, componentName ); if (themePath) { resolved = themePath; source = `theme:${this.activeTheme.name}`; } } // Check active modules if (!resolved) { for (const mod of this.activeModules) { const modulePath = this.findComponentFile(mod.module.path, componentName); if (modulePath) { resolved = modulePath; source = `module:${mod.name}`; break; } } } // Fall back to default components directory if (!resolved) { const defaultPath = this.findComponentFile( path.join(process.cwd(), this.vsv.options.componentsDir), componentName ); if (defaultPath) { resolved = defaultPath; source = 'default'; } } const resolution = { path: resolved, source }; this.componentResolution.set(componentName, resolution); return resolution; } /** * Get all available components */ getAllComponents() { const components = new Map(); // Add default components const defaultDir = path.join(process.cwd(), this.vsv.options.componentsDir); for (const comp of this.scanComponents(defaultDir)) { components.set(comp, { source: 'default' }); } // Add module components (may override) for (const mod of this.activeModules) { for (const comp of mod.module.manifest.components || this.scanComponents(mod.module.path)) { components.set(comp, { source: `module:${mod.name}` }); } } // Add theme components (highest priority override) if (this.activeTheme) { for (const comp of this.activeTheme.module.manifest.components || this.scanComponents(this.activeTheme.module.path)) { components.set(comp, { source: `theme:${this.activeTheme.name}` }); } } return components; } /** * Register a hook */ registerHook(hookName, moduleName) { if (!this.hooks.has(hookName)) { this.hooks.set(hookName, new Set()); } this.hooks.get(hookName).add(moduleName); } /** * Execute hook */ async executeHook(hookName, context = {}) { const handlers = this.hooks.get(hookName); if (!handlers || handlers.size === 0) return context; let result = context; for (const moduleName of handlers) { const moduleInfo = this.modules.get(moduleName) || this.plugins.get(moduleName); if (!moduleInfo) continue; const hookPath = path.join(moduleInfo.path, 'hooks', `${hookName}.js`); if (fs.existsSync(hookPath)) { try { const handler = require(hookPath); if (typeof handler === 'function') { result = await handler(result, this); } } catch (error) { console.warn(`Hook ${hookName} in ${moduleName} failed: ${error.message}`); } } } return result; } /** * Get module/theme configuration */ getConfig(moduleName) { if (this.activeTheme && this.activeTheme.name === moduleName) { return this.activeTheme.config; } for (const mod of this.activeModules) { if (mod.name === moduleName) { return mod.config; } } return null; } /** * Update module/theme configuration */ updateConfig(moduleName, config) { if (this.activeTheme && this.activeTheme.name === moduleName) { this.activeTheme.config = { ...this.activeTheme.config, ...config }; return this.activeTheme.config; } for (const mod of this.activeModules) { if (mod.name === moduleName) { mod.config = { ...mod.config, ...config }; return mod.config; } } return null; } /** * Get module status summary */ getStatus() { return { themes: { available: Array.from(this.themes.keys()), active: this.activeTheme?.name || null }, modules: { available: Array.from(this.modules.keys()), active: Array.from(this.activeModules).map(m => m.name) }, plugins: { available: Array.from(this.plugins.keys()), active: Array.from(this.activeModules) .filter(m => this.plugins.has(m.name)) .map(m => m.name) }, widgets: { available: Array.from(this.widgets.keys()) }, hooks: Array.from(this.hooks.keys()) }; } } module.exports = { ModuleTypes, ModuleManager };