From 76c25d2d8d1de0fa458b46b473c200a2d6bd542a Mon Sep 17 00:00:00 2001 From: Ahn Date: Thu, 7 Jan 2021 16:01:09 +0100 Subject: [PATCH] feat(compiler): use `replace-resources` for `isolatedModules: true` (#717) Copy source code of TypeScript `transpileModule` so that we can get `Program` which is created in that function which then allows us to use `replace-resources` transformer from Angular. This also provides fully compatibility to ESM for `isolatedModules: true`, related to #710 BREAKING CHANGE - `inline-files` and `strip-styles` are removed from `jest-preset-angular` and now `jest-preset-angular` always uses Angular `replace-resources` instead for both `isolatedModules: false` and `isolatedModules: true`. - The transformers in your jest config ``` 'jest-preset-angular/build/InlineFilesTransformer' 'jest-preset-angular/build/StripStylesTransformer' ``` or ``` 'jest-preset-angular/build/transformers/inline-files' 'jest-preset-angular/build/transformers/strip-styles' ``` must be removed --- .../__snapshots__/hoisting.spec.ts.snap | 12 +- .../__snapshots__/inline-files.spec.ts.snap | 207 ------------------ .../replace-resources.spec.ts.snap | 51 ++++- .../__snapshots__/strip-styles.spec.ts.snap | 47 ---- src/__tests__/hoisting.spec.ts | 12 +- src/__tests__/inline-files.spec.ts | 189 ---------------- src/__tests__/ng-jest-compiler.spec.ts | 45 ++-- src/__tests__/replace-resources.spec.ts | 59 ++--- src/__tests__/strip-styles.spec.ts | 61 ------ src/compiler/ng-jest-compiler.ts | 156 +++++++++++-- src/transformers/inline-files.ts | 175 --------------- src/transformers/strip-styles.ts | 160 -------------- 12 files changed, 239 insertions(+), 935 deletions(-) delete mode 100644 src/__tests__/__snapshots__/inline-files.spec.ts.snap delete mode 100644 src/__tests__/__snapshots__/strip-styles.spec.ts.snap delete mode 100644 src/__tests__/inline-files.spec.ts delete mode 100644 src/__tests__/strip-styles.spec.ts delete mode 100644 src/transformers/inline-files.ts delete mode 100644 src/transformers/strip-styles.ts diff --git a/src/__tests__/__snapshots__/hoisting.spec.ts.snap b/src/__tests__/__snapshots__/hoisting.spec.ts.snap index 1e90c22f7e..7adcdee381 100644 --- a/src/__tests__/__snapshots__/hoisting.spec.ts.snap +++ b/src/__tests__/__snapshots__/hoisting.spec.ts.snap @@ -1,6 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Hoisting should hoist correctly 1`] = ` +exports[`Hoisting should hoist correctly with isolatedModules false 1`] = ` +"\\"use strict\\"; +Object.defineProperty(exports, \\"__esModule\\", { value: true }); +const globals_1 = require(\\"@jest/globals\\"); +globals_1.jest.mock('./foo'); +const foo_1 = require(\\"./foo\\"); +console.log(foo_1.getFoo()); +//# " +`; + +exports[`Hoisting should hoist correctly with isolatedModules true 1`] = ` "\\"use strict\\"; Object.defineProperty(exports, \\"__esModule\\", { value: true }); const globals_1 = require(\\"@jest/globals\\"); diff --git a/src/__tests__/__snapshots__/inline-files.spec.ts.snap b/src/__tests__/__snapshots__/inline-files.spec.ts.snap deleted file mode 100644 index 24d26b5e65..0000000000 --- a/src/__tests__/__snapshots__/inline-files.spec.ts.snap +++ /dev/null @@ -1,207 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`inlining template and stripping styleUrls should handle all decorator assignments in differently named decorators 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 }); -exports.AngularComponent = void 0; -var core_1 = require(\\"@angular/core\\"); -var AngularComponent = /** @class */ (function () { - function AngularComponent() { - } - AngularComponent = __decorate([ - core_1.Component({ - template: require('./page.html') - }) - ], AngularComponent); - return AngularComponent; -}()); -exports.AngularComponent = AngularComponent; -" -`; - -exports[`inlining template and stripping styleUrls should handle all transformable decorator assignments 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 }); -exports.AngularComponent = void 0; -var core_1 = require(\\"@angular/core\\"); -var AngularComponent = /** @class */ (function () { - function AngularComponent() { - } - AngularComponent = __decorate([ - SomeDecorator({ - value: 'test' - }), - core_1.Component({ - template: require('./page.html'), - styleUrls: [], - styles: [ - 'body { display: none }', - 'html { background-color: red }' - ], - unaffectedProperty: 'whatever' - }), - SomeOtherDecorator({ - prop: 'ok' - }) - ], AngularComponent); - return AngularComponent; -}()); -exports.AngularComponent = AngularComponent; -" -`; - -exports[`inlining template and stripping styleUrls should handle templateUrl in test file outside decorator 1`] = ` -"\\"use strict\\"; -Object.defineProperty(exports, \\"__esModule\\", { value: true }); -var testing_1 = require(\\"@angular/core/testing\\"); -var a_component_1 = require(\\"./a.component\\"); -describe('AComponent', function () { - var fixture, instance; - beforeEach(testing_1.async(function () { - testing_1.TestBed.configureTestingModule({ - declarations: [ - a_component_1.AComponent, - ], - }).overrideComponent(a_component_1.AComponent, { - set: { - template: require('../__mocks__/alert-follow-stub.component.html'), - }, - }); - fixture = testing_1.TestBed.createComponent(a_component_1.AComponent); - instance = fixture.componentInstance; - fixture.detectChanges(); - })); - it('should render the component', function () { - expect(fixture).toMatchSnapshot(); - }); -}); -" -`; - -exports[`inlining template and stripping styleUrls should inline non-relative templateUrl assignment and make it relative 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 }); -exports.AngularComponent = void 0; -var core_1 = require(\\"@angular/core\\"); -var AngularComponent = /** @class */ (function () { - function AngularComponent() { - } - AngularComponent = __decorate([ - core_1.Component({ - template: require(\\"./page.html\\") - }) - ], AngularComponent); - return AngularComponent; -}()); -exports.AngularComponent = AngularComponent; -" -`; - -exports[`inlining template and stripping styleUrls should inline templateUrl 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 }); -exports.AngularComponent = void 0; -var core_1 = require(\\"@angular/core\\"); -var AngularComponent = /** @class */ (function () { - function AngularComponent() { - } - AngularComponent = __decorate([ - core_1.Component({ - template: require('./page.html') - }) - ], AngularComponent); - return AngularComponent; -}()); -exports.AngularComponent = AngularComponent; -" -`; - -exports[`inlining template and stripping styleUrls should not strip styles 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 }); -exports.AngularComponent = void 0; -var core_1 = require(\\"@angular/core\\"); -var AngularComponent = /** @class */ (function () { - function AngularComponent() { - } - AngularComponent = __decorate([ - core_1.Component({ - styles: [ - 'body { display: none }', - 'html { background-color: red }' - ] - }) - ], AngularComponent); - return AngularComponent; -}()); -exports.AngularComponent = AngularComponent; -" -`; - -exports[`inlining template and stripping styleUrls 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 styleUrls should strip styleUrls 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 }); -exports.AngularComponent = void 0; -var core_1 = require(\\"@angular/core\\"); -var AngularComponent = /** @class */ (function () { - function AngularComponent() { - } - AngularComponent = __decorate([ - core_1.Component({ - styleUrls: [] - }) - ], AngularComponent); - return AngularComponent; -}()); -exports.AngularComponent = AngularComponent; -" -`; diff --git a/src/__tests__/__snapshots__/replace-resources.spec.ts.snap b/src/__tests__/__snapshots__/replace-resources.spec.ts.snap index 29610bde70..240ab2ce5a 100644 --- a/src/__tests__/__snapshots__/replace-resources.spec.ts.snap +++ b/src/__tests__/__snapshots__/replace-resources.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Replace resources transformer should use inline-files + strip-styles for isolatedModules true 1`] = ` +exports[`Replace resources transformer with isolatedModules false should use replaceResources transformer from @angular/compiler-cli with useESM false 1`] = ` "\\"use strict\\"; Object.defineProperty(exports, \\"__esModule\\", { value: true }); exports.AppComponent = void 0; @@ -14,16 +14,15 @@ let AppComponent = class AppComponent { AppComponent = tslib_1.__decorate([ core_1.Component({ selector: 'app-root', - template: require('./app.component.html'), - styleUrls: [], - styles: [], + template: require(\\"./app.component.html\\"), + styles: [] }) ], AppComponent); exports.AppComponent = AppComponent; //# " `; -exports[`Replace resources transformer should use replaceResources transformer from @angular/compiler-cli for isolatedModules false 1`] = ` +exports[`Replace resources transformer with isolatedModules false should use replaceResources transformer from @angular/compiler-cli with useESM true 1`] = ` "\\"use strict\\"; Object.defineProperty(exports, \\"__esModule\\", { value: true }); exports.AppComponent = void 0; @@ -44,3 +43,45 @@ AppComponent = tslib_1.__decorate([ exports.AppComponent = AppComponent; //# " `; + +exports[`Replace resources transformer with isolatedModules true should use replaceResources transformer from @angular/compiler-cli with useESM false 1`] = ` +"\\"use strict\\"; +Object.defineProperty(exports, \\"__esModule\\", { value: true }); +exports.AppComponent = void 0; +const tslib_1 = require(\\"tslib\\"); +const core_1 = require(\\"@angular/core\\"); +let AppComponent = class AppComponent { + constructor() { + this.title = 'test-app-v10'; + } +}; +AppComponent = tslib_1.__decorate([ + core_1.Component({ + selector: 'app-root', + template: require(\\"./app.component.html\\"), + styles: [] + }) +], AppComponent); +exports.AppComponent = AppComponent; +//# " +`; + +exports[`Replace resources transformer with isolatedModules true should use replaceResources transformer from @angular/compiler-cli with useESM true 1`] = ` +"import { __decorate } from \\"tslib\\"; +import __NG_CLI_RESOURCE__0 from \\"./app.component.html\\"; +import { Component } from '@angular/core'; +let AppComponent = class AppComponent { + constructor() { + this.title = 'test-app-v10'; + } +}; +AppComponent = __decorate([ + Component({ + selector: 'app-root', + template: __NG_CLI_RESOURCE__0, + styles: [] + }) +], AppComponent); +export { AppComponent }; +//# " +`; diff --git a/src/__tests__/__snapshots__/strip-styles.spec.ts.snap b/src/__tests__/__snapshots__/strip-styles.spec.ts.snap deleted file mode 100644 index 9bedac7f08..0000000000 --- a/src/__tests__/__snapshots__/strip-styles.spec.ts.snap +++ /dev/null @@ -1,47 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`inlining template and stripping styles should not strip styleUrls 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 }); -exports.AngularComponent = void 0; -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; -" -`; - -exports[`inlining template and stripping styles should not transform styles outside decorator 1`] = ` -"var assignmentsToNotBeTransformed = { - styles: [{ - color: 'red' - }] -}; -" -`; diff --git a/src/__tests__/hoisting.spec.ts b/src/__tests__/hoisting.spec.ts index 5305179d91..53034432b0 100644 --- a/src/__tests__/hoisting.spec.ts +++ b/src/__tests__/hoisting.spec.ts @@ -11,8 +11,16 @@ import { mockFolder } from './__helpers__/test-helpers'; describe('Hoisting', () => { // Verify if we use `ts-jest` hoisting transformer - test('should hoist correctly', () => { - const ngJestConfig = new NgJestConfig(jestCfgStub); + test.each([true, false])('should hoist correctly with isolatedModules %p', (isolatedModules) => { + const ngJestConfig = new NgJestConfig({ + ...jestCfgStub, + globals: { + 'ts-jest': { + ...jestCfgStub.globals['ts-jest'], + isolatedModules, + }, + }, + }); const fileName = join(mockFolder, 'foo.spec.ts'); const compiler = new NgJestCompiler(ngJestConfig, new Map()); diff --git a/src/__tests__/inline-files.spec.ts b/src/__tests__/inline-files.spec.ts deleted file mode 100644 index 25fc3302f2..0000000000 --- a/src/__tests__/inline-files.spec.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Code is inspired by - * https://github.com/kulshekhar/ts-jest/blob/25e1c63dd3797793b0f46fa52fdee580b46f66ae/src/transformers/hoist-jest.spec.ts - * - */ - -import ts from 'typescript'; - -import * as transformer from '../transformers/inline-files'; - -const CODE_WITH_TEMPLATE_URL = ` - import { Component } from '@angular/core'; - - @Component({ - templateUrl: './page.html' - }) - export class AngularComponent { - } -`; - -const CODE_WITH_NON_RELATIVE_TEMPLATE_URL = ` - import { Component } from '@angular/core'; - - @Component({ - templateUrl: 'page.html' - }) - export class AngularComponent { - } -`; - -const CODE_WITH_STYLE_URLS = ` - import { Component } from '@angular/core'; - - @Component({ - styleUrls: [ - './fancy-styles.css', - './basic-styles.scss' - ] - }) - export class AngularComponent { - } -`; - -const CODE_WITH_STYLES = ` - import { Component } from '@angular/core'; - - @Component({ - styles: [ - 'body { display: none }', - 'html { background-color: red }' - ] - }) - export class AngularComponent { - } -`; - -const CODE_WITH_ALL_DECORATOR_PROPERTIES = ` - import { Component } from '@angular/core'; - - @SomeDecorator({ - value: 'test' - }) - @Component({ - templateUrl: './page.html', - styleUrls: [ - './fancy-styles.css', - './basic-styles.scss' - ], - styles: [ - 'body { display: none }', - 'html { background-color: red }' - ], - unaffectedProperty: 'whatever' - }) - @SomeOtherDecorator({ - prop: 'ok' - }) - export class AngularComponent { - } -`; - -const CODE_WITH_CUSTOM_DECORATOR = ` - import { Component as CustomDecoratorName } from '@angular/core'; - - @CustomDecoratorName({ - templateUrl: './page.html' - }) - export class AngularComponent { - } -`; - -const CODE_TEST_WITH_TEMPLATE_URL_OVERRIDE = ` -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { AComponent } from './a.component'; - -describe('AComponent', () => { - let fixture: ComponentFixture, - instance: AComponent; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ - AComponent, - ], - }).overrideComponent(AComponent, { - set: { - templateUrl: '../__mocks__/alert-follow-stub.component.html', - }, - }); - - fixture = TestBed.createComponent(AComponent); - instance = fixture.componentInstance; - fixture.detectChanges(); - })); - - it('should render the component', () => { - 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 = () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return transformer.factory({ compilerModule: ts } as any); -}; -const transpile = (source: string) => ts.transpileModule(source, { transformers: { before: [createFactory()] } }); - -describe('inlining template and stripping styleUrls', () => { - it('should strip styleUrls assignment', () => { - const out = transpile(CODE_WITH_STYLE_URLS); - - expect(out.outputText).toMatchSnapshot(); - }); - - it('should inline templateUrl assignment', () => { - const out = transpile(CODE_WITH_TEMPLATE_URL); - - expect(out.outputText).toMatchSnapshot(); - }); - - it('should not strip styles assignment', () => { - const out = transpile(CODE_WITH_STYLES); - - expect(out.outputText).toMatchSnapshot(); - }); - - it('should inline non-relative templateUrl assignment and make it relative', () => { - const out = transpile(CODE_WITH_NON_RELATIVE_TEMPLATE_URL); - - expect(out.outputText).toMatchSnapshot(); - }); - - it('should handle all transformable decorator assignments', () => { - const out = transpile(CODE_WITH_ALL_DECORATOR_PROPERTIES); - - expect(out.outputText).toMatchSnapshot(); - }); - - it('should handle all decorator assignments in differently named decorators', () => { - const out = transpile(CODE_WITH_CUSTOM_DECORATOR); - - expect(out.outputText).toMatchSnapshot(); - }); - - it('should handle templateUrl in test file outside decorator', () => { - const out = transpile(CODE_TEST_WITH_TEMPLATE_URL_OVERRIDE); - - 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/src/__tests__/ng-jest-compiler.spec.ts b/src/__tests__/ng-jest-compiler.spec.ts index 533abe0b0a..e452f819e1 100644 --- a/src/__tests__/ng-jest-compiler.spec.ts +++ b/src/__tests__/ng-jest-compiler.spec.ts @@ -10,11 +10,8 @@ import { NgJestConfig } from '../config/ng-jest-config'; import { jestCfgStub } from './__helpers__/test-constants'; import { mockFolder } from './__helpers__/test-helpers'; -import SpyInstance = jest.SpyInstance; - describe('NgJestCompiler', () => { describe('with isolatedModules true', () => { - let transpileModuleSpy: SpyInstance; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const baseJestCfg = { ...jestCfgStub, @@ -27,46 +24,36 @@ describe('NgJestCompiler', () => { }, }; - beforeEach(() => { - // @ts-expect-error testing purpose - transpileModuleSpy = ts.transpileModule = jest.fn().mockReturnValueOnce({ - outputText: 'var foo = 1', - diagnostics: [], - sourceMapText: '{}', - }); - }); - - test('should call transpileModule with CommonJS module', () => { - const ngJestConfig = new NgJestConfig(baseJestCfg); - const fileName = join(mockFolder, 'foo.service.ts'); - const compiler = new NgJestCompiler(ngJestConfig, new Map()); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - compiler.getCompiledOutput(fileName, readFileSync(fileName, 'utf-8'), false)!; - - expect(transpileModuleSpy).toHaveBeenCalled(); - expect(transpileModuleSpy.mock.calls[0][1].compilerOptions.module).toEqual(ts.ModuleKind.CommonJS); - }); - - test('should call transpileModule with ESM module', () => { + test.each([true, false])('should call transpileModule with useESM %p', (useESM) => { const ngJestConfig = new NgJestConfig({ ...baseJestCfg, globals: { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 'ts-jest': { ...baseJestCfg.globals['ts-jest'], - useESM: true, + useESM, }, }, }); const fileName = join(mockFolder, 'foo.service.ts'); const compiler = new NgJestCompiler(ngJestConfig, new Map()); + // @ts-expect-error testing purpose + compiler._transpileModule = jest.fn().mockReturnValueOnce({ + outputText: 'var foo = 1', + diagnostics: [], + sourceMapText: '{}', + }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - compiler.getCompiledOutput(fileName, readFileSync(fileName, 'utf-8'), true)!; + compiler.getCompiledOutput(fileName, readFileSync(fileName, 'utf-8'), useESM)!; - expect(transpileModuleSpy).toHaveBeenCalled(); - expect(transpileModuleSpy.mock.calls[0][1].compilerOptions.module).not.toEqual(ts.ModuleKind.CommonJS); + // @ts-expect-error testing purpose + expect(compiler._transpileModule).toHaveBeenCalled(); + // @ts-expect-error testing purpose + const moduleKind = compiler._transpileModule.mock.calls[0][1].compilerOptions.module; + useESM + ? expect(moduleKind).not.toEqual(ts.ModuleKind.CommonJS) + : expect(moduleKind).toEqual(ts.ModuleKind.CommonJS); }); }); diff --git a/src/__tests__/replace-resources.spec.ts b/src/__tests__/replace-resources.spec.ts index cc1f25507d..1a67ad8612 100644 --- a/src/__tests__/replace-resources.spec.ts +++ b/src/__tests__/replace-resources.spec.ts @@ -13,43 +13,28 @@ describe('Replace resources transformer', () => { const fileName = join(mockFolder, 'app.component.ts'); const fileContent = readFileSync(fileName, 'utf-8'); - test('should use replaceResources transformer from @angular/compiler-cli for isolatedModules false', () => { - const ngJestConfig = new NgJestConfig({ - ...jestCfgStub, - globals: { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - 'ts-jest': { - ...jestCfgStub.globals['ts-jest'], - isolatedModules: false, - }, + describe.each([true, false])('with isolatedModules %p', (isolatedModules) => { + test.each([true, false])( + 'should use replaceResources transformer from @angular/compiler-cli with useESM %p', + (useESM) => { + const ngJestConfig = new NgJestConfig({ + ...jestCfgStub, + globals: { + 'ts-jest': { + ...jestCfgStub.globals['ts-jest'], + isolatedModules, + useESM, + }, + }, + }); + const compiler = new NgJestCompiler(ngJestConfig, new Map()); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const emittedResult = compiler.getCompiledOutput(fileName, fileContent, useESM)!; + + // Source map is different based on file location which can fail on CI, so we only compare snapshot for js + expect(emittedResult.substring(0, emittedResult.indexOf(SOURCE_MAPPING_PREFIX))).toMatchSnapshot(); }, - }); - const compiler = new NgJestCompiler(ngJestConfig, new Map()); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const emittedResult = compiler.getCompiledOutput(fileName, fileContent, false)!; - - // Source map is different based on file location which can fail on CI, so we only compare snapshot for js - expect(emittedResult.substring(0, emittedResult.indexOf(SOURCE_MAPPING_PREFIX))).toMatchSnapshot(); - }); - - test('should use inline-files + strip-styles for isolatedModules true', () => { - const ngJestConfig = new NgJestConfig({ - ...jestCfgStub, - globals: { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - 'ts-jest': { - ...jestCfgStub.globals['ts-jest'], - isolatedModules: true, - }, - }, - }); - const compiler = new NgJestCompiler(ngJestConfig, new Map()); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const emittedResult = compiler.getCompiledOutput(fileName, fileContent, false)!; - - // Source map is different based on file location which can fail on CI, so we only compare snapshot for js - expect(emittedResult.substring(0, emittedResult.indexOf(SOURCE_MAPPING_PREFIX))).toMatchSnapshot(); + ); }); }); diff --git a/src/__tests__/strip-styles.spec.ts b/src/__tests__/strip-styles.spec.ts deleted file mode 100644 index 87c4b5964a..0000000000 --- a/src/__tests__/strip-styles.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Code is inspired by - * https://github.com/kulshekhar/ts-jest/blob/25e1c63dd3797793b0f46fa52fdee580b46f66ae/src/transformers/hoist-jest.spec.ts - * - */ - -import tsc from 'typescript'; - -import * as transformer from '../transformers/strip-styles'; - -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); // eslint-disable-line @typescript-eslint/no-explicit-any -}; -const transpile = (source: string) => tsc.transpileModule(source, { transformers: { before: [createFactory()] } }); - -describe('inlining template and stripping styles', () => { - it('should not strip styleUrls 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/src/compiler/ng-jest-compiler.ts b/src/compiler/ng-jest-compiler.ts index 17b5001c93..30fed28371 100644 --- a/src/compiler/ng-jest-compiler.ts +++ b/src/compiler/ng-jest-compiler.ts @@ -7,20 +7,27 @@ import type * as ts from 'typescript'; import type { NgJestConfig } from '../config/ng-jest-config'; import { constructorParametersDownlevelTransform } from '../transformers/downlevel-ctor'; -import { factory as inlineFiles } from '../transformers/inline-files'; import { replaceResources } from '../transformers/replace-resources'; -import { factory as stripStyles } from '../transformers/strip-styles'; import { NgJestCompilerHost } from './compiler-host'; +interface PatchedTranspileOptions { + fileName: string; + compilerOptions: ts.CompilerOptions; + moduleName?: string; + renamedDependencies?: ts.MapLike; +} + export class NgJestCompiler implements CompilerInstance { private _compilerOptions!: CompilerOptions; - private _program: ts.Program | undefined; + private _program!: ts.Program; private _compilerHost: CompilerHost | undefined; private _tsHost: NgJestCompilerHost | undefined; private _rootNames: string[] = []; private readonly _logger: Logger; private readonly _ts: TTypeScript; + private readonly isAppPath = (fileName: string) => + !fileName.endsWith('.ngfactory.ts') && !fileName.endsWith('.ngstyle.ts'); constructor(readonly ngJestConfig: NgJestConfig, readonly jestCacheFS: Map) { this._logger = this.ngJestConfig.logger; @@ -39,7 +46,6 @@ export class NgJestCompiler implements CompilerInstance { getCompiledOutput(fileName: string, fileContent: string, supportsStaticESM: boolean): string { const customTransformers = this.ngJestConfig.customTransformers; - const isAppPath = (fileName: string) => !fileName.endsWith('.ngfactory.ts') && !fileName.endsWith('.ngstyle.ts'); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const getTypeChecker = () => this._program!.getTypeChecker(); if (this._program) { @@ -67,7 +73,7 @@ export class NgJestCompiler implements CompilerInstance { * _createCompilerHost */ constructorParametersDownlevelTransform(this._program), - replaceResources(isAppPath, getTypeChecker), + replaceResources(this.isAppPath, getTypeChecker), ], }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -105,24 +111,17 @@ export class NgJestCompiler implements CompilerInstance { this._logger.debug({ fileName }, 'getCompiledOutput: compiling as isolated module'); - const result: ts.TranspileOutput = this._ts.transpileModule(fileContent, { - fileName, - transformers: { - ...customTransformers, - before: [ - // hoisting from `ts-jest` or other before transformers - ...(customTransformers.before as Array>), - inlineFiles(this.ngJestConfig), - stripStyles(this.ngJestConfig), - ], + const result: ts.TranspileOutput = this._transpileModule( + fileContent, + { + fileName, + compilerOptions: { + ...this._compilerOptions, + module: moduleKind, + }, }, - compilerOptions: { - ...this._compilerOptions, - module: moduleKind, - }, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - reportDiagnostics: this.ngJestConfig.shouldReportDiagnostics(fileName), - }); + customTransformers, + ); if (result.diagnostics && this.ngJestConfig.shouldReportDiagnostics(fileName)) { this.ngJestConfig.raiseDiagnostics(result.diagnostics, fileName, this._logger); } @@ -164,4 +163,117 @@ export class NgJestCompiler implements CompilerInstance { this._program = this._ts.createProgram(this._rootNames, this._compilerOptions, this._compilerHost, oldTsProgram); } + + /** + * Copy from https://github.com/microsoft/TypeScript/blob/master/src/services/transpile.ts + * This is required because the exposed function `transpileModule` from TypeScript doesn't allow to access `Program` + * and we need `Program` to be able to use Angular `replace-resources` transformer. + */ + private _transpileModule( + fileContent: string, + transpileOptions: PatchedTranspileOptions, + customTransformers: ts.CustomTransformers, + ): ts.TranspileOutput { + const diagnostics: ts.Diagnostic[] = []; + const options: ts.CompilerOptions = transpileOptions.compilerOptions + ? // @ts-expect-error internal TypeScript API + this._ts.fixupCompilerOptions(transpileOptions.compilerOptions, diagnostics) + : {}; + + // mix in default options + const defaultOptions = this._ts.getDefaultCompilerOptions(); + for (const key in defaultOptions) { + // @ts-expect-error internal TypeScript API + if (this._ts.hasProperty(defaultOptions, key) && options[key] === undefined) { + options[key] = defaultOptions[key]; + } + } + + // @ts-expect-error internal TypeScript API + for (const option of this._ts.transpileOptionValueCompilerOptions) { + options[option.name] = option.transpileOptionValue; + } + + // transpileModule does not write anything to disk so there is no need to verify that there are no conflicts between input and output paths. + options.suppressOutputPathCheck = true; + + // Filename can be non-ts file. + options.allowNonTsExtensions = true; + + // if jsx is specified then treat file as .tsx + const inputFileName = + transpileOptions.fileName || + (transpileOptions.compilerOptions && transpileOptions.compilerOptions.jsx ? 'module.tsx' : 'module.ts'); + const sourceFile = this._ts.createSourceFile(inputFileName, fileContent, options.target!); // TODO: GH#18217 + if (transpileOptions.moduleName) { + sourceFile.moduleName = transpileOptions.moduleName; + } + + if (transpileOptions.renamedDependencies) { + // @ts-expect-error internal TypeScript API + sourceFile.renamedDependencies = new Map(getEntries(transpileOptions.renamedDependencies)); + } + + // @ts-expect-error internal TypeScript API + const newLine = this._ts.getNewLineCharacter(options); + + // Output + let outputText: string | undefined; + let sourceMapText: string | undefined; + + // Create a compilerHost object to allow the compiler to read and write files + const compilerHost: ts.CompilerHost = { + // @ts-expect-error internal TypeScript API + getSourceFile: (fileName) => (fileName === this._ts.normalizePath(inputFileName) ? sourceFile : undefined), + writeFile: (name, text) => { + // @ts-expect-error internal TypeScript API + if (this._ts.fileExtensionIs(name, '.map')) { + // @ts-expect-error internal TypeScript API + this._ts.Debug.assertEqual(sourceMapText, undefined, 'Unexpected multiple source map outputs, file:', name); + sourceMapText = text; + } else { + // @ts-expect-error internal TypeScript API + this._ts.Debug.assertEqual(outputText, undefined, 'Unexpected multiple outputs, file:', name); + outputText = text; + } + }, + getDefaultLibFileName: () => 'lib.d.ts', + useCaseSensitiveFileNames: () => false, + getCanonicalFileName: (fileName) => fileName, + getCurrentDirectory: () => '', + getNewLine: () => newLine, + fileExists: (fileName): boolean => fileName === inputFileName, + readFile: () => '', + directoryExists: () => true, + getDirectories: () => [], + }; + + const program = this._ts.createProgram([inputFileName], options, compilerHost); + if (this.ngJestConfig.shouldReportDiagnostics(inputFileName)) { + // @ts-expect-error internal TypeScript API + this._ts.addRange(/*to*/ diagnostics, /*from*/ program.getSyntacticDiagnostics(sourceFile)); + // @ts-expect-error internal TypeScript API + this._ts.addRange(/*to*/ diagnostics, /*from*/ program.getOptionsDiagnostics()); + } + // Emit + program.emit( + /*targetSourceFile*/ undefined, + /*writeFile*/ undefined, + /*cancellationToken*/ undefined, + /*emitOnlyDtsFiles*/ undefined, + { + ...customTransformers, + before: [ + // hoisting from `ts-jest` or other before transformers + ...(customTransformers.before as Array>), + replaceResources(this.isAppPath, program.getTypeChecker), + ], + }, + ); + + // @ts-expect-error internal TypeScript API + if (outputText === undefined) return this._ts.Debug.fail('Output generation failed'); + + return { outputText, diagnostics, sourceMapText }; + } } diff --git a/src/transformers/inline-files.ts b/src/transformers/inline-files.ts deleted file mode 100644 index d7f9655280..0000000000 --- a/src/transformers/inline-files.ts +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Code is inspired by - * https://github.com/kulshekhar/ts-jest/blob/25e1c63dd3797793b0f46fa52fdee580b46f66ae/src/transformers/hoist-jest.ts - * - */ - -/* - * IMPLEMENTATION DETAILS: - * This transformer handles: - * - inlining referenced template files and - * - removing referenced style files. - * - * The assignments 'templateUrl', 'styleUrls' can be located anywhere in a file. - * Caveats: - * 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 type { ConfigSet } from 'ts-jest/dist/config/config-set'; -import type { - Node, - SourceFile, - TransformationContext, - Transformer, - Visitor, - PropertyAssignment, - LiteralLikeNode, - StringLiteral, -} from 'typescript'; - -import { TEMPLATE_URL, STYLE_URLS, REQUIRE, TEMPLATE } from '../constants'; - -// 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 - -/** - * Property names anywhere in an angular project to transform - */ -const TRANSFORM_PROPS = [TEMPLATE_URL, STYLE_URLS]; - -/** - * Transformer ID - * @internal - */ -export const name = 'angular-component-inline-files'; - -// 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): (ctx: TransformationContext) => Transformer { - /** - * Our compiler (typescript, or a module with typescript-like interface) - */ - const ts = cs.compilerModule; - function getCreateStringLiteral(): 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; - }; - } - const createStringLiteral = getCreateStringLiteral(); - - /** - * 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) && ts.isIdentifier(node.name) && TRANSFORM_PROPS.includes(node.name.text); - } - - /** - * Clones the assignment and manipulates it depending on its name. - * @param node the property assignment to change - */ - function transfromPropertyAssignmentForJest(node: PropertyAssignment) { - const mutableAssignment = ts.getMutableClone(node); - const assignmentNameText = (mutableAssignment.name as LiteralLikeNode).text; - switch (assignmentNameText) { - case TEMPLATE_URL: - // replace 'templateUrl' with 'template' - - // reuse the right-hand-side literal (the filepath) from the assignment - // eslint-disable-next-line no-case-declarations - let pathLiteral = mutableAssignment.initializer; - - // fix templatePathLiteral if it was a non-relative path - if (ts.isStringLiteral(pathLiteral)) { - // match if it does not start with ./ or ../ or / - // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec - if (pathLiteral.text && !pathLiteral.text.match(/^(\.\/|\.\.\/|\/)/)) { - // make path relative by prepending './' - pathLiteral = createStringLiteral(`./${pathLiteral.text}`); - } - } - - // replace current initializer with require(path) - // eslint-disable-next-line no-case-declarations - const requireCall = ts.createCall( - /* expression */ ts.createIdentifier(REQUIRE), - /* type arguments */ undefined, - /* arguments array */ [pathLiteral], - ); - - mutableAssignment.name = ts.createIdentifier(TEMPLATE); - mutableAssignment.initializer = requireCall; - break; - - case STYLE_URLS: - // replace styleUrls value with emtpy array - // inlining all urls would be way more complicated and slower - mutableAssignment.initializer = ts.createArrayLiteral(); - break; - default: - break; - } - - return mutableAssignment; - } - - /** - * 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) { - /** - * 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; - - // 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); - } - - // 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 (ctx: TransformationContext): Transformer => (sf: SourceFile) => - ts.visitNode(sf, createVisitor(ctx, sf)); -} diff --git a/src/transformers/strip-styles.ts b/src/transformers/strip-styles.ts deleted file mode 100644 index 49da3c322d..0000000000 --- a/src/transformers/strip-styles.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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 type { ConfigSet } from 'ts-jest/dist/config/config-set'; -import type { - Node, - SourceFile, - TransformationContext, - Transformer, - Visitor, - Identifier, - ClassDeclaration, - PropertyAssignment, - CallExpression, - ObjectLiteralExpression, -} from 'typescript'; - -import { STYLES, COMPONENT } from '../constants'; - -/** 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): (ctx: TransformationContext) => Transformer { - /** - * 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: CallExpression) => - ts.isIdentifier(callExpr.expression) && callExpr.expression.getText() === COMPONENT, - ) - .reduce( - (acc, nxtCallExpr: CallExpression) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - Array.prototype.concat.apply( - acc, - nxtCallExpr.arguments.filter(ts.isObjectLiteralExpression).reduce( - (acc, nxtArg: ObjectLiteralExpression) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - Array.prototype.concat.apply( - acc, - nxtArg.properties - .filter(ts.isPropertyAssignment) - .filter( - (propAss) => - ts.isIdentifier(propAss.name) && TRANSFORM_IN_DECORATOR_PROPS.includes(propAss.name.text), - ), - ), - [] as PropertyAssignment[], - ), - ), - [] as PropertyAssignment[], - ); - } - - /** - * Clones the styles assignment and manipulates it. - * @param node the property assignment to change - */ - function transformStylesAssignmentForJest(node: ClassDeclaration) { - const mutableNode = ts.getMutableClone(node); - const assignments = getInDecoratorPropertyAssignmentsToTransform(mutableNode); - - assignments.forEach((assignment: PropertyAssignment) => { - if ((assignment.name as Identifier).text === STYLES) { - // replace initializer array with empty array - assignment.initializer = ts.createArrayLiteral(); - } - }); - - 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 transformStylesAssignmentForJest(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)); -}