@notjustcoders/ioc-arise
Version:
Arise type-safe IoC containers from your code. Zero overhead, zero coupling.
281 lines • 14.7 kB
JavaScript
;
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