veko
Version:
Ultra-lightweight Node.js framework with ZERO dependencies - VSV components, Tailwind CSS, asset imports, SSR
538 lines (455 loc) • 15 kB
JavaScript
/**
* 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
};