From 6272b7f5f221786fa25edbd5c7e312551ed72332 Mon Sep 17 00:00:00 2001 From: Thomas Wirth Date: Tue, 11 Jun 2019 00:56:55 +0200 Subject: [PATCH] feat: transform 'styles' only in decorator Until now we transformed 'templateUrl', 'styleUrls' and 'styles' everywhere they were assigned. The new implementation of the AstTransformer splits it in two: * InlineFilesTransformer, which inlines `templateUrl` and removes `styleUrls` files * StripStylesTransformer, which removes the `styles` property, but only inside the `@Component` decorator --- .gitignore | 4 +- ...test.ts => InlineFilesTransformer.test.ts} | 102 +++++++----- __tests__/StripStylesTransformer.test.ts | 68 ++++++++ ...ap => InlineFilesTransformer.test.ts.snap} | 23 ++- .../StripStylesTransformer.test.ts.snap | 46 ++++++ jest-preset.js | 5 +- ...ansformer.ts => InlineFilesTransformer.ts} | 134 ++++++++-------- src/StripStylesTransformer.ts | 149 ++++++++++++++++++ src/TransformUtils.ts | 26 +++ 9 files changed, 445 insertions(+), 112 deletions(-) rename __tests__/{InlineHtmlStripStylesTransformer.test.ts => InlineFilesTransformer.test.ts} (63%) create mode 100644 __tests__/StripStylesTransformer.test.ts rename __tests__/__snapshots__/{InlineHtmlStripStylesTransformer.test.ts.snap => InlineFilesTransformer.test.ts.snap} (93%) create mode 100644 __tests__/__snapshots__/StripStylesTransformer.test.ts.snap rename src/{InlineHtmlStripStylesTransformer.ts => InlineFilesTransformer.ts} (54%) create mode 100644 src/StripStylesTransformer.ts create mode 100644 src/TransformUtils.ts diff --git a/.gitignore b/.gitignore index 10aa828c4f..c88ec1d91b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules -InlineHtmlStripStylesTransformer.js +InlineFilesTransformer.js +StripStylesTransformer.js +TransformUtils.js *.log .idea diff --git a/__tests__/InlineHtmlStripStylesTransformer.test.ts b/__tests__/InlineFilesTransformer.test.ts similarity index 63% rename from __tests__/InlineHtmlStripStylesTransformer.test.ts rename to __tests__/InlineFilesTransformer.test.ts index e0449c0e72..b9eb1c5410 100644 --- a/__tests__/InlineHtmlStripStylesTransformer.test.ts +++ b/__tests__/InlineFilesTransformer.test.ts @@ -1,11 +1,11 @@ /* * Code is inspired by * https://github.com/kulshekhar/ts-jest/blob/25e1c63dd3797793b0f46fa52fdee580b46f66ae/src/transformers/hoist-jest.spec.ts - * + * */ -import * as tsc from 'typescript' -import * as transformer from '../InlineHtmlStripStylesTransformer' +import * as tsc from 'typescript'; +import * as transformer from '../InlineFilesTransformer'; const CODE_WITH_TEMPLATE_URL = ` import { Component } from '@angular/core'; @@ -15,7 +15,7 @@ const CODE_WITH_TEMPLATE_URL = ` }) export class AngularComponent { } -` +`; const CODE_WITH_NON_RELATIVE_TEMPLATE_URL = ` import { Component } from '@angular/core'; @@ -25,7 +25,7 @@ const CODE_WITH_NON_RELATIVE_TEMPLATE_URL = ` }) export class AngularComponent { } -` +`; const CODE_WITH_STYLE_URLS = ` import { Component } from '@angular/core'; @@ -38,7 +38,7 @@ const CODE_WITH_STYLE_URLS = ` }) export class AngularComponent { } -` +`; const CODE_WITH_STYLES = ` import { Component } from '@angular/core'; @@ -51,7 +51,7 @@ const CODE_WITH_STYLES = ` }) export class AngularComponent { } -` +`; const CODE_WITH_ALL_DECORATOR_PROPERTIES = ` import { Component } from '@angular/core'; @@ -76,7 +76,7 @@ const CODE_WITH_ALL_DECORATOR_PROPERTIES = ` }) export class AngularComponent { } -` +`; const CODE_WITH_CUSTOM_DECORATOR = ` import { Component as CustomDecoratorName } from '@angular/core'; @@ -86,7 +86,7 @@ const CODE_WITH_CUSTOM_DECORATOR = ` }) export class AngularComponent { } -` +`; const CODE_TEST_WITH_TEMPLATE_URL_OVERRIDE = ` import { async, ComponentFixture, TestBed } from '@angular/core/testing'; @@ -117,62 +117,80 @@ describe('AComponent', () => { expect(fixture).toMatchSnapshot(); }); }); -` - - +`; + +const CODE_WITH_ASSIGNMENTS_OUTSIDE_DECORATOR = ` + const assignmentsToNotBeTransformed = { + styles: [{ + color: 'red' + }] + }; + const assignmentsToBeTransformed = { + styleUrls: ['./some-styles.css'], + templateUrl: './some-styles.css' + }; +`; const createFactory = () => { - return transformer.factory({ compilerModule: tsc } as any) -} -const transpile = (source: string) => tsc.transpileModule(source, { transformers: { before: [createFactory()] } }) + return transformer.factory({ compilerModule: tsc } as any); +}; +const transpile = (source: string) => + tsc.transpileModule(source, { transformers: { before: [createFactory()] } }); + describe('inlining template and stripping styles', () => { it('should have correct signature', () => { - expect(transformer.name).toBe('angular-component-inline-template-strip-styles') - expect(typeof transformer.version).toBe('number') - expect(transformer.version).toBeGreaterThan(0) - expect(typeof transformer.factory).toBe('function') - }) + expect(transformer.name).toBe('angular-component-inline-files'); + expect(typeof transformer.version).toBe('number'); + expect(transformer.version).toBeGreaterThan(0); + expect(typeof transformer.factory).toBe('function'); + }); it('should strip styleUrl assignment', () => { - const out = transpile(CODE_WITH_STYLE_URLS) + const out = transpile(CODE_WITH_STYLE_URLS); - expect(out.outputText).toMatchSnapshot() - }) + expect(out.outputText).toMatchSnapshot(); + }); it('should strip styles assignment', () => { - const out = transpile(CODE_WITH_STYLES) + const out = transpile(CODE_WITH_STYLES); - expect(out.outputText).toMatchSnapshot() - }) + expect(out.outputText).toMatchSnapshot(); + }); it('should inline templateUrl assignment', () => { - const out = transpile(CODE_WITH_TEMPLATE_URL) + const out = transpile(CODE_WITH_TEMPLATE_URL); - expect(out.outputText).toMatchSnapshot() - }) + expect(out.outputText).toMatchSnapshot(); + }); it('should inline non-relative templateUrl assignment and make it relative', () => { - const out = transpile(CODE_WITH_NON_RELATIVE_TEMPLATE_URL) + const out = transpile(CODE_WITH_NON_RELATIVE_TEMPLATE_URL); - expect(out.outputText).toMatchSnapshot() - }) + expect(out.outputText).toMatchSnapshot(); + }); it('should handle all transformable decorator assignments', () => { - const out = transpile(CODE_WITH_ALL_DECORATOR_PROPERTIES) + const out = transpile(CODE_WITH_ALL_DECORATOR_PROPERTIES); - expect(out.outputText).toMatchSnapshot() - }) + expect(out.outputText).toMatchSnapshot(); + }); it('should handle all decorator assignments in differently named decorators', () => { - const out = transpile(CODE_WITH_CUSTOM_DECORATOR) + const out = transpile(CODE_WITH_CUSTOM_DECORATOR); - expect(out.outputText).toMatchSnapshot() - }) + expect(out.outputText).toMatchSnapshot(); + }); it('should handle templateUrl in test file outside decorator', () => { - const out = transpile(CODE_TEST_WITH_TEMPLATE_URL_OVERRIDE) + const out = transpile(CODE_TEST_WITH_TEMPLATE_URL_OVERRIDE); - expect(out.outputText).toMatchSnapshot() - }) -}) + expect(out.outputText).toMatchSnapshot(); + }); + + it('should not transform styles outside decorator', () => { + const out = transpile(CODE_WITH_ASSIGNMENTS_OUTSIDE_DECORATOR); + + expect(out.outputText).toMatchSnapshot(); + }); +}); diff --git a/__tests__/StripStylesTransformer.test.ts b/__tests__/StripStylesTransformer.test.ts new file mode 100644 index 0000000000..bca3351500 --- /dev/null +++ b/__tests__/StripStylesTransformer.test.ts @@ -0,0 +1,68 @@ +/* + * Code is inspired by + * https://github.com/kulshekhar/ts-jest/blob/25e1c63dd3797793b0f46fa52fdee580b46f66ae/src/transformers/hoist-jest.spec.ts + * + */ + +import * as tsc from 'typescript'; +import * as transformer from '../StripStylesTransformer'; + +const CODE_WITH_STYLES_AND_OTHER_ASSIGNMENTS = ` + import { Component } from '@angular/core'; + + @SomeDecorator({ + value: 'test', + styles: [ + ':host { background-color: red }' + ], + }) + @Component({ + templateUrl: './page.html', + styleUrls: [ + './fancy-styles.css', + './basic-styles.scss' + ], + styles: [ + 'body { display: none }', + 'html { background-color: red }' + ], + unaffectedProperty: 'whatever' + }) + export class AngularComponent { + } +`; + +const CODE_WITH_ASSIGNMENT_OUTSIDE_DECORATOR = ` + const assignmentsToNotBeTransformed = { + styles: [{ + color: 'red' + }] + }; +`; + +const createFactory = () => { + return transformer.factory({ compilerModule: tsc } as any); +}; +const transpile = (source: string) => + tsc.transpileModule(source, { transformers: { before: [createFactory()] } }); + +describe('inlining template and stripping styles', () => { + it('should have correct signature', () => { + expect(transformer.name).toBe('angular-component-strip-styles'); + expect(typeof transformer.version).toBe('number'); + expect(transformer.version).toBeGreaterThan(0); + expect(typeof transformer.factory).toBe('function'); + }); + + it('should strip styleUrl assignment', () => { + const out = transpile(CODE_WITH_STYLES_AND_OTHER_ASSIGNMENTS); + + expect(out.outputText).toMatchSnapshot(); + }); + + it('should not transform styles outside decorator', () => { + const out = transpile(CODE_WITH_ASSIGNMENT_OUTSIDE_DECORATOR); + + expect(out.outputText).toMatchSnapshot(); + }); +}); diff --git a/__tests__/__snapshots__/InlineHtmlStripStylesTransformer.test.ts.snap b/__tests__/__snapshots__/InlineFilesTransformer.test.ts.snap similarity index 93% rename from __tests__/__snapshots__/InlineHtmlStripStylesTransformer.test.ts.snap rename to __tests__/__snapshots__/InlineFilesTransformer.test.ts.snap index 2a83bb10f7..ba2579146f 100644 --- a/__tests__/__snapshots__/InlineHtmlStripStylesTransformer.test.ts.snap +++ b/__tests__/__snapshots__/InlineFilesTransformer.test.ts.snap @@ -44,7 +44,10 @@ var AngularComponent = /** @class */ (function () { core_1.Component({ template: require('./page.html'), styleUrls: [], - styles: [], + styles: [ + 'body: { display: none }', + 'html: { background-color: red }' + ], unaffectedProperty: 'whatever' }), SomeOtherDecorator({ @@ -133,6 +136,19 @@ exports.AngularComponent = AngularComponent; " `; +exports[`inlining template and stripping styles should not transform styles outside decorator 1`] = ` +"var assignmentsToNotBeTransformed = { + styles: [{ + color: 'red' + }] +}; +var assignmentsToBeTransformed = { + styleUrls: [], + template: require('./some-styles.css') +}; +" +`; + exports[`inlining template and stripping styles should strip styleUrl assignment 1`] = ` "\\"use strict\\"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { @@ -172,7 +188,10 @@ var AngularComponent = /** @class */ (function () { } AngularComponent = __decorate([ core_1.Component({ - styles: [] + styles: [ + 'body: { display: none }', + 'html: { background-color: red }' + ] }) ], AngularComponent); return AngularComponent; diff --git a/__tests__/__snapshots__/StripStylesTransformer.test.ts.snap b/__tests__/__snapshots__/StripStylesTransformer.test.ts.snap new file mode 100644 index 0000000000..9d05e4f1fc --- /dev/null +++ b/__tests__/__snapshots__/StripStylesTransformer.test.ts.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`inlining template and stripping styles should not transform styles outside decorator 1`] = ` +"var assignmentsToNotBeTransformed = { + styles: [{ + color: 'red' + }] +}; +" +`; + +exports[`inlining template and stripping styles should strip styleUrl assignment 1`] = ` +"\\"use strict\\"; +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === \\"object\\" && typeof Reflect.decorate === \\"function\\") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +Object.defineProperty(exports, \\"__esModule\\", { value: true }); +var core_1 = require(\\"@angular/core\\"); +var AngularComponent = /** @class */ (function () { + function AngularComponent() { + } + AngularComponent = __decorate([ + SomeDecorator({ + value: 'test', + styles: [ + ':host { background-color: red }' + ], + }), + core_1.Component({ + templateUrl: './page.html', + styleUrls: [ + './fancy-styles.css', + './basic-styles.scss' + ], + styles: [], + unaffectedProperty: 'whatever' + }) + ], AngularComponent); + return AngularComponent; +}()); +exports.AngularComponent = AngularComponent; +" +`; diff --git a/jest-preset.js b/jest-preset.js index da04315d42..777e808e99 100644 --- a/jest-preset.js +++ b/jest-preset.js @@ -3,7 +3,10 @@ module.exports = { 'ts-jest': { tsConfig: '/src/tsconfig.spec.json', stringifyContentPathRegex: '\\.html$', - astTransformers: [require.resolve('./InlineHtmlStripStylesTransformer')], + astTransformers: [ + require.resolve('./InlineFilesTransformer'), + require.resolve('./StripStylesTransformer'), + ], }, }, transform: { diff --git a/src/InlineHtmlStripStylesTransformer.ts b/src/InlineFilesTransformer.ts similarity index 54% rename from src/InlineHtmlStripStylesTransformer.ts rename to src/InlineFilesTransformer.ts index 08ea6f2adf..81aadc75c7 100644 --- a/src/InlineHtmlStripStylesTransformer.ts +++ b/src/InlineFilesTransformer.ts @@ -1,93 +1,94 @@ /* * Code is inspired by * https://github.com/kulshekhar/ts-jest/blob/25e1c63dd3797793b0f46fa52fdee580b46f66ae/src/transformers/hoist-jest.ts - * + * */ -/* +/* * IMPLEMENTATION DETAILS: - * This transformer handles two concerns: removing styles and inlining referenced templates. + * This transformer handles: + * - inlining referenced template files and + * - removing referenced style files. * - * The assignments can be located anywhere in a file. + * The assignments 'templateUrl', 'styleUrls' can be located anywhere in a file. * Caveats: - * All properties 'templateUrl', 'styles', 'styleUrls' ANYWHERE will be modified, even if they + * All properties 'templateUrl', 'styleUrls' ANYWHERE will be modified, even if they * are not used in the context of an Angular Component. - * + * * The AST has to simply look like this anywhere in a ts file: - * + * * PropertyAssignment * Identifier * Initializer */ - // only import types, for the rest use injected `ConfigSet.compilerModule` -import TS, { +import { Node, SourceFile, TransformationContext, Transformer, Visitor, PropertyAssignment, - Identifier, - StringLiteral, -} from 'typescript' + Identifier +} from 'typescript'; +import { getCreateStringLiteral, ConfigSet } from './TransformUtils'; // replace original ts-jest ConfigSet with this simple interface, as it would require // jest-preset-angular to add several babel devDependencies to get the other types // inside the ConfigSet right -interface ConfigSet { - compilerModule: typeof TS -} /** Angular component decorator TemplateUrl property name */ -const TEMPLATE_URL = 'templateUrl' +const TEMPLATE_URL = 'templateUrl'; /** Angular component decorator StyleUrls property name */ -const STYLE_URLS = 'styleUrls' -/** Angular component decorator Styles property name */ -const STYLES = 'styles' +const STYLE_URLS = 'styleUrls'; /** Angular component decorator Template property name */ -const TEMPLATE = 'template' +const TEMPLATE = 'template'; /** Node require function name */ -const REQUIRE = 'require' +const REQUIRE = 'require'; /** - * Property names inside the decorator argument to transform + * Property names anywhere in an angular project to transform */ -const TRANSFORM_PROPS = [TEMPLATE_URL, STYLES, STYLE_URLS] +const TRANSFORM_PROPS = [TEMPLATE_URL, STYLE_URLS]; /** * Transformer ID * @internal */ -export const name = 'angular-component-inline-template-strip-styles' +export const name = 'angular-component-inline-files'; // increment this each time the code is modified /** * Transformer Version * @internal */ -export const version = 1 +export const version = 1; /** * The factory of hoisting transformer factory * @internal */ export function factory(cs: ConfigSet) { - /** * Our compiler (typescript, or a module with typescript-like interface) */ - const ts = cs.compilerModule + const ts = cs.compilerModule; + + const createStringLiteral = getCreateStringLiteral(ts); /** - * Traverses the AST down to the relevant assignments in the decorator - * argument and returns them in an array. + * Traverses the AST down to the relevant assignments anywhere in the file + * and returns a boolean indicating if it should be transformed. */ - function isPropertyAssignmentToTransform(node: Node): node is PropertyAssignment { - return ts.isPropertyAssignment(node) && + function isPropertyAssignmentToTransform( + node: Node + ): node is PropertyAssignment { + return ( + ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && TRANSFORM_PROPS.includes(node.name.text) + ); } /** @@ -95,44 +96,47 @@ export function factory(cs: ConfigSet) { * @param node the property assignment to change */ function transfromPropertyAssignmentForJest(node: PropertyAssignment) { + const mutableAssignment = ts.getMutableClone(node); - const mutableAssignment = ts.getMutableClone(node) - - const assignmentNameText = (mutableAssignment.name as Identifier).text - + const assignmentNameText = (mutableAssignment.name as Identifier).text; switch (assignmentNameText) { case TEMPLATE_URL: - // reuse the right-hand-side literal from the assignment - let templatePathLiteral = mutableAssignment.initializer + // replace 'templateUrl' with 'template' + + // reuse the right-hand-side literal (the filepath) from the assignment + let pathLiteral = mutableAssignment.initializer; // fix templatePathLiteral if it was a non-relative path - if (ts.isStringLiteral(mutableAssignment.initializer)) { - const templatePathStringLiteral: StringLiteral = mutableAssignment.initializer; - // match if it starts with ./ or ../ or / - if (templatePathStringLiteral.text && - !templatePathStringLiteral.text.match(/^(\.\/|\.\.\/|\/)/)) { - // make path relative by appending './' - templatePathLiteral = ts.createStringLiteral(`./${templatePathStringLiteral.text}`) + if (ts.isStringLiteral(pathLiteral)) { + // match if it does not start with ./ or ../ or / + if ( + pathLiteral.text && + !pathLiteral.text.match(/^(\.\/|\.\.\/|\/)/) + ) { + // make path relative by prepending './' + pathLiteral = createStringLiteral(`./${pathLiteral.text}`); } } - // replace 'templateUrl' with 'template' - mutableAssignment.name = ts.createIdentifier(TEMPLATE) // replace current initializer with require(path) - mutableAssignment.initializer = ts.createCall( + const requireCall = ts.createCall( /* expression */ ts.createIdentifier(REQUIRE), /* type arguments */ undefined, - /* arguments array */ [templatePathLiteral] - ) + /* arguments array */ [pathLiteral] + ); + + mutableAssignment.name = ts.createIdentifier(TEMPLATE); + mutableAssignment.initializer = requireCall; break; - case STYLES: + case STYLE_URLS: - // replace initializer array with empty array - mutableAssignment.initializer = ts.createArrayLiteral() + // replace styleUrls value with emtpy array + // inlining all urls would be way more complicated and slower + mutableAssignment.initializer = ts.createArrayLiteral(); break; } - return mutableAssignment + return mutableAssignment; } /** @@ -141,32 +145,30 @@ export function factory(cs: ConfigSet) { * @param _ The owning source file */ function createVisitor(ctx: TransformationContext, _: SourceFile) { - /** * Our main visitor, which will be called recursively for each node in the source file's AST * @param node The node to be visited */ const visitor: Visitor = node => { - - let resultNode: Node + let resultNode = node; // before we create a deep clone to modify, we make sure that // this is an assignment which we want to transform if (isPropertyAssignmentToTransform(node)) { - // get transformed node with changed properties - resultNode = transfromPropertyAssignmentForJest(node) - } else { - // look for interesting assignments inside this node - resultNode = ts.visitEachChild(node, visitor, ctx) + resultNode = transfromPropertyAssignmentForJest(node); } + // look for interesting assignments inside this node in any case + resultNode = ts.visitEachChild(resultNode, visitor, ctx); + // finally return the currently visited node - return resultNode - } - return visitor + return resultNode; + }; + return visitor; } - return (ctx: TransformationContext): Transformer => - (sf: SourceFile) => ts.visitNode(sf, createVisitor(ctx, sf)) + return (ctx: TransformationContext): Transformer => ( + sf: SourceFile + ) => ts.visitNode(sf, createVisitor(ctx, sf)); } diff --git a/src/StripStylesTransformer.ts b/src/StripStylesTransformer.ts new file mode 100644 index 0000000000..0baf1794a2 --- /dev/null +++ b/src/StripStylesTransformer.ts @@ -0,0 +1,149 @@ +/* + * Code is inspired by + * https://github.com/kulshekhar/ts-jest/blob/25e1c63dd3797793b0f46fa52fdee580b46f66ae/src/transformers/hoist-jest.ts + * + * + * IMPLEMENTATION DETAILS: + * This transformer handles one concern: removing styles. + * + * The property 'styles' inside a @Component(...) Decorator argument will + * be modified, even if they are not used in the context of an + * Angular Component. + * + * This is the required AST to trigger the transformation: + * + * ClassDeclaration + * Decorator + * CallExpression + * ObjectLiteralExpression + * PropertyAssignment + * Identifier + * StringLiteral + */ + +// only import types, for the rest use injected `ConfigSet.compilerModule` +import { + Node, + SourceFile, + TransformationContext, + Transformer, + Visitor, + Identifier, + ClassDeclaration +} from 'typescript'; +import { ConfigSet } from './TransformUtils'; + +/** Angular component decorator Styles property name */ +const STYLES = 'styles'; +/** Angular component decorator name */ +const COMPONENT = 'Component'; +/** All props to be transformed inside a decorator */ +const TRANSFORM_IN_DECORATOR_PROPS = [STYLES]; + +/** + * Transformer ID + * @internal + */ +export const name = 'angular-component-strip-styles'; + +// increment this each time the code is modified +/** + * Transformer Version + * @internal + */ +export const version = 1; + +/** + * The factory of hoisting transformer factory + * @internal + */ +export function factory(cs: ConfigSet) { + /** + * Our compiler (typescript, or a module with typescript-like interface) + */ + const ts = cs.compilerModule; + + /** + * Traverses the AST down inside a decorator to a styles assignment + * and returns a boolean indicating if it should be transformed. + */ + function isInDecoratorPropertyAssignmentToTransform(node: Node): node is ClassDeclaration { + return getInDecoratorPropertyAssignmentsToTransform(node).length > 0; + } + + /** + * Traverses the AST down inside a decorator to a styles assignment + * returns it in an array. + */ + function getInDecoratorPropertyAssignmentsToTransform(node: Node) { + if (!ts.isClassDeclaration(node) || !node.decorators) { + return []; + } + + return node.decorators + .map(dec => dec.expression) + .filter(ts.isCallExpression) + .filter(callExpr => ts.isIdentifier(callExpr.expression) && callExpr.expression.getText() === COMPONENT) + .flatMap(callExpr => + callExpr.arguments + .filter(ts.isObjectLiteralExpression) + .flatMap(arg => + arg.properties + .filter(ts.isPropertyAssignment) + .filter(propAss => ts.isIdentifier(propAss.name)) + .filter(propAss => + TRANSFORM_IN_DECORATOR_PROPS.includes( + (propAss.name as Identifier).text + ) + ) + ) + ) + } + + /** + * Clones the styles assignment and manipulates it. + * @param node the property assignment to change + */ + function transfromStylesAssignmentForJest(node: ClassDeclaration) { + const mutableNode = ts.getMutableClone(node) + const assignments = getInDecoratorPropertyAssignmentsToTransform(mutableNode) + + assignments.forEach(assignment => { + switch ((assignment.name as Identifier).text) { + case STYLES: + // replace initializer array with empty array + assignment.initializer = ts.createArrayLiteral() + break; + } + }) + return mutableNode + } + + /** + * Create a source file visitor which will visit all nodes in a source file + * @param ctx The typescript transformation context + * @param _ The owning source file + */ + function createVisitor(ctx: TransformationContext, _: SourceFile) { + /** + * Main visitor, which will be called recursively for each node in the source file's AST + * @param node The node to be visited + */ + const visitor: Visitor = node => { + // before we create a deep clone to modify, we make sure that + // this is an assignment which we want to transform + if (isInDecoratorPropertyAssignmentToTransform(node)) { + // get transformed node with changed properties + return transfromStylesAssignmentForJest(node); + } else { + // else look for assignments inside this node recursively + return ts.visitEachChild(node, visitor, ctx); + } + }; + return visitor; + } + + return (ctx: TransformationContext): Transformer => ( + sf: SourceFile + ) => ts.visitNode(sf, createVisitor(ctx, sf)); +} diff --git a/src/TransformUtils.ts b/src/TransformUtils.ts new file mode 100644 index 0000000000..d2d8ee86d1 --- /dev/null +++ b/src/TransformUtils.ts @@ -0,0 +1,26 @@ +import TS from 'typescript'; + +// replace original ts-jest ConfigSet with this simple interface, as it would require +// jest-preset-angular to add several babel devDependencies to get the other types +// inside the ConfigSet right +export interface ConfigSet { + compilerModule: typeof TS; +} + +/** + * returns the compiler function to create a string literal. If an old version + * of the TypeScript module is used, it will create a function that replaces the + * behavior of the `createStringLiteral` function. + * @param ts TypeScript compiler module + */ +export function getCreateStringLiteral(ts: typeof TS): typeof TS.createStringLiteral { + if (ts.createStringLiteral && typeof ts.createStringLiteral === 'function') { + return ts.createStringLiteral; + } + return function createStringLiteral(text: string) { + const node = ts.createNode(ts.SyntaxKind.StringLiteral, -1, -1); + node.text = text; + node.flags |= ts.NodeFlags.Synthesized; + return node; + } +}