@dbs-portal/core-module-registry
Version:
Core module registry system for automatic module discovery and registration
409 lines • 15.5 kB
JavaScript
/**
* 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