UNPKG

ts-bakery

Version:

Baked dependency injection for Typescript.

220 lines 11.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const typescript = require("typescript"); const DependencyRegistrationExpressionRequirement_1 = require("../NodeSequencing/Requirements/DependencyRegistrationExpressionRequirement"); const NamedPropertyAccessExpressionRequirement_1 = require("../NodeSequencing/Requirements/NamedPropertyAccessExpressionRequirement"); const ThisKeywordRequirement_1 = require("../NodeSequencing/Requirements/ThisKeywordRequirement"); const TsNodeSequenceMatch_1 = require("../NodeSequencing/TsNodeSequenceMatch"); const FunctionNames_1 = require("../Util/FunctionNames"); const Paths_1 = require("../Util/Paths"); const DependencyConfiguration_1 = require("./DependencyConfiguration"); const DependencyMapping_1 = require("./DependencyMapping"); const RegisteredBindingDefinition_1 = require("./RegisteredBindingDefinition"); class DependencyConfigurationBuilder { constructor(scriptIndex, bakeryConverter, nodeConverter) { this.scriptIndex = scriptIndex; this.bakeryConverter = bakeryConverter; this.nodeConverter = nodeConverter; } buildConfiguration() { const configuration = new DependencyConfiguration_1.default(); const resolverImplementation = this.getResolverImplementation(); if (resolverImplementation == null) { throw new Error('Unable to load DependencyResolver implementation.'); } const resolverModules = this.getRegisteredResolverModules(resolverImplementation); for (const resolverModule of resolverModules) { const bindingDefinitions = this.resolveBindingsFromModule(resolverModule); for (const bindingDefinition of bindingDefinitions) { const implementationIndex = this.getClassNameIndex(configuration, bindingDefinition.implementation.name); const abstractionIndex = bindingDefinition.abstraction != null ? this.getClassNameIndex(configuration, bindingDefinition.abstraction.name) : -1; // Add interface binding if there's an abstraction if (abstractionIndex !== -1) { this.addInterfaceBinding(configuration, abstractionIndex, implementationIndex); } const mapping = this.getDependencyMapping(bindingDefinition.implementation, configuration); if (mapping != null) { // Always add to requestedDependencies, even if no dependencies (empty array) this.addRequestedDependency(configuration, mapping.implementationIndex, mapping.dependencyIndices); } } } return configuration; } getClassNameIndex(configuration, className) { let index = configuration.dependencies.indexOf(className); if (index === -1) { index = configuration.dependencies.length; configuration.dependencies.push(className); } return index; } addRequestedDependency(configuration, implementationIndex, dependencyIndices) { configuration.requestedDependencies.push([implementationIndex, dependencyIndices]); } addInterfaceBinding(configuration, abstractionIndex, implementationIndex) { configuration.interfaceBindings.push([abstractionIndex, implementationIndex]); } resolveBindingsFromModule(resolverModule) { const filePath = resolverModule.getFilePath(); const originalFile = this.scriptIndex.getFile(filePath); //TODO: Double check inheritance / use NodeSequenceMatching const registerMethod = resolverModule.methods.find(m => m.name == 'registerDependencies'); if (registerMethod == null) { console.error('not a resolver: ' + resolverModule.name); return []; } const node = this.nodeConverter.convertToMethodDeclaration(registerMethod.node); if (node.body == null) { console.warn(`ResolverModule ${resolverModule.name} is empty.`); return []; } const registrationMatch = new TsNodeSequenceMatch_1.default(); registrationMatch.addRequirement(new ThisKeywordRequirement_1.default()); registrationMatch.addRequirement(new NamedPropertyAccessExpressionRequirement_1.default('register')); registrationMatch.addRequirement(new DependencyRegistrationExpressionRequirement_1.default()); const bindings = []; const body = node.body; for (const statement of body.statements) { if (statement.kind != typescript.SyntaxKind.ExpressionStatement) { continue; } const expressionStatement = statement; const sequence = this.bakeryConverter.convertExpression(expressionStatement.expression); const result = registrationMatch.match(sequence); const binding = this.resolveBinding(result, originalFile); if (binding != null) { bindings.push(binding); } } return bindings; } resolveBinding(result, originalFile) { if (!result.success) { return null; } const callExpression = result.matchedNodes.find(n => n.kind == typescript.SyntaxKind.CallExpression); const argument = callExpression.arguments[0]; const typeReference = this.bakeryConverter.convertNodeToTypeReference(argument); if (typeReference == null) { return null; } const classDefintion = this.scriptIndex.resolveTypeReference(typeReference, originalFile); if (classDefintion == null) { return null; } let typeArgument = null; if (callExpression.typeArguments != null) { const argument = callExpression.typeArguments[0]; const typeReference = this.bakeryConverter.convertTypeNode(argument); typeArgument = this.scriptIndex.resolveTypeReference(typeReference, originalFile); if (typeArgument == null) { throw new Error(`Unable to resolve type argument for binding of ${classDefintion.name}.`); } } const binding = new RegisteredBindingDefinition_1.default(); binding.implementation = classDefintion; binding.abstraction = typeArgument; return binding; } getDependencyMapping(classDefinition, configuration) { const mapping = new DependencyMapping_1.default(); const sourceFilePath = classDefinition.getFilePath(); const sourceFile = this.scriptIndex.getFile(sourceFilePath); mapping.implementation = classDefinition.name; mapping.implementationIndex = this.getClassNameIndex(configuration, mapping.implementation); const injectMethod = this.getInjectMethod(classDefinition); if (injectMethod != null) { // Method injection const parameters = injectMethod.parameters; const parameterTypeNames = this.getParameterTypeNames(parameters, sourceFile); mapping.dependencies.push(...parameterTypeNames); mapping.dependencyIndices = parameterTypeNames.map(name => this.getClassNameIndex(configuration, name)); return mapping; } if (classDefinition.classConstructor != null) { // Constructor injection const constructorParameters = classDefinition.classConstructor.parameters; const parameterTypeNames = this.getParameterTypeNames(constructorParameters, sourceFile); mapping.dependencies.push(...parameterTypeNames); mapping.dependencyIndices = parameterTypeNames.map(name => this.getClassNameIndex(configuration, name)); return mapping; } //TODO: Check if class inherits from injectable class // Return empty mapping for classes without dependencies return mapping; } getInjectMethod(classDefinition) { for (const method of classDefinition.methods) { const injectDecorator = method.decorators.find(d => d.name == FunctionNames_1.default.InjectDecorator); if (injectDecorator != null) { return method; } } return null; } getParameterTypeNames(parameters, sourceFile) { const typeNames = []; for (const parameter of parameters) { const resolvedType = this.scriptIndex.resolveTypeReference(parameter.type, sourceFile); typeNames.push(resolvedType.name); } return typeNames; } getResolverImplementation() { const resolverDeclaration = this.bakeryConverter.convertPath(Paths_1.default.DependencyResolverDeclaration); const resolverClass = resolverDeclaration.classes[0]; //TODO: check for tsconfig, parse tsconfig for included/excluded dirs const allInheriting = this.scriptIndex.getInheritingClasses(resolverClass); if (allInheriting.length == 0) { return null; } return allInheriting[0]; } getRegisteredResolverModules(resolverClass) { const registerModulesFunction = resolverClass.methods.find(f => f.name == FunctionNames_1.default.RegisterModules); if (registerModulesFunction == null) { return; } const node = this.nodeConverter.convertToMethodDeclaration(registerModulesFunction.node); return this.inpectClassMember(node, resolverClass); } inpectClassMember(methodDeclaration, resolverClass) { if (methodDeclaration.body == null) { return null; } const modules = []; for (const statement of methodDeclaration.body.statements) { const moduleClassDefinition = this.inspectMethodStatement(statement, resolverClass); if (moduleClassDefinition != null) { modules.push(moduleClassDefinition); } } return modules; } inspectMethodStatement(statement, resolverClass) { if (statement.kind != typescript.SyntaxKind.ExpressionStatement) { return null; } const sourcePath = resolverClass.getFilePath(); const sourceFile = this.scriptIndex.getFile(sourcePath); const expressionStatement = statement; const callExpression = expressionStatement.expression; if (callExpression.arguments.length != 1) { throw new Error(`Interpretation Error in ${sourcePath} (Invalid number of arguments).`); } const propAccExcp = callExpression.expression; const functionName = this.bakeryConverter.convertIdentifier(propAccExcp.name); if (functionName != FunctionNames_1.default.RegisterModule) { return null; } const arg = callExpression.arguments[0]; const typeReference = this.bakeryConverter.convertNodeToTypeReference(arg); if (typeReference == null) { throw new Error(`Interpretation Error in ${sourcePath} (Unable to interpret type reference).`); } return this.scriptIndex.resolveTypeReference(typeReference, sourceFile); } } exports.default = DependencyConfigurationBuilder; //# sourceMappingURL=DependencyConfigurationBuilder.js.map