ts-bakery
Version:
Baked dependency injection for Typescript.
220 lines • 11.3 kB
JavaScript
"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