refactor(compiler-cli): skip class decorators in tooling constructor parameters transform (#37545)

We recently added a transformer to NGC that is responsible for downleveling Angular
decorators and constructor parameter types. The primary goal was to mitigate a
TypeScript limitation/issue that surfaces in Angular projects due to the heavy
reliance on type metadata being captured for DI. Additionally this is a pre-requisite
of making `tsickle` optional in the Angular bazel toolchain.

See: 401ef71ae5 for more context on this.

Another (less important) goal was to make sure that the CLI can re-use
this transformer for its JIT mode compilation. The CLI (as outlined in
the commit mentioned above), already has a transformer for downleveling
constructor parameters. We want to avoid this duplication and exported
the transform through the tooling-private compiler entry-point.

Early experiments in using this transformer over the current one, highlighted
that in JIT, class decorators cannot be downleveled. Angular relies on those
to be invoked immediately for JIT (so that factories etc. are generated upon loading)

The transformer we exposed, always downlevels such class decorators
though, so that would break CLI's JIT mode. We can address the CLI's
needs by adding another flag to skip class decorators. This will allow
us to continue with the goal of de-duplication.

PR Close #37545
This commit is contained in:
Paul Gschwendtner
2020-06-12 00:22:00 +02:00
committed by Misko Hevery
parent cc49a91de7
commit 66e6b932d8
4 changed files with 122 additions and 13 deletions

View File

@ -21,6 +21,7 @@ describe('downlevel decorator transform', () => {
let context: MockAotContext;
let diagnostics: ts.Diagnostic[];
let isClosureEnabled: boolean;
let skipClassDecorators: boolean;
beforeEach(() => {
diagnostics = [];
@ -32,6 +33,7 @@ describe('downlevel decorator transform', () => {
});
host = new MockCompilerHost(context);
isClosureEnabled = false;
skipClassDecorators = false;
});
function transform(
@ -58,7 +60,7 @@ describe('downlevel decorator transform', () => {
...preTransformers,
getDownlevelDecoratorsTransform(
program.getTypeChecker(), reflectionHost, diagnostics,
/* isCore */ false, isClosureEnabled)
/* isCore */ false, isClosureEnabled, skipClassDecorators)
]
};
let output: string|null = null;
@ -606,6 +608,97 @@ describe('downlevel decorator transform', () => {
expect(output).not.toContain('MyDir.ctorParameters');
expect(output).not.toContain('tslib');
});
describe('class decorators skipped', () => {
beforeEach(() => skipClassDecorators = true);
it('should not downlevel Angular class decorators', () => {
const {output} = transform(`
import {Injectable} from '@angular/core';
@Injectable()
export class MyService {}
`);
expect(diagnostics.length).toBe(0);
expect(output).not.toContain('MyService.decorators');
expect(output).toContain(dedent`
MyService = tslib_1.__decorate([
core_1.Injectable()
], MyService);
`);
});
it('should downlevel constructor parameters', () => {
const {output} = transform(`
import {Injectable} from '@angular/core';
@Injectable()
export class InjectClass {}
@Injectable()
export class MyService {
constructor(dep: InjectClass) {}
}
`);
expect(diagnostics.length).toBe(0);
expect(output).not.toContain('MyService.decorators');
expect(output).toContain('MyService.ctorParameters');
expect(output).toContain(dedent`
MyService.ctorParameters = () => [
{ type: InjectClass }
];
MyService = tslib_1.__decorate([
core_1.Injectable()
], MyService);
`);
});
it('should downlevel constructor parameter decorators', () => {
const {output} = transform(`
import {Injectable, Inject} from '@angular/core';
@Injectable()
export class InjectClass {}
@Injectable()
export class MyService {
constructor(@Inject('test') dep: InjectClass) {}
}
`);
expect(diagnostics.length).toBe(0);
expect(output).not.toContain('MyService.decorators');
expect(output).toContain('MyService.ctorParameters');
expect(output).toContain(dedent`
MyService.ctorParameters = () => [
{ type: InjectClass, decorators: [{ type: core_1.Inject, args: ['test',] }] }
];
MyService = tslib_1.__decorate([
core_1.Injectable()
], MyService);
`);
});
it('should downlevel class member Angular decorators', () => {
const {output} = transform(`
import {Injectable, Input} from '@angular/core';
export class MyService {
@Input() disabled: boolean;
}
`);
expect(diagnostics.length).toBe(0);
expect(output).not.toContain('tslib');
expect(output).toContain(dedent`
MyService.propDecorators = {
disabled: [{ type: core_1.Input }]
};
`);
});
});
});
/** Template string function that can be used to dedent a given string literal. */