feat(core): undecorated-classes-with-decorated-fields migration should handle classes with lifecycle hooks (#36921)

As of v10, undecorated classes using Angular features are no longer
supported. In v10, we plan on removing the undecorated classes
compatibility code in ngtsc. This means that old patterns for
undecorated classes will result in compilation errors.

We had a migration for this in v9 already, but it looks like
the migration does not handle cases where classes uses lifecycle
hooks. This is handled in the ngtsc compatibility code, and we
should handle it similarly in migrations too.

This has not been outlined in the migration plan initially,
but an appendix has been added for v10 to the plan document.

https://hackmd.io/vuQfavzfRG6KUCtU7oK_EA?both.

Note: The migration is unable to determine whether a given undecorated
class that only defines `ngOnDestroy` is a directive or an actual
service. This means that in some cases the migration cannot do
more than adding a TODO and printing an failure.

Certainly there are more ways to determine the type of such classes,
but it would involve metadata and NgModule analysis. This is out of
scope for this migration.

PR Close #36921
This commit is contained in:
Paul Gschwendtner
2020-04-30 10:07:02 +02:00
committed by Alex Rickabaugh
parent 20cc3ab37e
commit c6ecdc9a81
6 changed files with 221 additions and 24 deletions

View File

@ -18,6 +18,7 @@ describe('Undecorated classes with decorated fields migration', () => {
let tree: UnitTestTree;
let tmpDirPath: string;
let previousWorkingDir: string;
let warnings: string[];
beforeEach(() => {
runner = new SchematicTestRunner('test', require.resolve('../migrations.json'));
@ -29,6 +30,13 @@ describe('Undecorated classes with decorated fields migration', () => {
projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}
}));
warnings = [];
runner.logger.subscribe(entry => {
if (entry.level === 'warn') {
warnings.push(entry.message);
}
});
previousWorkingDir = shx.pwd();
tmpDirPath = getSystemPath(host.root);
@ -228,6 +236,45 @@ describe('Undecorated classes with decorated fields migration', () => {
expect(tree.readContent('/index.ts')).toContain(`@Directive()\nexport class Base {`);
});
it('should migrate undecorated class that uses "ngOnChanges" lifecycle hook',
() => assertLifecycleHookMigrated('ngOnChanges'));
it('should migrate undecorated class that uses "ngOnInit" lifecycle hook',
() => assertLifecycleHookMigrated('ngOnInit'));
it('should migrate undecorated class that uses "ngDoCheck" lifecycle hook',
() => assertLifecycleHookMigrated('ngDoCheck'));
it('should migrate undecorated class that uses "ngAfterViewInit" lifecycle hook',
() => assertLifecycleHookMigrated('ngAfterViewInit'));
it('should migrate undecorated class that uses "ngAfterViewChecked" lifecycle hook',
() => assertLifecycleHookMigrated('ngAfterViewChecked'));
it('should migrate undecorated class that uses "ngAfterContentInit" lifecycle hook',
() => assertLifecycleHookMigrated('ngAfterContentInit'));
it('should migrate undecorated class that uses "ngAfterContentChecked" lifecycle hook',
() => assertLifecycleHookMigrated('ngAfterContentChecked'));
it(`should report an error and add a TODO for undecorated classes that only define ` +
`the "ngOnDestroy" lifecycle hook`,
async () => {
writeFile('/index.ts', `
import { Input } from '@angular/core';
export class SomeClassWithAngularFeatures {
ngOnDestroy() {
// noop for testing
}
}
`);
await runMigration();
expect(warnings.length).toBe(1);
expect(warnings[0])
.toMatch(
'index.ts@4:7: Class uses Angular features but cannot be migrated automatically. ' +
'Please add an appropriate Angular decorator.');
expect(tree.readContent('/index.ts'))
.toMatch(/TODO: Add Angular decorator\.\nexport class SomeClassWithAngularFeatures {/);
});
it('should add @Directive to undecorated derived classes of a migrated class', async () => {
writeFile('/index.ts', `
import { Input, Directive, NgModule } from '@angular/core';
@ -314,6 +361,22 @@ describe('Undecorated classes with decorated fields migration', () => {
expect(error).toBe(null);
});
async function assertLifecycleHookMigrated(lifecycleHookName: string) {
writeFile('/index.ts', `
import { Input } from '@angular/core';
export class SomeClassWithAngularFeatures {
${lifecycleHookName}() {
// noop for testing
}
}
`);
await runMigration();
expect(tree.readContent('/index.ts'))
.toContain(`@Directive()\nexport class SomeClassWithAngularFeatures {`);
}
function writeFile(filePath: string, contents: string) {
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
}