UNPKG

@notjustcoders/ioc-arise

Version:

Arise type-safe IoC containers from your code. Zero overhead, zero coupling.

281 lines 14.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateCommand = void 0; const commander_1 = require("commander"); const path_1 = require("path"); const fs_1 = require("fs"); const analyser_1 = require("../analyser"); const generator_1 = require("../generator"); const circular_dependency_detector_1 = require("../utils/circular-dependency-detector"); const configManager_1 = require("../utils/configManager"); const configValidator_1 = require("../utils/configValidator"); const errorFactory_1 = require("../errors/errorFactory"); const IoCError_1 = require("../errors/IoCError"); const moduleResolver_1 = require("../utils/moduleResolver"); const tsConfigPathsResolver_1 = require("../utils/tsConfigPathsResolver"); const logger_1 = require("../utils/logger"); const generatedCodeValidator_1 = require("../utils/generatedCodeValidator"); exports.generateCommand = new commander_1.Command("generate") .description("Generate IoC container from TypeScript classes") .option("-s, --source <dir>", "Source directory to scan") .option("-o, --output <file>", "Output file path") .option("-i, --interface <pattern>", "Interface name pattern to match (regex)") .option("-e, --exclude <patterns...>", "Exclude patterns for files") .option("--factory-pattern <pattern>", "Factory function name pattern to match (regex). Default: functions with @factory JSDoc comment") .option("--value-pattern <pattern>", "Value name pattern to match (regex). Default: values with @value JSDoc comment") .option("--check-cycles", "Only check for circular dependencies without generating") .option("--no-validate", "Disable validation of generated code") .option("--verbose", "Enable verbose logging") .action(async (options) => { try { // Initialize logger logger_1.Logger.initialize({ verbose: options.verbose ?? false }); // Initialize config manager with the source directory const initialSourceDir = (0, path_1.resolve)(options.source || "."); const configManager = new configManager_1.ConfigManager(initialSourceDir); // Validate config if present if (configManager.hasConfigFile()) { const config = configManager.getConfig(); if (!configValidator_1.ConfigValidator.validateAndLog(config, configManager.getConfigPath())) { process.exit(1); } } // Merge CLI options with config file const mergedOptions = configManager.mergeWithCliOptions(options); const sourceDir = (0, path_1.resolve)(mergedOptions.source); const outputPath = (0, path_1.resolve)(initialSourceDir, mergedOptions.output); if (configManager.hasConfigFile() && mergedOptions.verbose) { logger_1.Logger.info(`Using config file: ${configManager.getConfigPath()}`); } // Initialize module resolver if modules are configured let moduleResolver = null; if (mergedOptions.modules && Object.keys(mergedOptions.modules).length > 0) { moduleResolver = new moduleResolver_1.ModuleResolver(mergedOptions.modules, sourceDir); if (mergedOptions.verbose) { logger_1.Logger.info(`Module support enabled with ${Object.keys(mergedOptions.modules).length} modules`); } } if (!(0, fs_1.existsSync)(sourceDir)) { const error = errorFactory_1.ErrorFactory.sourceDirectoryNotFound(sourceDir); logger_1.Logger.error(IoCError_1.ErrorUtils.formatForConsole(error)); process.exit(1); } if (mergedOptions.verbose) { logger_1.Logger.info(`Scanning directory: ${sourceDir}`); logger_1.Logger.info(`Output file: ${outputPath}`); if (mergedOptions.interface) { logger_1.Logger.info(`Interface pattern: ${mergedOptions.interface}`); } } logger_1.Logger.custom("🚀", "Starting analysis...", logger_1.Logger.getColors().cyan + logger_1.Logger.getColors().bright); // Analyze the project (including factories) const { classes, factories, values } = await (0, analyser_1.analyzeProjectWithFactories)(sourceDir, { sourceDir, interfacePattern: mergedOptions.interface, excludePatterns: mergedOptions.exclude, factoryPattern: mergedOptions.factoryPattern, valuePattern: mergedOptions.valuePattern, }); // Check if we have anything to generate if (classes.length === 0 && (!factories || factories.length === 0) && (!values || values.length === 0)) { const error = errorFactory_1.ErrorFactory.noClassesFound(mergedOptions.interface || 'interfaces with "implements" keyword'); logger_1.Logger.error(IoCError_1.ErrorUtils.formatForConsole(error)); logger_1.Logger.error("No classes, factories, or values found to generate a container."); process.exit(1); } // Group everything by module let moduleGroupedClasses; let moduleGroupedFactories; let moduleGroupedValues; if (moduleResolver) { if (classes.length > 0) { moduleGroupedClasses = moduleResolver.groupClassesByModule(classes); } if (factories && factories.length > 0) { moduleGroupedFactories = moduleResolver.groupFactoriesByModule(factories); } if (values && values.length > 0) { moduleGroupedValues = moduleResolver.groupValuesByModule(values); } } else { if (classes.length > 0) { moduleGroupedClasses = new Map([["CoreModule", classes]]); } } // Print structured analysis summary logger_1.Logger.newline(); printAnalysisSummary(moduleResolver !== null, moduleGroupedClasses, moduleGroupedFactories, moduleGroupedValues, classes, factories, values, mergedOptions.verbose ?? false); logger_1.Logger.newline(); // Check for circular dependencies between classes const cycles = circular_dependency_detector_1.CircularDependencyDetector.detect(classes); if (cycles.length > 0) { const firstCycle = cycles[0]; if (firstCycle && firstCycle.length > 0) { const className = firstCycle[0] || "unknown"; const error = errorFactory_1.ErrorFactory.circularDependency(className, firstCycle); logger_1.Logger.error(IoCError_1.ErrorUtils.formatForConsole(error)); if (cycles.length > 1) { logger_1.Logger.log("\n Additional cycles:"); cycles.slice(1).forEach((cycle, index) => { logger_1.Logger.log(` ${index + 2}. ${cycle.join(" → ")}`); }); } } else { logger_1.Logger.error("Circular dependencies detected but cycle information is incomplete"); } process.exit(1); } // Check for circular dependencies between modules if (moduleGroupedClasses && moduleGroupedClasses.size > 1 && classes.length > 0) { const moduleCycles = circular_dependency_detector_1.CircularDependencyDetector.detectModuleCycles(moduleGroupedClasses); if (moduleCycles.length > 0) { logger_1.Logger.error("Circular dependencies detected between modules:"); moduleCycles.forEach((cycle, index) => { if (cycle.length > 0) { // The cycle already includes the starting node at the end from TopologicalSorter const cycleDisplay = cycle.join(" → "); logger_1.Logger.log(` ${index + 1}. ${cycleDisplay}`); } }); logger_1.Logger.log("\n Module dependencies must form a directed acyclic graph (DAG)."); logger_1.Logger.log(" Consider refactoring to break the circular dependency or merging the modules."); process.exit(1); } } if (mergedOptions.checkCycles) { logger_1.Logger.success("No circular dependencies found (classes or modules)."); process.exit(0); } // Generate container file logger_1.Logger.custom("🚀", "Generating container...", logger_1.Logger.getColors().cyan + logger_1.Logger.getColors().bright); const pathsResolver = new tsConfigPathsResolver_1.TsConfigPathsResolver(sourceDir); const generatedFiles = generator_1.IoCContainerGenerator.generate(classes, outputPath, moduleGroupedClasses, factories, values, moduleGroupedFactories, moduleGroupedValues, pathsResolver, moduleResolver !== null); // Validate generated code if (mergedOptions.validate !== false) { try { generatedCodeValidator_1.GeneratedCodeValidator.validate(generatedFiles, sourceDir); } catch (validationError) { logger_1.Logger.error("Validation failed. Generated code may be invalid."); // We don't exit here to allow user to see stats and output path } } const statParts = []; if (classes.length > 0) statParts.push(`${classes.length} class${classes.length !== 1 ? "es" : ""}`); if (factories && factories.length > 0) statParts.push(`${factories.length} factor${factories.length !== 1 ? "ies" : "y"}`); if (values && values.length > 0) statParts.push(`${values.length} value${values.length !== 1 ? "s" : ""}`); if (moduleResolver) { const moduleCount = new Set([ ...(moduleGroupedClasses?.keys() ?? []), ...(moduleGroupedFactories?.keys() ?? []), ...(moduleGroupedValues?.keys() ?? []), ]).size; statParts.unshift(`${moduleCount} module${moduleCount !== 1 ? "s" : ""}`); } logger_1.Logger.success("Container generated successfully!"); logger_1.Logger.log(logger_1.Logger.colorizeText(` 📁 ${outputPath}`, logger_1.Logger.getColors().gray)); logger_1.Logger.log(logger_1.Logger.colorizeText(` ${statParts.join(" · ")}`, logger_1.Logger.getColors().gray)); process.exit(0); } catch (error) { if (IoCError_1.ErrorUtils.isIoCError(error)) { logger_1.Logger.error(IoCError_1.ErrorUtils.formatForConsole(error)); } else { const wrappedError = errorFactory_1.ErrorFactory.unexpectedError(error); logger_1.Logger.error(IoCError_1.ErrorUtils.formatForConsole(wrappedError)); } process.exit(1); } }); function printAnalysisSummary(isModular, moduleGroupedClasses, moduleGroupedFactories, moduleGroupedValues, classes, factories, values, _verbose) { const c = logger_1.Logger.getColors(); const printClass = (cls, indent) => { const name = logger_1.Logger.colorizeText(`${indent}🏗️ ${cls.name}`, c.white); if (cls.interfaceName) { logger_1.Logger.log(name + logger_1.Logger.colorizeText(" ──implements──▶ ", c.gray) + logger_1.Logger.colorizeText(cls.interfaceName, c.green)); } else if (cls.abstractClassName) { logger_1.Logger.log(name + logger_1.Logger.colorizeText(" ──extends──▶ ", c.gray) + logger_1.Logger.colorizeText(cls.abstractClassName, c.green)); } else { logger_1.Logger.log(name); } if (cls.dependencies.length > 0) { const deps = cls.dependencies.map((d) => d.name).join(", "); logger_1.Logger.log(logger_1.Logger.colorizeText(`${indent} deps: `, c.gray) + logger_1.Logger.colorizeText(deps, c.yellow)); } }; const printFactory = (factory, indent) => { if (factory.instanceFactoryFor) { logger_1.Logger.log(logger_1.Logger.colorizeText(`${indent}🔌 ${factory.name}()`, c.white) + logger_1.Logger.colorizeText(" ──returns──▶ ", c.gray) + logger_1.Logger.colorizeText(factory.instanceFactoryFor, c.green)); } else { const token = factory.token || factory.name; logger_1.Logger.log(logger_1.Logger.colorizeText(`${indent}🏭 ${factory.name}()`, c.white) + logger_1.Logger.colorizeText(" ──factory──▶ ", c.gray) + logger_1.Logger.colorizeText(`'${token}'`, c.magenta)); } if (factory.dependencies.length > 0) { const deps = factory.dependencies.map((d) => d.name).join(", "); logger_1.Logger.log(logger_1.Logger.colorizeText(`${indent} deps: `, c.gray) + logger_1.Logger.colorizeText(deps, c.yellow)); } }; const printValue = (value, indent) => { const label = value.interfaceName || value.token || value.name; logger_1.Logger.log(logger_1.Logger.colorizeText(`${indent}💎 ${value.name}`, c.white) + logger_1.Logger.colorizeText(" ──value──▶ ", c.gray) + logger_1.Logger.colorizeText(label, c.green)); // ValueInfo has no dependencies field, so nothing to print }; if (isModular) { const allModuleNames = new Set([ ...(moduleGroupedClasses?.keys() ?? []), ...(moduleGroupedFactories?.keys() ?? []), ...(moduleGroupedValues?.keys() ?? []), ]); for (const moduleName of allModuleNames) { logger_1.Logger.log(logger_1.Logger.colorizeText(`📦 ${moduleName}`, c.cyan + c.bright)); for (const cls of moduleGroupedClasses?.get(moduleName) ?? []) { printClass(cls, " "); } for (const factory of moduleGroupedFactories?.get(moduleName) ?? []) { printFactory(factory, " "); } for (const value of moduleGroupedValues?.get(moduleName) ?? []) { printValue(value, " "); } } } else { for (const cls of classes) { printClass(cls, " "); } for (const factory of factories ?? []) { printFactory(factory, " "); } for (const value of values ?? []) { printValue(value, " "); } } } //# sourceMappingURL=generate.js.map