UNPKG

@typefixture/jest

Version:

typefixture integration with Jest

465 lines (447 loc) 18.9 kB
(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