UNPKG

@notjustcoders/ioc-arise

Version:

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

571 lines (563 loc) 27.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.IoCContainerGenerator = void 0; const path_1 = require("path"); const fs_1 = require("fs"); const type_declaration_generator_1 = require("./type-declaration-generator"); const errorFactory_1 = require("../errors/errorFactory"); class IoCContainerGenerator { /** * Generates the IoC container code for the given classes * @param classes - Analyzed classes to be registered (can be flat or grouped) * @param outputPath - Path where the container file will be generated * @param moduleGroupedClasses - Optional: Classes grouped by modules * @param factories - Optional: Factory functions to be registered * @param values - Optional: Plain object values to be registered with useValue */ static generate(classes, outputPath, moduleGroupedClasses, factories, values, moduleGroupedFactories, moduleGroupedValues, pathsResolver, isModular) { const generatedFiles = []; // Check for name collisions before generating // If moduleGroupedClasses is provided, check all classes from all modules const allClassesToCheck = moduleGroupedClasses && moduleGroupedClasses.size > 0 ? Array.from(moduleGroupedClasses.values()).flat() : classes; this.checkForNameCollisions(allClassesToCheck); // Ensure directory exists const outputDir = (0, path_1.dirname)(outputPath); (0, fs_1.mkdirSync)(outputDir, { recursive: true }); if (isModular) { // Generate modular code with separate files for each module const modularFiles = this.generateModularFiles(moduleGroupedClasses ?? new Map(), outputPath, factories, values, moduleGroupedFactories, moduleGroupedValues); generatedFiles.push(...modularFiles); } else { // Generate single flat file const containerCode = this.generateFlatCode(classes, outputPath, factories, values); (0, fs_1.writeFileSync)(outputPath, containerCode); generatedFiles.push(outputPath); } // Generate type declarations (.d.ts file) const typesPath = outputPath.endsWith('.gen.ts') ? outputPath.replace(/\.gen\.ts$/, '.gen.d.ts') : outputPath.replace(/\.ts$/, '.d.ts'); type_declaration_generator_1.TypeDeclarationGenerator.generate(classes, typesPath, factories, values, pathsResolver); generatedFiles.push(typesPath); return generatedFiles; } static generateFlatCode(classes, outputPath, factories, values) { // Collect all interface names to identify which dependencies should use string tokens const interfaceNames = new Set(); classes.forEach(cls => { if (cls.interfaceName) { interfaceNames.add(cls.interfaceName); } }); // Also add interface names from instance factories and factory tokens if (factories) { factories.forEach(factory => { if (factory.instanceFactoryFor) { interfaceNames.add(factory.instanceFactoryFor); } const token = factory.token || factory.name; if (token) interfaceNames.add(token); }); } // Add value tokens — values are always registered as string tokens, so any dependency // on a value interface/name must also use a string token if (values) { values.forEach(value => { const token = value.token || value.interfaceName || value.name; if (token) interfaceNames.add(token); }); } // Collect abstract class names from dependencies const abstractClassNames = new Set(); classes.forEach(cls => { cls.dependencies.forEach(dep => { const isInCurrentClasses = classes.some(c => c.name === dep.name); const looksLikeAbstractClass = dep.name.startsWith('Abstract') || dep.name.includes('Abstract'); if (!isInCurrentClasses && looksLikeAbstractClass) { abstractClassNames.add(dep.name); } }); }); // Collect concrete class names — a dep whose name matches a registered class is // referenced by its constructor (not a string token). Everything else (interfaces, // external types, factories, values) uses a string token. const classNames = new Set(classes.map(c => c.name)); const imports = this.generateImports(classes, outputPath, factories, values, classes, interfaceNames, abstractClassNames); const classRegistrations = this.generateRegistrations(classes, interfaceNames, '', abstractClassNames, classNames); const factoryRegistrations = factories && factories.length > 0 ? this.generateFactoryRegistrations(factories, interfaceNames, '', abstractClassNames, classNames) : ''; const valueRegistrations = values && values.length > 0 ? this.generateValueRegistrations(values, '') : ''; const filename = outputPath.split('/').pop()?.replace(/\.ts$/, '') || 'container.gen'; const dtsFilename = outputPath.split('/').pop()?.replace(/\.ts$/, '.d') || 'container.gen.d'; const allRegistrations = [ classRegistrations, factoryRegistrations, valueRegistrations ].filter(Boolean).join('\n\n'); return `/** * This file is auto-generated by ioc-arise. * Do not modify this file manually. */ import { Container, Lifecycle } from '@notjustcoders/di-container'; import type { ContainerRegistry } from './${dtsFilename}'; ${imports} export const container = new Container<ContainerRegistry>(); ${allRegistrations} `; } static generateModularFiles(moduleGroupedClasses, outputPath, _factories, _values, moduleGroupedFactories, moduleGroupedValues) { const generatedFiles = []; const outputDir = (0, path_1.dirname)(outputPath); const baseFilename = (0, path_1.basename)(outputPath, '.ts'); const modulesDir = (0, path_1.join)(outputDir, 'modules'); // Create modules directory (0, fs_1.mkdirSync)(modulesDir, { recursive: true }); // Collect all interface names and class names across all modules const allClasses = Array.from(moduleGroupedClasses.values()).flat(); const classNames = new Set(allClasses.map(c => c.name)); const interfaceNames = new Set(); allClasses.forEach(cls => { if (cls.interfaceName) { interfaceNames.add(cls.interfaceName); } }); // Also add interface names from instance factories and factory tokens if (moduleGroupedFactories) { moduleGroupedFactories.forEach(moduleFactories => { moduleFactories.forEach(factory => { if (factory.instanceFactoryFor) { interfaceNames.add(factory.instanceFactoryFor); } const token = factory.token || factory.name; if (token) interfaceNames.add(token); }); }); } // Add value tokens so dependencies on values use string tokens if (moduleGroupedValues) { moduleGroupedValues.forEach(moduleValues => { moduleValues.forEach(value => { const token = value.token || value.interfaceName || value.name; if (token) interfaceNames.add(token); }); }); } // Collect the union of all module names (a module may have only factories/values, no classes) const allModuleNames = new Set([ ...moduleGroupedClasses.keys(), ...(moduleGroupedFactories?.keys() ?? []), ...(moduleGroupedValues?.keys() ?? []), ]); // Generate each module file const moduleImports = []; const moduleNames = []; for (const moduleName of allModuleNames) { const classes = moduleGroupedClasses.get(moduleName) ?? []; const moduleFactories = moduleGroupedFactories?.get(moduleName); const moduleValues = moduleGroupedValues?.get(moduleName); const moduleFilename = `${moduleName.charAt(0).toLowerCase() + moduleName.slice(1)}.module.ts`; const moduleFilePath = (0, path_1.join)(modulesDir, moduleFilename); const moduleCode = this.generateModuleFile(moduleName, classes, interfaceNames, moduleFilePath, moduleFactories, moduleValues, allClasses, classNames); (0, fs_1.writeFileSync)(moduleFilePath, moduleCode); generatedFiles.push(moduleFilePath); // Add import for main container file const moduleVarName = moduleName.charAt(0).toLowerCase() + moduleName.slice(1); moduleImports.push(`import { ${moduleVarName} } from './modules/${moduleFilename.replace('.ts', '')}';`); moduleNames.push(moduleName); } // Generate main container file const dtsFilename = baseFilename.replace(/\.gen$/, '.gen.d'); const mainContainerCode = this.generateMainContainerFile(allModuleNames, moduleImports, dtsFilename); (0, fs_1.writeFileSync)(outputPath, mainContainerCode); generatedFiles.push(outputPath); return generatedFiles; } static generateModuleFile(moduleName, classes, interfaceNames, moduleFilePath, factories, values, allClasses = [], classNames = new Set()) { // Collect abstract class names that are dependencies const abstractClassNames = new Set(); classes.forEach(cls => { cls.dependencies.forEach(dep => { const isInCurrentModule = classes.some(c => c.name === dep.name); const looksLikeAbstractClass = dep.name.startsWith('Abstract') || dep.name.includes('Abstract'); if (!isInCurrentModule && looksLikeAbstractClass) { abstractClassNames.add(dep.name); } }); }); const imports = this.generateImports(classes, moduleFilePath, factories, values, allClasses, interfaceNames, abstractClassNames); const classRegistrations = this.generateModuleRegistrations(classes, interfaceNames, abstractClassNames, classNames); const factoryRegistrations = factories && factories.length > 0 ? this.generateModuleFactoryRegistrations(factories, interfaceNames, abstractClassNames, classNames) : ''; const valueRegistrations = values && values.length > 0 ? this.generateModuleValueRegistrations(values) : ''; const allRegistrations = [classRegistrations, factoryRegistrations, valueRegistrations] .filter(Boolean) .join('\n'); const moduleVarName = moduleName.charAt(0).toLowerCase() + moduleName.slice(1); return `/** * This file is auto-generated by ioc-arise. * Do not modify this file manually. * * Module: ${moduleName} */ import { ContainerModule, Lifecycle } from '@notjustcoders/di-container'; ${imports} export const ${moduleVarName} = new ContainerModule() ${allRegistrations}; `; } static generateModuleFactoryRegistrations(factories, interfaceNames, abstractClassNames = new Set(), classNames = new Set()) { return factories.map(factory => { const token = factory.token ? `'${factory.token}'` : `'${factory.name}'`; const dependencies = factory.dependencies.length > 0 ? `, dependencies: [${factory.dependencies.map(dep => { if (interfaceNames.has(dep.name)) return `'${dep.name}'`; if (abstractClassNames.has(dep.name)) return `'${dep.name}'`; if (classNames.has(dep.name)) return dep.name; return `'${dep.name}'`; }).join(', ')}]` : ''; const lifecycle = factory.scope === 'singleton' ? 'Lifecycle.Singleton' : 'Lifecycle.Transient'; let factoryFunction; if (factory.useContextObject && factory.contextObjectProperties) { const depParams = factory.dependencies.map((_, index) => { return factory.contextObjectProperties?.[index]?.name || `dep${index}`; }).join(', '); const contextObject = factory.contextObjectProperties.map(prop => prop.name).join(', '); factoryFunction = `(${depParams}) => ${factory.name}({ ${contextObject} })`; } else { factoryFunction = factory.name; } return ` .register(${token}, { useFactory: ${factoryFunction}${dependencies}, lifecycle: ${lifecycle} })`; }).join('\n'); } static generateModuleValueRegistrations(values) { return values.map(value => { const token = value.token ? `'${value.token}'` : value.interfaceName ? `'${value.interfaceName}'` : `'${value.name}'`; return ` .register(${token}, { useValue: ${value.name}, lifecycle: Lifecycle.Singleton })`; }).join('\n'); } static generateMainContainerFile(moduleNames, moduleImports, dtsFilename) { const moduleInstantiations = Array.from(moduleNames.keys()) .map(name => { const varName = name.charAt(0).toLowerCase() + name.slice(1); return `container.registerModule(${varName});`; }) .join('\n'); return `/** * This file is auto-generated by ioc-arise. * Do not modify this file manually. */ import { Container } from '@notjustcoders/di-container'; import type { ContainerRegistry } from './${dtsFilename}'; ${moduleImports.join('\n')} export const container = new Container<ContainerRegistry>(); ${moduleInstantiations} `; } static generateModularCode(moduleGroupedClasses, outputPath, factories, values) { const allClasses = Array.from(moduleGroupedClasses.values()).flat(); // Collect all interface names const interfaceNames = new Set(); allClasses.forEach(cls => { if (cls.interfaceName) { interfaceNames.add(cls.interfaceName); } }); // Also add factory tokens if (factories) { factories.forEach(factory => { if (factory.instanceFactoryFor) { interfaceNames.add(factory.instanceFactoryFor); } const token = factory.token || factory.name; if (token) interfaceNames.add(token); }); } // Add value tokens if (values) { values.forEach(value => { const token = value.token || value.interfaceName || value.name; if (token) interfaceNames.add(token); }); } // Collect abstract class names from dependencies const abstractClassNames = new Set(); allClasses.forEach(cls => { cls.dependencies.forEach(dep => { const isInCurrentClasses = allClasses.some(c => c.name === dep.name); const looksLikeAbstractClass = dep.name.startsWith('Abstract') || dep.name.includes('Abstract'); if (!isInCurrentClasses && looksLikeAbstractClass) { abstractClassNames.add(dep.name); } }); }); const imports = this.generateImports(allClasses, outputPath, undefined, undefined, allClasses, interfaceNames, abstractClassNames); const moduleDefinitions = this.generateModuleDefinitions(moduleGroupedClasses, interfaceNames, abstractClassNames); const moduleInstantiations = this.generateModuleInstantiations(moduleGroupedClasses); const filename = outputPath.split('/').pop()?.replace(/\.ts$/, '') || 'container.gen'; const dtsFilename = outputPath.split('/').pop()?.replace(/\.ts$/, '.d') || 'container.gen.d'; return `/** * This file is auto-generated by ioc-arise. * Do not modify this file manually. */ import { Container, ContainerModule, Lifecycle } from '@notjustcoders/di-container'; import type { ContainerRegistry } from './${dtsFilename}'; ${imports} ${moduleDefinitions} export const container = new Container<ContainerRegistry>(); ${moduleInstantiations} `; } static generateModuleDefinitions(moduleGroupedClasses, interfaceNames, abstractClassNames = new Set()) { const modules = []; for (const [moduleName, classes] of moduleGroupedClasses.entries()) { const registrations = this.generateModuleRegistrations(classes, interfaceNames, abstractClassNames); modules.push(`const ${moduleName.charAt(0).toLowerCase() + moduleName.slice(1)} = new ContainerModule() ${registrations};`); } return modules.join('\n\n'); } static generateModuleInstantiations(moduleGroupedClasses) { const moduleNames = Array.from(moduleGroupedClasses.keys()); const instantiations = moduleNames.map(name => { const varName = name.charAt(0).toLowerCase() + name.slice(1); return `container.registerModule(${varName});`; }); return instantiations.join('\n'); } static generateModuleRegistrations(classes, interfaceNames, abstractClassNames = new Set(), classNames = new Set()) { return classes.map(cls => { // Determine registration token const token = cls.interfaceName ? `'${cls.interfaceName}'` : cls.abstractClassName ? `'${cls.abstractClassName}'` : cls.name; // Generate dependencies array const dependencies = cls.dependencies.length > 0 ? `, dependencies: [${cls.dependencies.map(dep => { if (interfaceNames.has(dep.name)) return `'${dep.name}'`; if (abstractClassNames.has(dep.name)) return `'${dep.name}'`; if (classNames.has(dep.name)) return dep.name; return `'${dep.name}'`; }).join(', ')}]` : ''; // Determine lifecycle enum value const lifecycle = cls.scope === 'singleton' ? 'Lifecycle.Singleton' : 'Lifecycle.Transient'; return ` .register(${token}, { useClass: ${cls.name}${dependencies}, lifecycle: ${lifecycle} })`; }).join('\n'); } static generateImports(classes, outputPath, factories, values, allProjectClasses = [], interfaceNames = new Set(), abstractClassNames = new Set()) { const outputDir = (0, path_1.dirname)(outputPath); const importStatements = []; const importMap = new Map(); // Helper to add an import to the map const addImportToMap = (filePath, name) => { // Calculate relative path from output file to file let relativePath = (0, path_1.relative)(outputDir, filePath); // Remove extension (.ts) relativePath = relativePath.replace(/\.ts$/, ''); // Add ./ if it doesn't start with . or / if (!relativePath.startsWith('.')) { relativePath = `./${relativePath}`; } // Use forward slashes for imports relativePath = relativePath.replace(/\\/g, '/'); if (!importMap.has(relativePath)) { importMap.set(relativePath, new Set()); } importMap.get(relativePath).add(name); }; // Collect class imports classes.forEach(cls => { addImportToMap(cls.filePath, cls.name); }); // Collect factory imports if (factories) { factories.forEach(factory => { addImportToMap(factory.filePath, factory.name); }); } // Collect value imports if (values) { values.forEach(value => { addImportToMap(value.filePath, value.name); }); } // Collect dependencies that need class token imports // (those not registered as string tokens via interfaces or abstract classes) const allEntities = [...classes, ...(factories || [])]; const registeredNamesInModule = new Set([ ...classes.map(c => c.name), ...(factories?.map(f => f.name) ?? []), ...(values?.map(v => v.name) ?? []) ]); allEntities.forEach(entity => { entity.dependencies.forEach(dep => { // Skip if it's already registered in this module, or if it's a string token // (registered via interfaces, abstract classes, factories or values) if (registeredNamesInModule.has(dep.name) || interfaceNames.has(dep.name) || abstractClassNames.has(dep.name)) { return; } // If it's a project class, import it using its actual file path const depClass = allProjectClasses.find(c => c.name === dep.name); if (depClass) { addImportToMap(depClass.filePath, depClass.name); } // Note: If not found in project classes, it might be an external dependency. // For now we don't handle external class token imports as we don't have their absolute paths. }); }); // Generate import statements for (const [importPath, names] of importMap.entries()) { const namesArray = Array.from(names).sort(); importStatements.push(`import { ${namesArray.join(', ')} } from '${importPath}';`); } return importStatements.join('\n'); } /** * Checks for class name collisions and throws an error if any are found */ static checkForNameCollisions(classes) { const nameToClasses = new Map(); classes.forEach(cls => { if (!nameToClasses.has(cls.name)) { nameToClasses.set(cls.name, []); } nameToClasses.get(cls.name).push(cls); }); // Check for collisions for (const [className, classesWithSameName] of nameToClasses.entries()) { if (classesWithSameName.length > 1) { const filePaths = classesWithSameName.map(cls => cls.filePath); throw errorFactory_1.ErrorFactory.classNameCollision(className, filePaths); } } } static generateRegistrations(classes, interfaceNames, indent = '', abstractClassNames = new Set(), classNames = new Set()) { return classes.map(cls => { // Determine registration token: // - If implements interface, use string token of interface name // - If extends abstract class, use string token of abstract class name // - Otherwise, use class constructor const token = cls.interfaceName ? `'${cls.interfaceName}'` : cls.abstractClassName ? `'${cls.abstractClassName}'` : cls.name; // Generate dependencies array const dependencies = cls.dependencies.length > 0 ? `\n${indent} dependencies: [${cls.dependencies.map(dep => { if (interfaceNames.has(dep.name)) return `'${dep.name}'`; if (abstractClassNames.has(dep.name)) return `'${dep.name}'`; if (classNames.has(dep.name)) return dep.name; return `'${dep.name}'`; }).join(', ')}],` : ''; // Determine lifecycle enum value const lifecycle = cls.scope === 'singleton' ? 'Lifecycle.Singleton' : 'Lifecycle.Transient'; return `${indent}container.register(${token}, { ${indent} useClass: ${cls.name},${dependencies} ${indent} lifecycle: ${lifecycle}, ${indent}});`; }).join('\n\n'); } static generateFactoryRegistrations(factories, interfaceNames, indent = '', abstractClassNames = new Set(), classNames = new Set()) { return factories.map(factory => { // Use token if provided, otherwise use function name directly const token = factory.token ? `'${factory.token}'` : `'${factory.name}'`; // Generate dependencies array const dependencies = factory.dependencies.length > 0 ? `\n${indent} dependencies: [${factory.dependencies.map(dep => { if (interfaceNames.has(dep.name)) return `'${dep.name}'`; if (abstractClassNames.has(dep.name)) return `'${dep.name}'`; if (classNames.has(dep.name)) return dep.name; return `'${dep.name}'`; }).join(', ')}],` : ''; // Determine lifecycle enum value const lifecycle = factory.scope === 'singleton' ? 'Lifecycle.Singleton' : 'Lifecycle.Transient'; // Generate factory function - wrap if context object pattern is used let factoryFunction; if (factory.useContextObject && factory.contextObjectProperties) { // Generate wrapper function that passes dependencies as context object const contextProps = factory.contextObjectProperties.map(prop => prop.name).join(', '); const contextObject = factory.contextObjectProperties.map(prop => `${prop.name}`).join(', '); const depParams = factory.dependencies.map((_, index) => { const propName = factory.contextObjectProperties?.[index]?.name || `dep${index}`; return propName; }).join(', '); factoryFunction = `(${depParams}) => ${factory.name}({ ${contextObject} })`; } else { // Use factory directly (existing behavior) factoryFunction = factory.name; } return `${indent}container.register(${token}, { ${indent} useFactory: ${factoryFunction},${dependencies} ${indent} lifecycle: ${lifecycle}, ${indent}});`; }).join('\n\n'); } static generateValueRegistrations(values, indent = '') { return values.map(value => { // Use token if provided, otherwise use interface name or value name const token = value.token ? `'${value.token}'` : value.interfaceName ? `'${value.interfaceName}'` : `'${value.name}'`; const lifecycle = value.scope === 'singleton' ? 'Lifecycle.Singleton' : 'Lifecycle.Transient'; return `${indent}container.register(${token}, { ${indent} useValue: ${value.name}, ${indent} lifecycle: ${lifecycle}, ${indent}});`; }).join('\n\n'); } } exports.IoCContainerGenerator = IoCContainerGenerator; //# sourceMappingURL=ioc-container-generator.js.map