Skip to content

Commit

Permalink
chore(cdk): migrations for TuiDestroyService => `takeUntilDestroyed…
Browse files Browse the repository at this point in the history
…`(`@angular/core/rxjs-interop`)
  • Loading branch information
nsbarsukov committed Mar 11, 2024
1 parent 84907b7 commit 5a57a09
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 0 deletions.
2 changes: 2 additions & 0 deletions projects/cdk/schematics/ng-update/v4/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -34,6 +35,7 @@ function main(options: TuiSchema): Rule {
restoreTuiMapper(options);
restoreTuiMatcher(options);
migrateLegacyMask(options);
migrateDestroyService(options);

migrateTemplates(fileSystem, options);
showWarnings(context, MIGRATION_WARNINGS);
Expand Down
1 change: 1 addition & 0 deletions projects/cdk/schematics/ng-update/v4/steps/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './migrate-destroy-service';
export * from './migrate-legacy-mask';
export * from './migrate-templates';
export * from './restore-tui-mapper';
Expand Down
114 changes: 114 additions & 0 deletions projects/cdk/schematics/ng-update/v4/steps/migrate-destroy-service.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<TuiSchema>,
host,
);

const modifiedFile = tree.readContent('test/app/test.component.ts');

expect(modifiedFile).toEqual(AFTER);
});

afterEach(() => resetActiveProject());
});

0 comments on commit 5a57a09

Please sign in to comment.