@travetto/di
Version:
Dependency registration/management and injection support.
197 lines (166 loc) • 6.29 kB
text/typescript
import ts from 'typescript';
import {
TransformerState, DecoratorMeta, OnClass, OnProperty, OnStaticMethod, DecoratorUtil, LiteralUtil, OnSetter
} from '@travetto/transformer';
import { ForeignType } from '@travetto/transformer/src/resolver/types';
const INJECTABLE_MOD = '@travetto/di/src/decorator';
/**
* Injectable/Injection transformer
*/
export class InjectableTransformer {
static getForeignTarget(state: TransformerState, ret: ForeignType): ts.Expression {
return state.fromLiteral({
Ⲑid: `${ret.source.split('node_modules/')[1]}+${ret.name}`
});
}
/**
* Handle a specific declaration param/property
*/
static processDeclaration(state: TransformerState, param: ts.ParameterDeclaration | ts.SetAccessorDeclaration | ts.PropertyDeclaration): ts.Expression[] {
const existing = state.findDecorator(this, param, 'Inject', INJECTABLE_MOD);
if (!(existing || ts.isParameter(param))) {
return [];
}
const args: ts.Expression[] = [];
if (existing && ts.isCallExpression(existing.expression)) {
args.push(...existing.expression.arguments);
}
const payload: { target?: unknown, qualifier?: unknown, optional?: boolean } = {};
if (!!param.questionToken) {
payload.optional = true;
}
const keyParam = ts.isSetAccessorDeclaration(param) ? param.parameters[0] : param;
const type = state.resolveType(keyParam);
if (type.key === 'managed') {
payload.target = state.getOrImport(type);
} else if (type.key === 'foreign') {
payload.target = this.getForeignTarget(state, type);
} else {
const file = param.getSourceFile().fileName;
const src = state.getFileImportName(file);
throw new Error(`Unable to import non-external type: ${param.getText()} ${type.key}: ${src}`);
}
args.unshift(state.fromLiteral(payload));
return args;
}
/**
* Mark class as Injectable
*/
('Injectable')
static registerInjectable(state: TransformerState, node: ts.ClassDeclaration): typeof node {
const cons = node.members.find((x): x is ts.ConstructorDeclaration => ts.isConstructorDeclaration(x));
const injectArgs = cons &&
state.fromLiteral(cons.parameters.map(x => this.processDeclaration(state, x)));
// Extract all interfaces
const interfaces: ts.Node[] = [];
for (const clause of node.heritageClauses ?? []) {
if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
for (const typeExpression of clause.types) {
const resolvedType = state.resolveType(typeExpression);
if (resolvedType.key === 'managed') {
const resolved = state.getOrImport(resolvedType);
interfaces.push(resolved);
}
}
}
}
// Add injectable decorator if not there
const decl = state.findDecorator(this, node, 'Injectable', INJECTABLE_MOD);
const args = decl && ts.isCallExpression(decl.expression) ? decl.expression.arguments : [undefined];
return state.factory.updateClassDeclaration(node,
DecoratorUtil.spliceDecorators(node, decl, [
state.createDecorator(INJECTABLE_MOD, 'Injectable', ...args, LiteralUtil.extendObjectLiteral(ts.factory, {}, {
interfaces
})),
state.createDecorator(INJECTABLE_MOD, 'InjectArgs', injectArgs)
]),
node.name,
node.typeParameters,
node.heritageClauses,
node.members
);
}
/**
* Handle Inject annotations for fields/args
*/
('Inject')
static registerInjectProperty(state: TransformerState, node: ts.PropertyDeclaration, dm?: DecoratorMeta): typeof node {
const decl = state.findDecorator(this, node, 'Inject', INJECTABLE_MOD);
// Doing decls
return state.factory.updatePropertyDeclaration(
node,
DecoratorUtil.spliceDecorators(node, decl, [
state.createDecorator(INJECTABLE_MOD, 'Inject', ...this.processDeclaration(state, node)),
], 0),
node.name,
node.questionToken,
node.type,
node.initializer
);
}
/**
* Handle Inject annotations for fields/args
*/
('Inject')
static registerInjectSetter(state: TransformerState, node: ts.SetAccessorDeclaration, dm?: DecoratorMeta): typeof node {
const decl = state.findDecorator(this, node, 'Inject', INJECTABLE_MOD);
const modifiers = DecoratorUtil.spliceDecorators(node, decl, [
state.createDecorator(INJECTABLE_MOD, 'Inject', ...this.processDeclaration(state, node)),
], 0);
// Doing decls
return state.factory.updateSetAccessorDeclaration(
node,
modifiers,
node.name,
node.parameters,
node.body
);
}
/**
* Handle InjectableFactory creation
*/
('InjectableFactory')
static registerFactory(state: TransformerState, node: ts.MethodDeclaration, dm?: DecoratorMeta): typeof node {
if (!dm?.dec) {
return node;
}
const parent = node.parent;
if (ts.isObjectLiteralExpression(parent)) {
return node;
}
const dec = dm?.dec;
// Extract config
const dependencies = node.parameters.map(x => this.processDeclaration(state, x));
// Read target from config or resolve
const config: { dependencies: unknown[], target?: unknown, qualifier?: unknown, src?: unknown } = {
dependencies,
src: parent.name,
};
let ret = state.resolveReturnType(node);
if (ret.key === 'literal' && ret.ctor === Promise && ret.typeArguments) {
ret = ret.typeArguments![0];
}
if (ret.key === 'managed') {
config.target = state.getOrImport(ret);
} else if (ret.key === 'foreign') {
config.target = this.getForeignTarget(state, ret);
}
// Build decl
const args = [...(dec && ts.isCallExpression(dec.expression) ? dec.expression.arguments : [undefined])];
args.unshift(state.extendObjectLiteral(config));
// Replace decorator
return state.factory.updateMethodDeclaration(
node,
DecoratorUtil.spliceDecorators(node, dec, [
state.createDecorator(INJECTABLE_MOD, 'InjectableFactory', ...args)
]),
node.asteriskToken,
node.name,
node.questionToken,
node.typeParameters,
node.parameters,
node.type,
node.body
);
}
}