From 5a57a09b55251a7f980fe7a54f3baab65e87da73 Mon Sep 17 00:00:00 2001 From: Nikita Barsukov Date: Mon, 11 Mar 2024 17:14:55 +0300 Subject: [PATCH] chore(cdk): migrations for `TuiDestroyService` => `takeUntilDestroyed`(`@angular/core/rxjs-interop`) --- projects/cdk/schematics/ng-update/v4/index.ts | 2 + .../schematics/ng-update/v4/steps/index.ts | 1 + .../v4/steps/migrate-destroy-service.ts | 114 ++++++++++++++++++ .../schematic-migrate-destroy-service.spec.ts | 108 +++++++++++++++++ 4 files changed, 225 insertions(+) create mode 100644 projects/cdk/schematics/ng-update/v4/steps/migrate-destroy-service.ts create mode 100644 projects/cdk/schematics/ng-update/v4/tests/schematic-migrate-destroy-service.spec.ts diff --git a/projects/cdk/schematics/ng-update/v4/index.ts b/projects/cdk/schematics/ng-update/v4/index.ts index 09ff1cb02ef1..41293d37d86a 100644 --- a/projects/cdk/schematics/ng-update/v4/index.ts +++ b/projects/cdk/schematics/ng-update/v4/index.ts @@ -11,6 +11,7 @@ import {getExecutionTime} from '../../utils/get-execution-time'; import {projectRoot} from '../../utils/project-root'; import {removeModules, replaceIdentifiers, showWarnings} from '../steps'; import { + migrateDestroyService, migrateLegacyMask, migrateTemplates, restoreTuiMapper, @@ -34,6 +35,7 @@ function main(options: TuiSchema): Rule { restoreTuiMapper(options); restoreTuiMatcher(options); migrateLegacyMask(options); + migrateDestroyService(options); migrateTemplates(fileSystem, options); showWarnings(context, MIGRATION_WARNINGS); diff --git a/projects/cdk/schematics/ng-update/v4/steps/index.ts b/projects/cdk/schematics/ng-update/v4/steps/index.ts index 610877274f9d..78e96ef47813 100644 --- a/projects/cdk/schematics/ng-update/v4/steps/index.ts +++ b/projects/cdk/schematics/ng-update/v4/steps/index.ts @@ -1,3 +1,4 @@ +export * from './migrate-destroy-service'; export * from './migrate-legacy-mask'; export * from './migrate-templates'; export * from './restore-tui-mapper'; diff --git a/projects/cdk/schematics/ng-update/v4/steps/migrate-destroy-service.ts b/projects/cdk/schematics/ng-update/v4/steps/migrate-destroy-service.ts new file mode 100644 index 000000000000..3b73bd005371 --- /dev/null +++ b/projects/cdk/schematics/ng-update/v4/steps/migrate-destroy-service.ts @@ -0,0 +1,114 @@ +import { + addUniqueImport, + FINISH_SYMBOL, + getNamedImportReferences, + infoLog, + removeImport, + REPLACE_SYMBOL, + SMALL_TAB_SYMBOL, + titleLog, +} from '@taiga-ui/cdk/schematics'; +import {Node} from 'ng-morph'; +import type {CallExpression} from 'ts-morph'; + +import type {TuiSchema} from '../../../ng-add/schema'; + +export function migrateDestroyService(options: TuiSchema): void { + !options['skip-logs'] && + infoLog( + `${SMALL_TAB_SYMBOL}${REPLACE_SYMBOL} migrating TuiDestroyService => takeUntilDestroyed ...`, + ); + + const references = getNamedImportReferences('TuiDestroyService', '@taiga-ui/cdk'); + + references.forEach(ref => { + if (ref.wasForgotten()) { + return; + } + + const parent = ref.getParent(); + const destroyObservableUsages: Node[] = []; + + if (Node.isImportSpecifier(parent)) { + // - import {TuiDestroyService} from '@taiga-ui/cdk'; + // + import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; + removeImport(parent); + addUniqueImport( + parent.getSourceFile().getFilePath(), + 'takeUntilDestroyed', + '@angular/core/rxjs-interop', + ); + } else if ( + Node.isArrayLiteralExpression(parent) && + parent.getParent().getText().includes('providers') + ) { + // providers: [TuiDestroyService] + const index = parent + .getElements() + .findIndex(el => el.getText() === 'TuiDestroyService'); + + parent.removeElement(index); + } else if (Node.isTypeReference(parent)) { + // constructor(private destroy$: TuiDestroyService) {} + const constructorParameter = parent.getParent(); + + if (Node.isParameterDeclaration(constructorParameter)) { + destroyObservableUsages.push( + ...constructorParameter.findReferencesAsNodes(), + ); + constructorParameter.remove(); + } + } else if ( + Node.isCallExpression(parent) && + Node.isDecorator(parent.getParent()) + ) { + // constructor(@Self() @Inject(TuiDestroyService) destroy$: TuiDestroyService) {} + const constructorParameter = parent.getParent()?.getParent(); + + if (Node.isParameterDeclaration(constructorParameter)) { + destroyObservableUsages.push( + ...constructorParameter.findReferencesAsNodes(), + ); + constructorParameter.remove(); + } + } else if ( + Node.isCallExpression(parent) && + parent.getText().includes('inject(') + ) { + const injectDestination = parent.getParent(); + const possibleTakeUntil = + injectDestination && findTakeUntil(injectDestination); + + if (possibleTakeUntil) { + // takeUntil(inject(TuiDestroyService), {...}) + possibleTakeUntil.replaceWithText('takeUntilDestroyed()'); + } else if (Node.isPropertyDeclaration(injectDestination)) { + // private destroy$ = inject(TuiDestroyService), {...}); + destroyObservableUsages.push( + ...injectDestination.findReferencesAsNodes(), + ); + injectDestination.remove(); + } + } + + destroyObservableUsages.forEach(node => { + const possibleTakeUntil = findTakeUntil(node); + + if (possibleTakeUntil) { + possibleTakeUntil.replaceWithText('takeUntilDestroyed()'); + } + }); + }); + + !options['skip-logs'] && titleLog(`${FINISH_SYMBOL} successfully migrated \n`); +} + +function findTakeUntil(node: Node, maxDepth = 10): CallExpression | null { + if (Node.isCallExpression(node) && node.getText().includes('takeUntil(')) { + return node; + } + + const parent = node.getParent(); + + return parent && maxDepth ? findTakeUntil(parent, maxDepth - 1) : null; +} diff --git a/projects/cdk/schematics/ng-update/v4/tests/schematic-migrate-destroy-service.spec.ts b/projects/cdk/schematics/ng-update/v4/tests/schematic-migrate-destroy-service.spec.ts new file mode 100644 index 000000000000..50f619a816dd --- /dev/null +++ b/projects/cdk/schematics/ng-update/v4/tests/schematic-migrate-destroy-service.spec.ts @@ -0,0 +1,108 @@ +import {HostTree} from '@angular-devkit/schematics'; +import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; +import type {TuiSchema} from '@taiga-ui/cdk/schematics/ng-add/schema'; +import { + createProject, + createSourceFile, + resetActiveProject, + saveActiveProject, + setActiveProject, +} from 'ng-morph'; +import {join} from 'path'; + +const collectionPath = join(__dirname, '../../../migration.json'); + +const BEFORE = ` +import {ChangeDetectionStrategy, Component, ElementRef, inject} from '@angular/core'; +import {TuiHoveredService, TuiDestroyService, TuiObscuredService} from '@taiga-ui/cdk'; +import {fromEvent, takeUntil} from 'rxjs'; + +@Component({ + selector: 'tui-destroy-example', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TuiObscuredService, TuiDestroyService, TuiHoveredService], +}) +export class TuiDestroyExample implements OnInit { + private stop$ = inject(TuiDestroyService); + + constructor( + @Self() + @Inject(TuiDestroyService) + destroy$: TuiDestroyService, + private readonly oneMoreService: TuiHoveredService, + private oldWayDestroy: TuiDestroyService, + ) { + fromEvent(inject(ElementRef).nativeElement, 'click') + .pipe(takeUntil(inject(TuiDestroyService, {self: true}))) + .subscribe(() => console.info('click')); + + fromEvent(inject(ElementRef).nativeElement, 'blur') + .pipe(takeUntil(destroy$)) + .subscribe(() => console.info('blur')); + } + + ngOnInit() { + timer(3000).pipe(takeUntil(this.stop$)).subscribe(); + } +}`.trim(); + +const AFTER = ` +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import {ChangeDetectionStrategy, Component, ElementRef, inject} from '@angular/core'; +import {TuiHoveredService, TuiObscuredService} from '@taiga-ui/cdk'; +import {fromEvent, takeUntil} from 'rxjs'; + +@Component({ + selector: 'tui-destroy-example', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TuiObscuredService, TuiHoveredService], +}) +export class TuiDestroyExample implements OnInit { + constructor( + private readonly oneMoreService: TuiHoveredService + ) { + fromEvent(inject(ElementRef).nativeElement, 'click') + .pipe(takeUntilDestroyed()) + .subscribe(() => console.info('click')); + + fromEvent(inject(ElementRef).nativeElement, 'blur') + .pipe(takeUntilDestroyed()) + .subscribe(() => console.info('blur')); + } + + ngOnInit() { + timer(3000).pipe(takeUntilDestroyed()).subscribe(); + } +}`.trim(); + +describe('ng-update', () => { + let host: UnitTestTree; + let runner: SchematicTestRunner; + + beforeEach(() => { + host = new UnitTestTree(new HostTree()); + runner = new SchematicTestRunner('schematics', collectionPath); + + setActiveProject(createProject(host)); + + createSourceFile('test/app/test.component.ts', BEFORE); + + saveActiveProject(); + }); + + it('should migrate TuiDestroyService to takeUntilDestroyed (from `@angular/core/rxjs-interop`)', async () => { + const tree = await runner.runSchematic( + 'updateToV4', + {'skip-logs': process.env['TUI_CI'] === 'true'} as Partial, + host, + ); + + const modifiedFile = tree.readContent('test/app/test.component.ts'); + + expect(modifiedFile).toEqual(AFTER); + }); + + afterEach(() => resetActiveProject()); +});