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