UNPKG

@notjustcoders/ioc-arise

Version:

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

630 lines 29.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.InstantiationUtils = void 0; const topological_sorter_1 = require("../../utils/topological-sorter"); const path_injection_utils_1 = require("./path-injection-utils"); const container_preservation_utils_1 = require("./container-preservation-utils"); /** * Shared utility class for instantiation-related functionality used by both * flat and modular container generators. */ class InstantiationUtils { // ========== Core String Utilities ========== /** * Converts a string to camelCase (first letter lowercase). */ static toCamelCase(str) { return str.charAt(0).toLowerCase() + str.slice(1).replace(/[^a-zA-Z0-9]/g, ''); } /** * Generates a factory function name from a class name. */ static generateFactoryName(className) { return `${this.toCamelCase(className)}Factory`; } /** * Generates a getter function name from a class name. */ static generateGetterName(className) { return `get${className}`; } /** * Generates an instance variable name from a class name. */ static generateInstanceName(className) { return this.toCamelCase(className); } /** * Gets the interface name, abstract class name, or falls back to class name. */ static getInterfaceOrClassName(classInfo) { return classInfo.interfaceName || classInfo.abstractClassName || classInfo.name; } // ========== Dependency Resolution Utilities ========== /** * Creates a map of interface names to their implementing class names. */ static createInterfaceToClassMap(classes, importGenerator) { const interfaceToClassMap = new Map(); const classNameCounts = new Map(); // First pass: count occurrences of each class name for (const classInfo of classes) { const count = classNameCounts.get(classInfo.name) || 0; classNameCounts.set(classInfo.name, count + 1); } const classNameIndices = new Map(); for (const classInfo of classes) { const className = importGenerator ? importGenerator.getClassAlias(classInfo) : classInfo.name; if (classInfo.interfaceName) { // Map interface name to class name interfaceToClassMap.set(classInfo.interfaceName, className); } else { // For classes without interfaces, map class name to itself // If there are multiple classes with the same name, use the aliased name const baseClassName = classInfo.name; if (classNameCounts.get(baseClassName) > 1) { // Use the aliased name for disambiguation interfaceToClassMap.set(baseClassName, className); } else { // Single occurrence, use the original name interfaceToClassMap.set(baseClassName, className); } } // Map abstract class name to concrete implementation if (classInfo.abstractClassName) { interfaceToClassMap.set(classInfo.abstractClassName, className); } } return interfaceToClassMap; } /** * Gets default value for a type when no implementation is found. */ static getDefaultValueForType(type, isOptional) { if (isOptional) { return 'undefined'; } // Handle primitive types switch (type.toLowerCase()) { case 'string': return '"default"'; case 'number': return '0'; case 'boolean': return 'false'; case 'date': return 'new Date()'; default: // For class types, create a simple instance return `new ${type}()`; } } /** * Generates constructor arguments for unmanaged dependencies. */ static generateConstructorArgs(params) { return params.map(param => this.getDefaultValueForType(param.type, param.isOptional)).join(', '); } /** * Creates an instance of an unmanaged dependency. */ static createUnmanagedDependencyInstance(className, classInfo) { if (!classInfo || !classInfo.constructorParams.length) { return `new ${className}()`; } const args = this.generateConstructorArgs(classInfo.constructorParams); return `new ${className}(${args})`; } /** * Finds a class by its interface name in a collection of classes. */ static findClassByInterface(interfaceName, classes) { return classes.find(c => c.interfaceName === interfaceName); } /** * Creates a dependency call for managed dependencies based on scope. */ static createDependencyCall(className, scope) { if (scope === 'transient') { return `${this.toCamelCase(className)}Factory()`; } return this.toCamelCase(className); } // ========== Code Generation Utilities ========== /** * Applies indentation to a string, handling multi-line content. */ static applyIndentation(content, indentation) { return indentation ? `${indentation}${content.replace(/\n/g, `\n${indentation}`)}` : content; } /** * Generates a constructor instantiation expression. */ static generateConstructorCall(className, constructorArgs) { return constructorArgs ? `new ${className}(${constructorArgs})` : `new ${className}()`; } /** * Generates a constructor instantiation expression with alias support. */ static generateConstructorCallWithAlias(classInfo, constructorArgs, importGenerator) { const className = importGenerator ? importGenerator.getClassAlias(classInfo) : classInfo.name; return constructorArgs ? `new ${className}(${constructorArgs})` : `new ${className}()`; } /** * Generates a function call expression with optional arguments. */ static generateFunctionCall(functionName, args) { const argsStr = args && args.length > 0 ? args.join(', ') : ''; return `${functionName}(${argsStr})`; } /** * Generates a getter property with return statement. */ static generateGetterProperty(propertyName, returnType, returnExpression, indentation = ' ') { return `${indentation}get ${propertyName}(): ${returnType} {\n${indentation} return ${returnExpression};\n${indentation}}`; } /** * Helper method to generate object exports with consistent formatting. */ static generateObjectExport(objectName, properties, separator = '') { const joinSeparator = separator || '\n'; return `export const ${objectName} = {\n${properties.join(joinSeparator)}\n};`; } /** * Helper method to generate a container property with consistent formatting. */ static generateContainerProperty(propertyName, returnType, returnExpression) { return ` get ${propertyName}(): ${returnType} {\n return ${returnExpression};\n },`; } /** * Generic helper for generating code sections with consistent indentation handling. */ static generateCodeSection(items, generator, indentation = '', isMultiline = false) { const results = []; for (const item of items) { const code = generator(item); const indentedCode = isMultiline ? this.applyIndentation(code, indentation) : `${indentation}${code}`; results.push(indentedCode); } return results; } // ========== Class Filtering and Sorting Utilities ========== /** * Filters classes to get only singletons. */ static getSingletonClasses(classes) { return classes.filter(c => c.scope !== 'transient'); } /** * Filters classes to get only transients. */ static getTransientClasses(classes) { return classes.filter(c => c.scope === 'transient'); } /** * Checks if a class is transient. */ static isTransient(classInfo) { return classInfo.scope === 'transient'; } /** * Checks if a class is singleton. */ static isSingleton(classInfo) { return classInfo.scope === 'singleton'; } // ========== Core Code Generation Methods ========== /** * Generates factory functions for transient classes. */ static generateTransientFactory(classInfo, constructorArgs, importGenerator) { const className = importGenerator ? importGenerator.getClassAlias(classInfo) : classInfo.name; const factoryName = this.generateFactoryName(className); const constructorCall = this.generateConstructorCallWithAlias(classInfo, constructorArgs, importGenerator); return `const ${factoryName} = (): ${className} => ${constructorCall};`; } /** * Generates a singleton variable declaration. */ static generateSingletonVariable(classInfo, importGenerator) { const className = importGenerator ? importGenerator.getClassAlias(classInfo) : classInfo.name; const instanceName = this.generateInstanceName(className); return `let ${instanceName}: ${className} | undefined;`; } /** * Generates a singleton getter function. */ static generateSingletonGetter(classInfo, constructorArgs, importGenerator) { const className = importGenerator ? importGenerator.getClassAlias(classInfo) : classInfo.name; const instanceName = this.generateInstanceName(className); const getterName = this.generateGetterName(className); const instantiation = this.generateConstructorCallWithAlias(classInfo, constructorArgs, importGenerator); return `const ${getterName} = (): ${className} => {\n if (!${instanceName}) {\n ${instanceName} = ${instantiation};\n }\n return ${instanceName};\n};`; } /** * Filters classes by scope. */ static filterClassesByScope(classes) { return { singletonClasses: this.getSingletonClasses(classes), transientClasses: this.getTransientClasses(classes) }; } /** * Creates a managed dependency call based on class scope. */ static createManagedDependencyCall(classInfo, implementingClass, importGenerator) { // Use aliased class name if available const aliasedClassName = importGenerator ? importGenerator.getClassAlias(classInfo) : implementingClass; if (this.isTransient(classInfo)) { const factoryName = this.generateFactoryName(aliasedClassName); return this.generateFunctionCall(factoryName); } const getterName = this.generateGetterName(aliasedClassName); return this.generateFunctionCall(getterName); } /** * Creates a function signature with parameters. */ static createFunctionSignature(functionName, params) { return params.length > 0 ? `function ${functionName}(${params.join(', ')})` : `function ${functionName}()`; } /** * Creates a module export getter. */ static createModuleExportGetter(classInfo, importGenerator) { // Use alias if available, otherwise use class name const className = importGenerator && importGenerator.getClassAlias ? importGenerator.getClassAlias(classInfo) : classInfo.name; const getters = []; // PRIORITIZE interface/abstract class names for dependency inversion // If there's an interface name, create the primary getter for it if (classInfo.interfaceName) { if (this.isTransient(classInfo)) { const factoryCall = this.generateFunctionCall(this.generateFactoryName(className)); getters.push(this.generateGetterProperty(classInfo.interfaceName, className, factoryCall, ' ')); } else { const getterCall = this.generateFunctionCall(this.generateGetterName(className)); getters.push(this.generateGetterProperty(classInfo.interfaceName, className, getterCall, ' ')); } } // If there's an abstract class name and it's different from interface name, create a getter for it if (classInfo.abstractClassName && classInfo.abstractClassName !== classInfo.interfaceName) { if (this.isTransient(classInfo)) { const factoryCall = this.generateFunctionCall(this.generateFactoryName(className)); getters.push(this.generateGetterProperty(classInfo.abstractClassName, className, factoryCall, ' ')); } else { const getterCall = this.generateFunctionCall(this.generateGetterName(className)); getters.push(this.generateGetterProperty(classInfo.abstractClassName, className, getterCall, ' ')); } } // ONLY create a getter for the implementation class name if there's NO interface or abstract class // This ensures we don't expose redundant getters const hasInterfaceOrAbstractGetter = classInfo.interfaceName || classInfo.abstractClassName; if (!hasInterfaceOrAbstractGetter) { if (this.isTransient(classInfo)) { const factoryCall = this.generateFunctionCall(this.generateFactoryName(className)); getters.push(this.generateGetterProperty(className, className, factoryCall, ' ')); } else { const getterCall = this.generateFunctionCall(this.generateGetterName(className)); getters.push(this.generateGetterProperty(className, className, getterCall, ' ')); } } return getters.join(',\n'); } /** * Sorts classes by their dependencies using topological sorting. */ static sortClassesByDependencies(classes, allModuleClasses) { // Build dependency graph for classes within the module const dependencyGraph = new Map(); // Create unique identifiers for classes to handle name collisions const getUniqueId = (classInfo) => `${classInfo.name}:${classInfo.filePath}`; for (const classInfo of classes) { const dependencies = []; // Only include dependencies that are within the same module and are singletons for (const dep of classInfo.dependencies) { const depClass = allModuleClasses.find(c => c.interfaceName === dep.name && c.scope !== 'transient'); if (depClass && classes.includes(depClass)) { dependencies.push(getUniqueId(depClass)); } } dependencyGraph.set(getUniqueId(classInfo), dependencies); } // Use TopologicalSorter to sort the classes const sortResult = topological_sorter_1.TopologicalSorter.sort(dependencyGraph); // Map sorted unique IDs back to ClassInfo objects const classMap = new Map(classes.map(c => [getUniqueId(c), c])); return sortResult.sorted.map(uniqueId => classMap.get(uniqueId)).filter(Boolean); } /** * Resolves a basic dependency by finding the implementing class and creating the appropriate call. * This is the common pattern used by both flat and modular generators. */ static resolveBasicDependency(dependency, availableClasses, interfaceToClassMap, importGenerator, requestingClass) { // Try to find by direct class name match first (highest priority) const directClassMatches = availableClasses.filter(c => c.name === dependency); if (directClassMatches.length > 0) { let selectedMatch = directClassMatches[0]; // If there are multiple matches and we have context about the requesting class, // prefer the one from the same directory/domain if (directClassMatches.length > 1 && requestingClass) { const requestingDir = requestingClass.filePath.split('/').slice(0, -1).join('/'); const sameDirectoryMatch = directClassMatches.find(c => { const candidateDir = c.filePath.split('/').slice(0, -1).join('/'); return candidateDir === requestingDir; }); if (sameDirectoryMatch) { selectedMatch = sameDirectoryMatch; } } if (selectedMatch) { const aliasedName = importGenerator ? importGenerator.getClassAlias(selectedMatch) : selectedMatch.name; return this.createManagedDependencyCall(selectedMatch, aliasedName, importGenerator); } } // Try to find by interface name const depClass = availableClasses.find(c => c.interfaceName === dependency); if (depClass) { return this.createManagedDependencyCall(depClass, depClass.name, importGenerator); } // Try to find by abstract class name - look for classes that extend the abstract class const abstractImplementation = availableClasses.find(c => c.abstractClassName === dependency); if (abstractImplementation) { return this.createManagedDependencyCall(abstractImplementation, abstractImplementation.name, importGenerator); } // Try to find by class name using interface map (lowest priority) if (interfaceToClassMap) { const implementingClass = interfaceToClassMap.get(dependency); if (implementingClass) { const depClassInfo = availableClasses.find(c => c.name === implementingClass); if (depClassInfo) { return this.createManagedDependencyCall(depClassInfo, implementingClass, importGenerator); } } } return null; } /** * Generates constructor arguments string from an array of argument strings. * Common pattern used by both generators. */ static joinConstructorArguments(args) { return args.length > 0 ? args.join(', ') : ''; } /** * Generates a section of transient factory functions with optional indentation. * Used by both flat and modular generators. */ static generateTransientFactoriesSection(classes, constructorArgsResolver, indentation = '', importGenerator) { const transientClasses = this.getTransientClasses(classes); return this.generateCodeSection(transientClasses, (classInfo) => { const constructorArgs = constructorArgsResolver(classInfo); return this.generateTransientFactory(classInfo, constructorArgs, importGenerator); }, indentation); } /** * Generates a section of singleton variable declarations with optional indentation. * Used by both flat and modular generators. */ static generateSingletonVariablesSection(classes, indentation = '', importGenerator) { const singletonClasses = this.getSingletonClasses(classes); const sortedSingletons = this.sortClassesByDependencies(singletonClasses, classes); return this.generateCodeSection(sortedSingletons, (classInfo) => this.generateSingletonVariable(classInfo, importGenerator), indentation); } /** * Generates a section of singleton getter functions with optional indentation. * Used by both flat and modular generators. */ static generateSingletonGettersSection(classes, constructorArgsResolver, indentation = '', importGenerator) { const singletonClasses = this.getSingletonClasses(classes); const sortedSingletons = this.sortClassesByDependencies(singletonClasses, classes); return this.generateCodeSection(sortedSingletons, (classInfo) => { const constructorArgs = constructorArgsResolver(classInfo); return this.generateSingletonGetter(classInfo, constructorArgs, importGenerator); }, indentation, true // multiline content ); } /** * Generates a section of module exports with optional indentation. * Used by modular generators. */ static generateModuleExportsSection(classes, indentation = '', importGenerator) { // Export all classes, not just those with interfaces // Controllers and other concrete classes should also be accessible return this.generateCodeSection(classes, (classInfo) => this.createModuleExportGetter(classInfo, importGenerator), indentation, true // multiline content ); } /** * Generates a container property getter for singleton classes. * Used by flat container generators. */ static generateSingletonContainerProperty(classInfo) { // Prioritize interface name over class name for property access const propertyName = classInfo.interfaceName || classInfo.abstractClassName || classInfo.name; const returnType = classInfo.interfaceName || classInfo.abstractClassName || classInfo.name; const getterCall = this.generateFunctionCall(this.generateGetterName(classInfo.name)); return this.generateContainerProperty(propertyName, returnType, getterCall); } /** * Generates a container property getter for transient classes. * Used by flat container generators. */ static generateTransientContainerProperty(classInfo) { // Prioritize interface name over class name for property access const propertyName = classInfo.interfaceName || classInfo.abstractClassName || classInfo.name; const returnType = classInfo.interfaceName || classInfo.abstractClassName || classInfo.name; const factoryCall = this.generateFunctionCall(this.generateFactoryName(classInfo.name)); return this.generateContainerProperty(propertyName, returnType, factoryCall); } /** * Generates the complete container object with all properties. * Used by flat container generators. */ static generateContainerObject(classes) { const properties = []; for (const classInfo of classes) { if (this.isSingleton(classInfo)) { properties.push(this.generateSingletonContainerProperty(classInfo)); } else if (this.isTransient(classInfo)) { properties.push(this.generateTransientContainerProperty(classInfo)); } } return this.generateObjectExport('container', properties); } /** * Generates the container type export. * Used by flat container generators. */ static generateContainerTypeExport() { return 'export type Container = typeof container;'; } /** * Generates the container type export with path injection utilities. * Used by flat container generators. * @param outputPath Path to the container file to check for existing onInit content */ static generateContainerTypeExportWithPathUtils(outputPath) { const typeExport = this.generateContainerTypeExport(); // Extract preserved onInit body and imports if container exists const preservedContent = outputPath ? container_preservation_utils_1.ContainerPreservationUtils.extractPreservedContent(outputPath) : { onInitBody: undefined, onInitImports: undefined }; const pathUtils = path_injection_utils_1.PathInjectionUtils.generatePathInjectionUtilities(preservedContent.onInitBody, preservedContent.onInitImports); return `${typeExport}\n\n${pathUtils}`; } /** * Generates a module function parameter with proper typing. * Used by modular generators for creating function parameters. */ static generateModuleFunctionParameter(moduleName) { const depVarName = this.toCamelCase(moduleName) + 'Container'; return `${depVarName}: ReturnType<typeof create${moduleName}Container>`; } /** * Generates function parameters for module dependencies. * Used by modular generators. */ static generateModuleFunctionParameters(moduleDeps) { return Array.from(moduleDeps).map(depModule => this.generateModuleFunctionParameter(depModule)); } /** * Generates a module container variable name. * Used by modular generators. */ static generateModuleContainerVariableName(moduleName) { return this.toCamelCase(moduleName) + 'Container'; } /** * Generates a module container function name. * Used by modular generators. */ static generateModuleContainerFunctionName(moduleName) { return `create${moduleName}Container`; } /** * Generates function arguments for module dependencies. * Used by modular generators. */ static generateModuleFunctionArguments(moduleDeps) { return Array.from(moduleDeps).map(depModule => this.generateModuleContainerVariableName(depModule)); } /** * Generates module instantiation code. * Used by modular generators. */ static generateModuleInstantiation(moduleName, moduleDependencies) { const moduleVarName = this.generateModuleContainerVariableName(moduleName); const moduleFunctionName = this.generateModuleContainerFunctionName(moduleName); const moduleDeps = moduleDependencies.get(moduleName) || new Set(); const functionArgs = this.generateModuleFunctionArguments(moduleDeps); const functionCall = this.generateFunctionCall(moduleFunctionName, functionArgs); return `const ${moduleVarName} = ${functionCall};`; } /** * Generates a module export entry for the aggregated container. * Used by container aggregators. */ static generateModuleExportEntry(moduleName) { const moduleVarName = this.generateModuleContainerVariableName(moduleName); const moduleKey = this.generateInstanceName(moduleName); return ` ${moduleKey}: ${moduleVarName}`; } /** * Generates the aggregated container code for modular architecture. * Used by container aggregators. */ static generateAggregatedContainer(sortedModules) { const moduleExports = sortedModules.map(moduleName => this.generateModuleExportEntry(moduleName)); return this.generateObjectExport('container', moduleExports, ','); } /** * Generates the modular container type export. * Used by container aggregators. */ static generateModularContainerTypeExport() { return 'export type Container = typeof container;'; } /** * Generates the modular container type export with path injection utilities. * Used by container aggregators. * @param outputPath Path to the container file to check for existing onInit content */ static generateModularContainerTypeExportWithPathUtils(outputPath) { const typeExport = this.generateModularContainerTypeExport(); // Extract preserved onInit body and imports if container exists const preservedContent = outputPath ? container_preservation_utils_1.ContainerPreservationUtils.extractPreservedContent(outputPath) : { onInitBody: undefined, onInitImports: undefined }; const pathUtils = path_injection_utils_1.PathInjectionUtils.generatePathInjectionUtilities(preservedContent.onInitBody, preservedContent.onInitImports); return `${typeExport}\n\n${pathUtils}`; } /** * Generates a module function return object. * Used by module function body generators. */ static generateModuleFunctionReturnObject(moduleExports) { return moduleExports.length > 0 ? ` return {\n${moduleExports.join(',\n')}\n };` : ' return {};'; } /** * Generates the complete module function body. * Used by module function body generators. */ static generateModuleFunctionBody(moduleClasses, constructorArgsResolver, importGenerator) { const factoryFunctions = this.generateTransientFactoriesSection(moduleClasses, constructorArgsResolver, ' ', importGenerator); const lazyInitializations = this.generateSingletonVariablesSection(moduleClasses, ' ', importGenerator); const lazyGetters = this.generateSingletonGettersSection(moduleClasses, constructorArgsResolver, ' ', importGenerator); const moduleExports = this.generateModuleExportsSection(moduleClasses, ' ', importGenerator); const returnObject = this.generateModuleFunctionReturnObject(moduleExports); return [ ...factoryFunctions, '', ...lazyInitializations, '', ...lazyGetters, '', returnObject ].filter(line => line !== '' || factoryFunctions.length > 0 || lazyInitializations.length > 0 || lazyGetters.length > 0).join('\n'); } } exports.InstantiationUtils = InstantiationUtils; //# sourceMappingURL=instantiation-utils.js.map