@typefixture/jest
Version:
typefixture integration with Jest
465 lines (447 loc) • 18.9 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('ts-morph')) :
typeof define === 'function' && define.amd ? define(['exports', 'ts-morph'], factory) :
(global = global || self, factory(global.typefixtureJest = {}, global.tsMorph));
}(this, function (exports, tsMorph) { 'use strict';
/**
* Supported specimen requests.
*/
var RequestKind;
(function (RequestKind) {
/**
* The number request. The value is ignored.
*/
RequestKind[RequestKind["number"] = 0] = "number";
/**
* The string request. The value is ignored.
*/
RequestKind[RequestKind["string"] = 1] = "string";
/**
* The request is inlined type info request.
*/
RequestKind[RequestKind["typeInfo"] = 2] = "typeInfo";
/**
* The request is type info and value is the type id.
*/
RequestKind[RequestKind["typeInfoId"] = 3] = "typeInfoId";
})(RequestKind || (RequestKind = {}));
/**
* Source processor inlining instrumentation call.
* Instead of putting the type info into a storage it rewrites reference and puts the type info right into the call.
* This works with Karma (because Karma launches a browser for test execution and we do not have to cross process boundaries)
*/
var SourceProcessorInline = /** @class */ (function () {
function SourceProcessorInline(instrumentationContext) {
this.references = [];
this.instrumentationContext = instrumentationContext;
}
/**
* Collects information about all Fixture.create() calls.
*/
SourceProcessorInline.prototype.processSourceFile = function (sourceFile) {
this.references = this.instrumentationContext.fixtureReferenceFinder.findReferences(sourceFile);
};
/**
* Rewrites Fixture.create() call or returns the node unchanged.
*/
SourceProcessorInline.prototype.rewriteNode = function (node) {
var matchedReference = this.references.find(function (r) { return r.match(node); });
return matchedReference ? matchedReference.rewrite(node, this.instrumentationContext.instrumentationWriter) : node;
};
return SourceProcessorInline;
}());
/**
* Supported type recipe requests.
*/
var TypeRecipeRequestKind;
(function (TypeRecipeRequestKind) {
/**
* The number request. The value is ignored.
*/
TypeRecipeRequestKind[TypeRecipeRequestKind["number"] = 0] = "number";
/**
* The string request. The value is ignored.
*/
TypeRecipeRequestKind[TypeRecipeRequestKind["string"] = 1] = "string";
/**
* A recipe request. The value is a type recipe.
*/
TypeRecipeRequestKind[TypeRecipeRequestKind["recipe"] = 2] = "recipe";
})(TypeRecipeRequestKind || (TypeRecipeRequestKind = {}));
/**
* Inline instrumentation
*/
var InstrumentationWriterInline = /** @class */ (function () {
function InstrumentationWriterInline(compilerModule) {
this.compilerModule = compilerModule;
}
/**
* IInstrumentationWriter
*/
InstrumentationWriterInline.prototype.rewrite = function (callExpression, typeRecipe) {
var typeInfoExpression = this.createTypeInfoExpression(typeRecipe);
// Create specimen request expression
var requestProperties = [
this.compilerModule.createPropertyAssignment('kind', this.compilerModule.createLiteral(2)),
this.compilerModule.createPropertyAssignment('value', typeInfoExpression)
];
var result = this.compilerModule.createObjectLiteral(requestProperties);
return result;
};
/**
* Type recipe -> expression with type info
*/
InstrumentationWriterInline.prototype.createTypeInfoExpression = function (typeRecipe) {
var _this = this;
var typeInfoAssignments = [];
// Fields
var memberExpressions = typeRecipe.fields.map(function (f) { return _this.createMemberExpression(f); });
var memberArray = this.compilerModule.createArrayLiteral(memberExpressions);
var fieldAssignments = this.compilerModule.createPropertyAssignment('fields', memberArray);
typeInfoAssignments.push(fieldAssignments);
// Add ctor assignment if available
if (typeRecipe.className) {
var ctorAssignment = this.createCtorAssignment(typeRecipe.className);
typeInfoAssignments.push(ctorAssignment);
}
var result = this.compilerModule.createObjectLiteral(typeInfoAssignments);
return result;
};
InstrumentationWriterInline.prototype.createCtorAssignment = function (className) {
var ctorIdentifier = this.compilerModule.createIdentifier(className);
var result = this.compilerModule.createPropertyAssignment('ctor', ctorIdentifier);
return result;
};
/**
* Creates expression for a member.
*/
InstrumentationWriterInline.prototype.createMemberExpression = function (memberRecipe) {
var requestEpxression = this.createMemberRequestExpression(memberRecipe);
var requestProperties = [
this.compilerModule.createPropertyAssignment('name', this.compilerModule.createLiteral(memberRecipe.name)),
this.compilerModule.createPropertyAssignment('request', requestEpxression)
];
var result = this.compilerModule.createObjectLiteral(requestProperties);
return result;
};
InstrumentationWriterInline.prototype.createMemberRequestExpression = function (memberRecipe) {
var propAssignments = [];
if (memberRecipe.request.kind === TypeRecipeRequestKind.number) {
var kindExpression = this.compilerModule.createPropertyAssignment('kind', this.compilerModule.createLiteral(0));
propAssignments.push(kindExpression);
}
else if (memberRecipe.request.kind === TypeRecipeRequestKind.string) {
var kindExpression = this.compilerModule.createPropertyAssignment('kind', this.compilerModule.createLiteral(1));
propAssignments.push(kindExpression);
}
else if (memberRecipe.request.kind === TypeRecipeRequestKind.recipe) {
var kindExpression = this.compilerModule.createPropertyAssignment('kind', this.compilerModule.createLiteral(2));
propAssignments.push(kindExpression);
var typeInfoExpressionValue = this.createTypeInfoExpression(memberRecipe.request.value);
var valueExpression = this.compilerModule.createPropertyAssignment('value', typeInfoExpressionValue);
propAssignments.push(valueExpression);
}
else {
throw new Error("Unsupported type recipe request: " + memberRecipe.request.kind + ".");
}
var result = this.compilerModule.createObjectLiteral(propAssignments);
return result;
};
return InstrumentationWriterInline;
}());
/**
* Signifies that it's not a type info.
*/
var NoTypeInfo = /** @class */ (function () {
function NoTypeInfo() {
}
return NoTypeInfo;
}());
/**
* Can build type recipe for a class.
*/
var TypeRecipeBuilderClass = /** @class */ (function () {
function TypeRecipeBuilderClass() {
}
/**
* ITypeInfoBuilder
. */
TypeRecipeBuilderClass.prototype.create = function (type, context) {
// Check if the type is a class
var result = new NoTypeInfo();
var symbol = type.getSymbol();
if (symbol) {
var isClass = symbol.getDeclarations().some(function (d) { return tsMorph.TypeGuards.isClassDeclaration(d); });
if (isClass) {
var typeRecipe = {
className: symbol.getFullyQualifiedName(),
fields: []
};
result = typeRecipe;
}
}
return result;
};
return TypeRecipeBuilderClass;
}());
/**
* Responsible for filling fields info.
*/
var TypeTraitBuilderFields = /** @class */ (function () {
function TypeTraitBuilderFields(context) {
this.context = context;
}
/**
* ITypeTraitBuilder
*/
TypeTraitBuilderFields.prototype.build = function (type, typeInfo) {
var _this = this;
var props = type.getProperties();
typeInfo.fields = props.map(function (p) { return _this.createFieldRequest(p); });
};
/**
* Creates request for given member symbol.
*/
TypeTraitBuilderFields.prototype.createFieldRequest = function (propertySymbol) {
var propSignature = propertySymbol.getDeclarations()[0];
var propType = propSignature.getType();
var memberRequest;
if (propType.isString()) {
memberRequest = { kind: TypeRecipeRequestKind.string };
}
else if (propType.isNumber()) {
memberRequest = { kind: TypeRecipeRequestKind.number };
}
else {
memberRequest = {
kind: TypeRecipeRequestKind.recipe,
value: this.context.resolveType(propType)
};
}
var memberRecipe = {
name: propertySymbol.getName(),
request: memberRequest
};
return memberRecipe;
};
return TypeTraitBuilderFields;
}());
/**
* Can build type recipe for an interface.
*/
var TypeRecipeBuilderInterface = /** @class */ (function () {
function TypeRecipeBuilderInterface() {
}
/**
* ITypeInfoBuilder
. */
TypeRecipeBuilderInterface.prototype.create = function (type, context) {
// Check if the type is an interface
var result = new NoTypeInfo();
var symbol = type.getSymbol();
if (symbol) {
var isInterface = symbol.getDeclarations().some(function (d) { return tsMorph.TypeGuards.isInterfaceDeclaration(d); });
if (isInterface) {
var typeRecipe = {
fields: []
};
var fieldsBuilder = new TypeTraitBuilderFields(context);
fieldsBuilder.build(type, typeRecipe);
result = typeRecipe;
}
}
return result;
};
return TypeRecipeBuilderInterface;
}());
/**
* Default implementation for type recipe context.
*/
var TypeRecipeContext = /** @class */ (function () {
function TypeRecipeContext() {
}
/**
* ITypeRecipeContext
*/
TypeRecipeContext.prototype.resolveType = function (type) {
var builder = new TypeRecipeBuilder();
return builder.build(type);
};
return TypeRecipeContext;
}());
var TypeRecipeBuilder = /** @class */ (function () {
function TypeRecipeBuilder() {
}
/**
* Type recipe builder root.
*/
TypeRecipeBuilder.prototype.build = function (type) {
var result;
var builders = [
new TypeRecipeBuilderClass(),
new TypeRecipeBuilderInterface()
];
var context = new TypeRecipeContext();
for (var _i = 0, builders_1 = builders; _i < builders_1.length; _i++) {
var builder = builders_1[_i];
var resolved = builder.create(type, context);
if (!(resolved instanceof NoTypeInfo)) {
result = resolved;
break;
}
}
if (!result) {
throw new Error('Cannot build type recipe.');
}
return result;
};
return TypeRecipeBuilder;
}());
/**
* Encapsulates info about reference to fixture.create() call.
*/
var FixtureCallReference = /** @class */ (function () {
function FixtureCallReference(compilerModule, callExpression, typeChecker) {
this.callExpression = callExpression;
this.compilerModule = compilerModule;
this.typeChecker = typeChecker;
}
/**
* Checks if the given node matches to the reference.
* @param node A node in source file.
*/
FixtureCallReference.prototype.match = function (node) {
var result = this.callExpression.compilerNode.pos === node.getFullStart()
&& this.compilerModule.isCallExpression(node)
? true
: false;
return result;
};
/**
* Performs type info rewrite for the node.
*/
FixtureCallReference.prototype.rewrite = function (node, instrumentationWriter) {
var callExpression = node;
var typeRecipe = this.createTypeRecipe();
var newParam = instrumentationWriter.rewrite(callExpression, typeRecipe);
return this.compilerModule.updateCall(callExpression, callExpression.expression, callExpression.typeArguments, [newParam]);
};
/**
* Creates type recipe from fixture.create<T>() expression.
*/
FixtureCallReference.prototype.createTypeRecipe = function () {
var typeNode = this.callExpression.getTypeArguments()[0];
var targetType = this.typeChecker.getTypeAtLocation(typeNode);
var recipeBuilder = new TypeRecipeBuilder();
var typeRecipe = recipeBuilder.build(targetType);
return typeRecipe;
};
return FixtureCallReference;
}());
/**
* Reference finder default implementation.
*/
var FixtureReferenceFinder = /** @class */ (function () {
function FixtureReferenceFinder(compilerModule) {
this.compilerModule = compilerModule;
}
/**
* IFixtureReferenceFinder
*/
FixtureReferenceFinder.prototype.findReferences = function (sourceFile) {
var _this = this;
var result = [];
var project = new tsMorph.Project({
addFilesFromTsConfig: false,
});
// Try obtain symbol for Fixture.create() and find all references (call expressions)
var sf = project.addExistingSourceFile(sourceFile.fileName);
var importDeclaration = sf.getImportDeclaration('@typefixture/core');
if (importDeclaration) {
var fixtureClassImport = importDeclaration.getNamedImports().find(function (i) { return i.getName() === 'Fixture'; });
if (fixtureClassImport) {
var nameNode = fixtureClassImport.getNameNode();
var typeChecker_1 = project.getTypeChecker();
var fixtureType = typeChecker_1.getTypeAtLocation(nameNode);
if (fixtureType) {
var fixtureClassSymbol = fixtureType.getSymbolOrThrow();
var fixtureClassDeclaration = fixtureClassSymbol.getDeclarations()[0];
var methodDecl = fixtureClassDeclaration.getMethodOrThrow('create');
var nodes = methodDecl.findReferencesAsNodes();
result = nodes.map(function (n) {
var callExp = n.getFirstAncestorByKindOrThrow(tsMorph.ts.SyntaxKind.CallExpression);
return new FixtureCallReference(_this.compilerModule, callExp, typeChecker_1);
});
}
}
}
return result;
};
return FixtureReferenceFinder;
}());
/**
* Integrated into ts-jest pipeline.
*/
var InstrumentationTransformer = /** @class */ (function () {
function InstrumentationTransformer(compilerModule) {
this.compilerModule = compilerModule;
}
InstrumentationTransformer.prototype.transform = function (context) {
var _this = this;
return function (sourceFile) {
console.log('Transforming file: ' + sourceFile.fileName);
// Find all references and collect type info
var instrumentationWriter = new InstrumentationWriterInline(_this.compilerModule);
var fixtureReferenceFinder = new FixtureReferenceFinder(_this.compilerModule);
var instrumentationContext = {
instrumentationWriter: instrumentationWriter,
fixtureReferenceFinder: fixtureReferenceFinder
};
var sourceProcessor = new SourceProcessorInline(instrumentationContext);
sourceProcessor.processSourceFile(sourceFile);
// Start transformation
var result = _this.compilerModule.visitNode(sourceFile, function (node) { return _this.visitNode(node, context, sourceProcessor); });
return result;
};
};
InstrumentationTransformer.prototype.visitNodeAndChildren = function (node, ctx, sourceProcessor) {
var _this = this;
return this.compilerModule.visitEachChild(this.visitNode(node, ctx, sourceProcessor), function (childNode) { return _this.visitNodeAndChildren(childNode, ctx, sourceProcessor); }, ctx);
};
InstrumentationTransformer.prototype.visitNode = function (node, ctx, sourceProcessor) {
var _this = this;
var newNode = sourceProcessor.rewriteNode(node);
if (newNode != node) {
return newNode;
}
else {
return this.compilerModule.visitEachChild(node, function (node) { return _this.visitNode(node, ctx, sourceProcessor); }, ctx);
}
};
return InstrumentationTransformer;
}());
/**
* ts-jest API
*/
var name = 'typefixture-transformer';
// ts-jest API, increment this each time the code is modified
/**
* @internal
*/
var version = 1;
/**
* The factory of transformer factory
* @param cs Current jest configuration-set
*/
function factory(cs) {
return function (ctx) {
var transformer = new InstrumentationTransformer(cs.compilerModule);
return transformer.transform(ctx);
};
}
exports.factory = factory;
exports.name = name;
exports.version = version;
Object.defineProperty(exports, '__esModule', { value: true });
}));
//# sourceMappingURL=typefixture-jest.umd.js.map