UNPKG

@notjustcoders/ioc-arise

Version:

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

325 lines 17 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FactoryAnalyzer = void 0; const path_1 = require("path"); const container_1 = require("../container"); const logger_1 = require("../utils/logger"); class FactoryAnalyzer { constructor(sourceDir, factoryPattern) { this.astParser = container_1.container.astParser; this.sourceDir = sourceDir; if (factoryPattern) { try { this.factoryPattern = new RegExp(factoryPattern, 'i'); } catch (error) { logger_1.Logger.warn(`Invalid factory pattern regex: ${factoryPattern}. Using default behavior.`); } } } async collectTokens(filePaths) { const factoryNames = new Set(); const interfaces = new Set(); for (const filePath of filePaths) { try { const root = this.astParser.parseFile(filePath); // Collect interfaces const extractedInterfaces = this.astParser.extractInterfaces(root); extractedInterfaces.forEach(i => interfaces.add(i)); // Collect type aliases const typeAliases = this.astParser.extractTypeAliases(root); for (const [alias] of typeAliases.entries()) { interfaces.add(alias); } // Collect names re-exported from external packages (e.g. from a monorepo sibling) const externalReExports = this.astParser.extractExternalReExports(root); externalReExports.forEach(name => interfaces.add(name)); // Collect factory names (exported functions with @factory or pattern match) const functionNodes = this.astParser.findAllFunctions(root); for (const functionNode of functionNodes) { const functionName = this.astParser.extractFunctionName(functionNode); if (functionName) { const isExported = this.astParser.isExportedFunction(functionNode); const hasFactoryAnnotation = this.hasFactoryAnnotation(root, functionNode); const matchesFactoryPattern = this.factoryPattern ? this.factoryPattern.test(functionName) : false; const returnType = this.astParser.extractFunctionReturnType(functionNode); const instanceFactoryFor = returnType ? this.extractCoreInterfaceName(returnType, interfaces) : undefined; if (isExported && (hasFactoryAnnotation || matchesFactoryPattern || instanceFactoryFor)) { factoryNames.add(functionName); if (instanceFactoryFor) { interfaces.add(instanceFactoryFor); } } } } } catch (error) { logger_1.Logger.warn(`Warning: Could not collect tokens from ${filePath}:`, { error }); } } return { factoryNames, interfaces }; } async analyzeFiles(filePaths, tokens) { const allFactories = []; let allInterfaces = tokens?.interfaces || new Set(); let allClassNames = tokens?.classNames || new Set(); let allFactoryNames = tokens?.factoryNames || new Set(); const fileASTMap = new Map(); if (!tokens) { // First pass: Collect types for dependency resolution for (const filePath of filePaths) { try { const root = this.astParser.parseFile(filePath); fileASTMap.set(filePath, root); const interfaces = this.astParser.extractInterfaces(root); interfaces.forEach(interfaceName => allInterfaces.add(interfaceName)); // Collect names re-exported from external packages const externalReExports = this.astParser.extractExternalReExports(root); externalReExports.forEach(name => allInterfaces.add(name)); const classNodes = this.astParser.findAllClasses(root); for (const classNode of classNodes) { const className = this.astParser.extractClassName(classNode); if (className) { allClassNames.add(className); } } // Collect factory names (exported functions with @factory or pattern match) const functionNodes = this.astParser.findAllFunctions(root); for (const functionNode of functionNodes) { const functionName = this.astParser.extractFunctionName(functionNode); if (functionName) { const isExported = this.astParser.isExportedFunction(functionNode); const hasFactoryAnnotation = this.hasFactoryAnnotation(root, functionNode); const matchesFactoryPattern = this.factoryPattern ? this.factoryPattern.test(functionName) : false; const returnType = this.astParser.extractFunctionReturnType(functionNode); const instanceFactoryFor = returnType ? this.extractCoreInterfaceName(returnType, allInterfaces) : undefined; if (isExported && (hasFactoryAnnotation || matchesFactoryPattern || instanceFactoryFor)) { allFactoryNames.add(functionName); } } } } catch (error) { logger_1.Logger.warn(`Warning: Could not parse ${filePath}:`, { error }); } } } // Second pass: Analyze factory functions for (const filePath of filePaths) { let root = fileASTMap.get(filePath); if (!root) { try { root = this.astParser.parseFile(filePath); } catch (e) { continue; } } if (root) { const factories = await this.analyzeFileFromAST(filePath, root, allInterfaces, allClassNames, allFactoryNames); allFactories.push(...factories); } } return allFactories; } async analyzeFileFromAST(filePath, root, allInterfaces, allClassNames, allFactoryNames) { const factories = []; try { const importMappings = this.astParser.extractImportMappings(root); const jsDocScopes = this.astParser.extractJSDocComments(root); const functionNodes = this.astParser.findAllFunctions(root); logger_1.Logger.debug(`Found ${functionNodes.length} function nodes in ${filePath}`); for (const functionNode of functionNodes) { const functionName = this.astParser.extractFunctionName(functionNode); logger_1.Logger.debug(`Extracted function name: ${functionName} from node: ${functionNode.text().substring(0, 100)}`); if (!functionName) { continue; } // Only analyze exported functions that are factories // Factory functions are detected by: // 1. @factory JSDoc comment (default behavior) // 2. Function name matching factoryPattern regex (if configured) const isExported = this.astParser.isExportedFunction(functionNode); const hasFactoryAnnotation = this.hasFactoryAnnotation(root, functionNode); const matchesFactoryPattern = this.factoryPattern ? this.factoryPattern.test(functionName) : false; // Detect instance factories: exported functions with an explicit return type // matching a known interface (e.g., ): IUserRepository {) const returnType = this.astParser.extractFunctionReturnType(functionNode); const instanceFactoryFor = returnType ? this.extractCoreInterfaceName(returnType, allInterfaces) : undefined; logger_1.Logger.debug(`Function ${functionName}: isExported=${isExported}, hasFactoryAnnotation=${hasFactoryAnnotation}, matchesFactoryPattern=${matchesFactoryPattern}, instanceFactoryFor=${instanceFactoryFor}`); if (!isExported || (!hasFactoryAnnotation && !matchesFactoryPattern && !instanceFactoryFor)) { logger_1.Logger.debug(`Skipping function ${functionName}`); continue; } const parameters = this.astParser.extractFunctionParameters(functionNode); // Instance factories default to transient (they produce new instances on demand). // Regular @factory / pattern-matched factories default to singleton. // Either default can be overridden with /** @scope transient|singleton */ const defaultScope = instanceFactoryFor ? 'transient' : 'singleton'; const scope = jsDocScopes.get(functionName) || defaultScope; // Check if this is a context object pattern (single parameter with object type) const isContextObjectPattern = parameters.length === 1 && parameters[0] !== undefined && parameters[0].type.startsWith('{') && parameters[0].type.includes(':'); let dependencies = []; let contextObjectName; let contextObjectProperties; if (isContextObjectPattern && parameters[0]) { // Extract properties from context object const contextParam = parameters[0]; contextObjectName = contextParam.name; contextObjectProperties = this.astParser.extractObjectTypeProperties(functionNode, contextParam.name); // Resolve dependencies from context object properties dependencies = this.resolveDependenciesFromContextProperties(contextObjectProperties || [], allInterfaces, allClassNames, allFactoryNames, importMappings); } else { // Resolve dependencies from individual parameters (existing behavior) dependencies = this.resolveDependencies(parameters, allInterfaces, allClassNames, allFactoryNames, importMappings); } // Calculate import path const importPath = this.calculateImportPath(filePath); // Determine token: // - Instance factories use the interface name as the token (they are implementation providers) // - Regular factories use the function name as the token const token = instanceFactoryFor || functionName; factories.push({ name: functionName, filePath, dependencies, parameters, returnType, importPath, scope, token, useContextObject: isContextObjectPattern, contextObjectName, contextObjectProperties, instanceFactoryFor, }); } } catch (error) { logger_1.Logger.warn(`Warning: Could not analyze factories in ${filePath}:`, { error }); } return factories; } hasFactoryAnnotation(root, functionNode) { try { // Find all JSDoc comments in the file const comments = root.findAll({ rule: { kind: 'comment' } }); const functionRange = functionNode.range(); const functionStartLine = functionRange.start.line; // Find the JSDoc comment that appears immediately before this function for (const comment of comments) { const commentText = comment.text(); if (commentText.includes('/**') && commentText.includes('@factory')) { const commentRange = comment.range(); const commentEndLine = commentRange.end.line; // Check if the comment comes before the function (within a few lines) if (commentEndLine < functionStartLine && functionStartLine - commentEndLine <= 2) { return true; } } } return false; } catch { return false; } } resolveDependencies(parameters, allInterfaces, allClassNames, allFactoryNames, importMappings) { const dependencies = []; for (const param of parameters) { const typeName = param.type; // Skip primitive types if (['string', 'number', 'boolean', 'Date', 'Object', 'Array'].includes(typeName)) { continue; } // Skip object types (they're handled separately) if (typeName.startsWith('{')) { continue; } // Determine if it's an interface, class, or factory const isInterface = allInterfaces.has(typeName); const isClass = allClassNames.has(typeName); const isFactory = allFactoryNames.has(typeName); // Also accept types imported directly from external packages (non-relative path) const rawImportPath = importMappings.get(typeName); const isExternalImport = rawImportPath !== undefined && !rawImportPath.startsWith('.'); if (isInterface || isClass || isFactory || isExternalImport) { const importPath = rawImportPath || './'; dependencies.push({ name: typeName, importPath, }); } } return dependencies; } resolveDependenciesFromContextProperties(properties, allInterfaces, allClassNames, allFactoryNames, importMappings) { const dependencies = []; for (const prop of properties) { const typeName = prop.type; // Skip primitive types if (['string', 'number', 'boolean', 'Date', 'Object', 'Array'].includes(typeName)) { continue; } // Determine if it's an interface, class, or factory const isInterface = allInterfaces.has(typeName); const isClass = allClassNames.has(typeName); const isFactory = allFactoryNames.has(typeName); // Also accept types imported directly from external packages (non-relative path) const rawImportPath = importMappings.get(typeName); const isExternalImport = rawImportPath !== undefined && !rawImportPath.startsWith('.'); if (isInterface || isClass || isFactory || isExternalImport) { const importPath = rawImportPath || './'; dependencies.push({ name: typeName, importPath, }); } } return dependencies; } calculateImportPath(filePath) { const relativePath = (0, path_1.relative)(this.sourceDir, filePath); // Convert to import path format (remove .ts extension, handle directory separators) return relativePath .replace(/\.ts$/, '') .replace(/\\/g, '/') .replace(/^\.\//, ''); } /** * Extracts the core interface name from a return type annotation. * Handles: IFoo, Promise<IFoo>, Awaited<IFoo>, Awaited<Promise<IFoo>> * Returns the interface name if it matches a known interface, otherwise null. */ extractCoreInterfaceName(returnType, allInterfaces) { const trimmed = returnType.trim(); // Direct: IFoo if (allInterfaces.has(trimmed)) { return trimmed; } // Promise<IFoo> const promiseMatch = trimmed.match(/^Promise\s*<\s*([A-Za-z_][A-Za-z0-9_]*)\s*>$/); if (promiseMatch?.[1] && allInterfaces.has(promiseMatch[1])) { return promiseMatch[1]; } // Awaited<IFoo> or Awaited<Promise<IFoo>> const awaitedMatch = trimmed.match(/^Awaited\s*<\s*(?:Promise\s*<\s*)?([A-Za-z_][A-Za-z0-9_]*)\s*>?\s*>$/); if (awaitedMatch?.[1] && allInterfaces.has(awaitedMatch[1])) { return awaitedMatch[1]; } return undefined; } } exports.FactoryAnalyzer = FactoryAnalyzer; //# sourceMappingURL=factory-analyzer.js.map