ts-bakery
Version:
Baked dependency injection for Typescript.
202 lines • 10 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 DependencyBinding_1 = require("./DependencyBinding");
const DependencyConfiguration_1 = require("./DependencyConfiguration");
const DependencyMapping_1 = require("./DependencyMapping");
const RegisteredBindingDefinition_1 = require("./RegisteredBindingDefinition");
const fs = require("fs");
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 binding = new DependencyBinding_1.default();
binding.implementation = bindingDefinition.implementation.name;
binding.abstraction = bindingDefinition.abstraction != null ? bindingDefinition.abstraction.name : null;
configuration.bindings.push(binding);
const mapping = this.getDependencyMapping(bindingDefinition.implementation);
if (mapping != null && mapping.dependencies.length > 0) {
configuration.mappings.push(mapping);
}
}
}
const jsonContent = JSON.stringify(configuration);
fs.writeFileSync(Paths_1.default.BakeryConfig, jsonContent);
}
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) {
const mapping = new DependencyMapping_1.default();
const sourceFilePath = classDefinition.getFilePath();
const sourceFile = this.scriptIndex.getFile(sourceFilePath);
mapping.implementation = classDefinition.name;
const injectMethod = this.getInjectMethod(classDefinition);
if (injectMethod != null) {
// Method injection
const parameters = injectMethod.parameters;
const parameterTypeNames = this.getParameterTypeNames(parameters, sourceFile);
mapping.dependencies.push(...parameterTypeNames);
return mapping;
}
if (classDefinition.classConstructor != null) {
// Constructor injection
const constructorParameters = classDefinition.classConstructor.parameters;
const parameterTypeNames = this.getParameterTypeNames(constructorParameters, sourceFile);
mapping.dependencies.push(...parameterTypeNames);
return mapping;
}
//TODO: Check if class inherits from injectable class
return null;
}
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