UNPKG

@dbs-portal/core-module-registry

Version:

Core module registry system for automatic module discovery and registration

409 lines 15.5 kB
/** * Module Discovery Service * * Orchestrates multiple discovery strategies to automatically find and load * modules from various sources (packages, configs, runtime registration). */ import { ModuleStatus, validateModuleMetadata } from '../types/ModuleMetadata'; import { getPath, getFs, getGlob, createPlatformError, getPlatformInfo } from '../utils/platform-modules'; import { createLogger, ENV, hasFeature } from '../utils/environment'; export class ModuleDiscoveryService { options; runtimeModules = []; logger = createLogger('discovery'); platformInfo = getPlatformInfo(); constructor(options = {}) { this.options = { packageScanPaths: ['packages/modules/*', 'packages/core/*'], configScanPaths: ['packages/modules/*', 'src/pages/modules/*'], packageNamePattern: '@dbs-portal/module-*', configFilePattern: 'module.config.{ts,js}', enablePackageScanning: true, enableConfigDetection: true, enableRuntimeRegistration: true, validateModules: true, ignorePatterns: ['node_modules/**', 'dist/**', '**/*.test.*', '**/*.spec.*'], ...options }; this.logger.debug('ModuleDiscoveryService initialized', { platform: this.platformInfo.platform, features: this.platformInfo.features, options: this.options }); } /** * Discover modules from all enabled sources */ async discoverModules() { const startTime = Date.now(); const errors = []; const warnings = []; const allModules = []; try { // Run discovery strategies in parallel const discoveryPromises = []; this.logger.debug("Starting module discovery: ", startTime); if (this.options.enablePackageScanning) { discoveryPromises.push(this.discoverFromPackages().catch(error => { errors.push({ source: 'package-scanning', message: error.message, details: error }); return []; })); } if (this.options.enableConfigDetection) { discoveryPromises.push(this.discoverFromConfigs().catch(error => { errors.push({ source: 'config-detection', message: error.message, details: error }); return []; })); } if (this.options.enableRuntimeRegistration) { discoveryPromises.push(this.discoverFromRuntime().catch(error => { errors.push({ source: 'runtime-registration', message: error.message, details: error }); return []; })); } const results = await Promise.all(discoveryPromises); // Flatten and merge results results.forEach(modules => allModules.push(...modules)); // Remove duplicates (prefer first occurrence) const uniqueModules = this.deduplicateModules(allModules); // Validate modules if enabled if (this.options.validateModules) { return this.validateDiscoveredModules(uniqueModules, errors, warnings); } this.logger.info(`Module discovery completed, found ${uniqueModules.length} modules`, { duration: Date.now() - startTime, modules: uniqueModules.map(m => m.id) }); return uniqueModules; } catch (error) { console.error('Module discovery failed:', error); throw error; } } /** * Discover modules from package.json files */ async discoverFromPackages() { if (!hasFeature('canScanPackages')) { this.logger.warn('Package scanning not available in current environment'); return []; } const modules = []; try { this.logger.debug('Starting package scanning', { paths: this.options.packageScanPaths }); const packageCandidates = await this.scanWorkspacePackages(); for (const candidate of packageCandidates) { try { const module = await this.loadModuleFromPackage(candidate); if (module) { modules.push(module); this.logger.debug(`Loaded module from package: ${module.id}`); } } catch (error) { this.logger.warn(`Failed to load module from package ${candidate.packageName}:`, error); } } } catch (error) { const platformError = createPlatformError('package-scanning', error, [ 'Check that package scan paths are correct', 'Ensure packages have valid package.json files', 'Verify file system permissions' ]); this.logger.error('Package scanning failed:', platformError); throw platformError; } this.logger.info(`Package scanning completed, found ${modules.length} modules`); return modules; } /** * Discover modules from module.config.ts files */ async discoverFromConfigs() { const modules = []; try { const configFiles = await this.findConfigFiles(); for (const configFile of configFiles) { try { const module = await this.loadModuleFromConfig(configFile); if (module) { modules.push(module); } } catch (error) { console.warn(`Failed to load module from config ${configFile}:`, error); } } } catch (error) { console.error('Config detection failed:', error); throw error; } return modules; } /** * Get modules registered at runtime */ async discoverFromRuntime() { return [...this.runtimeModules]; } /** * Register a module at runtime */ registerRuntimeModule(module) { this.runtimeModules.push(module); } /** * Scan workspace for packages matching the pattern */ async scanWorkspacePackages() { const candidates = []; try { const [path, glob] = await Promise.all([getPath(), getGlob()]); const cwd = ENV.isNode ? process.cwd() : '/'; // Read root package.json to get workspace configuration const rootPackageJson = await this.readPackageJson(cwd); const workspaces = rootPackageJson.workspaces || []; // Scan each workspace path for (const workspacePath of workspaces) { const packagePaths = await glob.glob(`${workspacePath}/package.json`, { cwd, ignore: this.options.ignorePatterns }); for (const packagePath of packagePaths) { try { const fullPath = path.resolve(cwd, packagePath); const packageJson = await this.readPackageJson(path.dirname(fullPath)); // Check if package matches module pattern if (this.isModulePackage(packageJson)) { candidates.push({ id: this.extractModuleId(packageJson.name), packageName: packageJson.name, version: packageJson.version, path: path.dirname(fullPath), packageJson }); } } catch (error) { this.logger.warn(`Failed to read package at ${packagePath}:`, error); } } } } catch (error) { console.error('Workspace scanning failed:', error); throw error; } return candidates; } /** * Find module configuration files */ async findConfigFiles() { if (!hasFeature('canScanFileSystem')) { this.logger.warn('Config file scanning not available in current environment'); return []; } const configFiles = []; try { const [path, glob] = await Promise.all([getPath(), getGlob()]); const cwd = ENV.isNode ? process.cwd() : '/'; for (const scanPath of this.options.configScanPaths) { const pattern = path.join(scanPath, this.options.configFilePattern); const files = await glob.glob(pattern, { cwd, ignore: this.options.ignorePatterns }); configFiles.push(...files.map((file) => path.resolve(cwd, file))); } } catch (error) { const platformError = createPlatformError('config-file-scanning', error, [ 'Check that config scan paths exist', 'Verify module.config.ts files are valid', 'Ensure proper file system permissions' ]); this.logger.error('Config file scanning failed:', platformError); throw platformError; } return configFiles; } /** * Load module metadata from package candidate */ async loadModuleFromPackage(candidate) { try { // Look for module.config.ts in the package directory const path = await getPath(); const configPath = path.join(candidate.path, 'module.config.ts'); try { return await this.loadModuleFromConfig(configPath); } catch { // If no config file, try to generate metadata from package.json return this.generateMetadataFromPackage(candidate); } } catch (error) { console.warn(`Failed to load module from package ${candidate.packageName}:`, error); return null; } } /** * Load module metadata from configuration file */ async loadModuleFromConfig(configPath) { try { // Dynamic import of the configuration file const configModule = await import(configPath); const config = configModule.default || configModule.moduleConfig; if (!config) { throw new Error('No module configuration found in file'); } return config; } catch (error) { console.warn(`Failed to load config from ${configPath}:`, error); return null; } } /** * Generate module metadata from package.json */ generateMetadataFromPackage(candidate) { const pkg = candidate.packageJson; return { id: candidate.id, name: this.formatModuleName(candidate.id), version: pkg.version || '1.0.0', description: pkg.description || `${candidate.id} module`, category: this.inferCategoryFromId(candidate.id), tags: pkg.keywords || [], icon: 'AppstoreOutlined', permissions: [], routes: [], navigation: [], dependencies: Object.keys(pkg.dependencies || {}), status: ModuleStatus.ACTIVE, priority: 100, author: pkg.author || 'Unknown', license: pkg.license || 'MIT', repository: pkg.repository?.url }; } /** * Check if package is a module package */ isModulePackage(packageJson) { return packageJson.name && packageJson.name.startsWith('@dbs-portal/module-'); } /** * Extract module ID from package name */ extractModuleId(packageName) { return packageName.replace('@dbs-portal/module-', ''); } /** * Format module name from ID */ formatModuleName(moduleId) { return moduleId .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } /** * Infer category from module ID */ inferCategoryFromId(moduleId) { const categoryMap = { 'identity': 'identity', 'permission': 'identity', 'user': 'identity', 'auth': 'identity', 'file': 'content', 'document': 'content', 'media': 'content', 'cms': 'content', 'blog': 'content', 'chat': 'communication', 'notification': 'communication', 'message': 'communication', 'setting': 'system', 'config': 'system', 'audit': 'system', 'language': 'system', 'tenant': 'system' }; for (const [key, category] of Object.entries(categoryMap)) { if (moduleId.includes(key)) { return category; } } return 'business'; } /** * Read and parse package.json file */ async readPackageJson(packagePath) { const [path, fs] = await Promise.all([getPath(), getFs()]); const packageJsonPath = path.join(packagePath, 'package.json'); try { const content = await fs.readFile(packageJsonPath, 'utf-8'); return JSON.parse(content); } catch (error) { const platformError = createPlatformError('read-package-json', error, [ `Check that ${packageJsonPath} exists`, 'Verify the file is valid JSON', 'Ensure proper file system permissions' ]); this.logger.error(`Failed to read package.json at ${packageJsonPath}:`, platformError); throw platformError; } } /** * Remove duplicate modules (prefer first occurrence) */ deduplicateModules(modules) { const seen = new Set(); return modules.filter(module => { if (seen.has(module.id)) { return false; } seen.add(module.id); return true; }); } /** * Validate discovered modules */ validateDiscoveredModules(modules, errors, warnings) { const validModules = []; for (const module of modules) { const validation = validateModuleMetadata(module); if (validation.valid) { validModules.push(module); } else { errors.push({ source: `module-${module.id}`, message: `Validation failed: ${validation.errors.map(e => e.message).join(', ')}`, details: validation.errors }); } if (validation.warnings) { warnings.push({ source: `module-${module.id}`, message: `Validation warnings: ${validation.warnings.map(w => w.message).join(', ')}`, details: validation.warnings }); } } return validModules; } } //# sourceMappingURL=ModuleDiscoveryService.js.map