fix(compiler-cli): downlevel angular decorators to static properties (#37382)

In v7 of Angular we removed `tsickle` from the default `ngc` pipeline.
This had the negative potential of breaking ES2015 output and SSR due
to a limitation in TypeScript.

TypeScript by default preserves type information for decorated constructor
parameters when `emitDecoratorMetadata` is enabled. For example,
consider this snippet below:

```
@Directive()
export class MyDirective {
  constructor(button: MyButton) {}
}

export class MyButton {}
```

TypeScript would generate metadata for the `MyDirective` class it has
a decorator applied. This metadata would be needed in JIT mode, or
for libraries that provide `MyDirective` through NPM. The metadata would
look as followed:

```
let MyDirective = class MyDir {}

MyDirective = __decorate([
  Directive(),
  __metadata("design:paramtypes", [MyButton]),
], MyDirective);

let MyButton = class MyButton {}
```

Notice that TypeScript generated calls to `__decorate` and
`__metadata`. These calls are needed so that the Angular compiler
is able to determine whether `MyDirective` is actually an directive,
and what types are needed for dependency injection.

The limitation surfaces in this concrete example because `MyButton`
is declared after the `__metadata(..)` call, while `__metadata`
actually directly references `MyButton`. This is illegal though because
`MyButton` has not been declared at this point. This is due to the
so-called temporal dead zone in JavaScript. Errors like followed will
be reported at runtime when such file/code evaluates:

```
Uncaught ReferenceError: Cannot access 'MyButton' before initialization
```

As noted, this is a TypeScript limitation because ideally TypeScript
shouldn't evaluate `__metadata`/reference `MyButton` immediately.
Instead, it should defer the reference until `MyButton` is actually
declared. This limitation will not be fixed by the TypeScript team
though because it's a limitation as per current design and they will
only revisit this once the tc39 decorator proposal is finalized
(currently stage-2 at time of writing).

Given this wontfix on the TypeScript side, and our heavy reliance on
this metadata in libraries (and for JIT mode), we intend to fix this
from within the Angular compiler by downleveling decorators to static
properties that don't need to evaluate directly. For example:

```
MyDirective.ctorParameters = () => [MyButton];
```

With this snippet above, `MyButton` is not referenced directly. Only
lazily when the Angular runtime needs it. This mitigates the temporal
dead zone issue caused by a limitation in TypeScript's decorator
metadata output. See: https://github.com/microsoft/TypeScript/issues/27519.

In the past (as noted; before version 7), the Angular compiler by
default used tsickle that already performed this transformation. We
moved the transformation to the CLI for JIT and `ng-packager`, but now
we realize that we can move this all to a single place in the compiler
so that standalone ngc consumers can benefit too, and that we can
disable tsickle in our Bazel `ngc-wrapped` pipeline (that currently
still relies on tsickle to perform this decorator processing).

This transformation also has another positive side-effect of making
Angular application/library code more compatible with server-side
rendering. In principle, TypeScript would also preserve type information
for decorated class members (similar to how it did that for constructor
parameters) at runtime. This becomes an issue when your application
relies on native DOM globals for decorated class member types. e.g.

```
@Input() panelElement: HTMLElement;
```

Your application code would then reference `HTMLElement` directly
whenever the source file is loaded in NodeJS for SSR. `HTMLElement`
does not exist on the server though, so that will become an invalid
reference. One could work around this by providing global mocks for
these DOM symbols, but that doesn't match up with other places where
dependency injection is used for mocking DOM/browser specific symbols.

More context in this issue: #30586. The TL;DR here is that the Angular
compiler does not care about types for these class members, so it won't
ever reference `HTMLElement` at runtime.

Fixes #30106. Fixes #30586. Fixes #30141.
Resolves FW-2196. Resolves FW-2199.

PR Close #37382
This commit is contained in:
Paul Gschwendtner
2020-06-05 16:26:23 +02:00
committed by atscott
parent 0a1d078a74
commit 401ef71ae5
14 changed files with 1550 additions and 420 deletions

View File

@ -10,7 +10,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {main, readCommandLineAndConfiguration, watchMode} from '../src/main';
import {main, mainDiagnosticsForTest, readCommandLineAndConfiguration, watchMode} from '../src/main';
import {setup, stripAnsi} from './test_support';
describe('ngc transformer command-line', () => {
@ -97,6 +97,103 @@ describe('ngc transformer command-line', () => {
expect(exitCode).toBe(1);
});
describe('decorator metadata', () => {
it('should add metadata as decorators if "annotationsAs" is set to "decorators"', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"compilerOptions": {
"emitDecoratorMetadata": true
},
"angularCompilerOptions": {
"annotationsAs": "decorators"
},
"files": ["mymodule.ts"]
}`);
write('aclass.ts', `export class AClass {}`);
write('mymodule.ts', `
import {NgModule} from '@angular/core';
import {AClass} from './aclass';
@NgModule({declarations: []})
export class MyModule {
constructor(importedClass: AClass) {}
}
`);
const exitCode = main(['-p', basePath], errorSpy);
expect(exitCode).toEqual(0);
const mymodulejs = path.resolve(outDir, 'mymodule.js');
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
expect(mymoduleSource).toContain('MyModule = __decorate([');
expect(mymoduleSource).toContain(`import { AClass } from './aclass';`);
expect(mymoduleSource).toContain(`__metadata("design:paramtypes", [AClass])`);
expect(mymoduleSource).not.toContain('MyModule.ctorParameters');
expect(mymoduleSource).not.toContain('MyModule.decorators');
});
it('should add metadata for Angular-decorated classes as static fields', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"files": ["mymodule.ts"]
}`);
write('aclass.ts', `export class AClass {}`);
write('mymodule.ts', `
import {NgModule} from '@angular/core';
import {AClass} from './aclass';
@NgModule({declarations: []})
export class MyModule {
constructor(importedClass: AClass) {}
}
`);
const exitCode = main(['-p', basePath], errorSpy);
expect(exitCode).toEqual(0);
const mymodulejs = path.resolve(outDir, 'mymodule.js');
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
expect(mymoduleSource).not.toContain('__decorate');
expect(mymoduleSource).toContain('args: [{ declarations: [] },] }');
expect(mymoduleSource).not.toContain(`__metadata`);
expect(mymoduleSource).toContain(`import { AClass } from './aclass';`);
expect(mymoduleSource).toContain(`{ type: AClass }`);
});
it('should not downlevel decorators for classes with custom decorators', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"files": ["mymodule.ts"]
}`);
write('aclass.ts', `export class AClass {}`);
write('decorator.ts', `
export function CustomDecorator(metadata: any) {
return (...args: any[]) => {}
}
`);
write('mymodule.ts', `
import {AClass} from './aclass';
import {CustomDecorator} from './decorator';
@CustomDecorator({declarations: []})
export class MyModule {
constructor(importedClass: AClass) {}
}
`);
const exitCode = main(['-p', basePath], errorSpy);
expect(exitCode).toEqual(0);
const mymodulejs = path.resolve(outDir, 'mymodule.js');
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
expect(mymoduleSource).toContain('__decorate');
expect(mymoduleSource).toContain('({ declarations: [] })');
expect(mymoduleSource).not.toContain('AClass');
expect(mymoduleSource).not.toContain('.ctorParameters =');
expect(mymoduleSource).not.toContain('.decorators = ');
});
});
describe('errors', () => {
beforeEach(() => {
errorSpy.and.stub();
@ -557,8 +654,6 @@ describe('ngc transformer command-line', () => {
const mymodulejs = path.resolve(outDir, 'mymodule.js');
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
expect(mymoduleSource).not.toContain('@fileoverview added by tsickle');
expect(mymoduleSource).toContain('MyComp = __decorate');
expect(mymoduleSource).not.toContain('MyComp.decorators = [');
});
it('should add closure annotations', () => {
@ -570,10 +665,14 @@ describe('ngc transformer command-line', () => {
"files": ["mymodule.ts"]
}`);
write('mymodule.ts', `
import {NgModule, Component} from '@angular/core';
import {NgModule, Component, Injectable} from '@angular/core';
@Injectable()
export class InjectedClass {}
@Component({template: ''})
export class MyComp {
constructor(injected: InjectedClass) {}
fn(p: any) {}
}
@ -588,74 +687,7 @@ describe('ngc transformer command-line', () => {
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
expect(mymoduleSource).toContain('@fileoverview added by tsickle');
expect(mymoduleSource).toContain('@param {?} p');
});
it('should add metadata as decorators', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"compilerOptions": {
"emitDecoratorMetadata": true
},
"angularCompilerOptions": {
"annotationsAs": "decorators"
},
"files": ["mymodule.ts"]
}`);
write('aclass.ts', `export class AClass {}`);
write('mymodule.ts', `
import {NgModule} from '@angular/core';
import {AClass} from './aclass';
@NgModule({declarations: []})
export class MyModule {
constructor(importedClass: AClass) {}
}
`);
const exitCode = main(['-p', basePath], errorSpy);
expect(exitCode).toEqual(0);
const mymodulejs = path.resolve(outDir, 'mymodule.js');
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
expect(mymoduleSource).toContain('MyModule = __decorate([');
expect(mymoduleSource).toContain(`import { AClass } from './aclass';`);
expect(mymoduleSource).toContain(`__metadata("design:paramtypes", [AClass])`);
});
it('should add metadata as static fields', () => {
// Note: Don't specify emitDecoratorMetadata here on purpose,
// as regression test for https://github.com/angular/angular/issues/19916.
writeConfig(`{
"extends": "./tsconfig-base.json",
"compilerOptions": {
"emitDecoratorMetadata": false
},
"angularCompilerOptions": {
"annotationsAs": "static fields"
},
"files": ["mymodule.ts"]
}`);
write('aclass.ts', `export class AClass {}`);
write('mymodule.ts', `
import {NgModule} from '@angular/core';
import {AClass} from './aclass';
@NgModule({declarations: []})
export class MyModule {
constructor(importedClass: AClass) {}
}
`);
const exitCode = main(['-p', basePath], errorSpy);
expect(exitCode).toEqual(0);
const mymodulejs = path.resolve(outDir, 'mymodule.js');
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
expect(mymoduleSource).not.toContain('__decorate');
expect(mymoduleSource).toContain('args: [{ declarations: [] },] }');
expect(mymoduleSource).not.toContain(`__metadata`);
expect(mymoduleSource).toContain(`import { AClass } from './aclass';`);
expect(mymoduleSource).toContain(`{ type: AClass }`);
expect(mymoduleSource).toMatch(/\/\*\* @nocollapse \*\/\s+MyComp\.ctorParameters = /);
});
});

View File

@ -8,6 +8,7 @@ ts_library(
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/test:test_utils",
"//packages/compiler/test:test_utils",
"//packages/core",

View File

@ -0,0 +1,624 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {TypeScriptReflectionHost} from '../../src/ngtsc/reflection';
import {getDownlevelDecoratorsTransform} from '../../src/transformers/downlevel_decorators_transform';
import {MockAotContext, MockCompilerHost} from '../mocks';
const TEST_FILE_INPUT = '/test.ts';
const TEST_FILE_OUTPUT = `/test.js`;
const TEST_FILE_DTS_OUTPUT = `/test.d.ts`;
describe('downlevel decorator transform', () => {
let host: MockCompilerHost;
let context: MockAotContext;
let diagnostics: ts.Diagnostic[];
let isClosureEnabled: boolean;
beforeEach(() => {
diagnostics = [];
context = new MockAotContext('/', {
'dom_globals.d.ts': `
declare class HTMLElement {};
declare class Document {};
`
});
host = new MockCompilerHost(context);
isClosureEnabled = false;
});
function transform(
contents: string, compilerOptions: ts.CompilerOptions = {},
preTransformers: ts.TransformerFactory<ts.SourceFile>[] = []) {
context.writeFile(TEST_FILE_INPUT, contents);
const program = ts.createProgram(
[TEST_FILE_INPUT, '/dom_globals.d.ts'], {
module: ts.ModuleKind.CommonJS,
importHelpers: true,
lib: ['dom', 'es2015'],
target: ts.ScriptTarget.ES2017,
declaration: true,
experimentalDecorators: true,
emitDecoratorMetadata: false,
...compilerOptions
},
host);
const testFile = program.getSourceFile(TEST_FILE_INPUT);
const typeChecker = program.getTypeChecker();
const reflectionHost = new TypeScriptReflectionHost(typeChecker);
const transformers: ts.CustomTransformers = {
before: [
...preTransformers,
getDownlevelDecoratorsTransform(
program.getTypeChecker(), reflectionHost, diagnostics,
/* isCore */ false, isClosureEnabled)
]
};
let output: string|null = null;
let dtsOutput: string|null = null;
const emitResult = program.emit(
testFile, ((fileName, outputText) => {
if (fileName === TEST_FILE_OUTPUT) {
output = outputText;
} else if (fileName === TEST_FILE_DTS_OUTPUT) {
dtsOutput = outputText;
}
}),
undefined, undefined, transformers);
diagnostics.push(...emitResult.diagnostics);
expect(output).not.toBeNull();
return {
output: omitLeadingWhitespace(output!),
dtsOutput: dtsOutput ? omitLeadingWhitespace(dtsOutput) : null
};
}
it('should downlevel decorators for @Injectable decorated class', () => {
const {output} = transform(`
import {Injectable} from '@angular/core';
export class ClassInject {};
@Injectable()
export class MyService {
constructor(v: ClassInject) {}
}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain(dedent`
MyService.decorators = [
{ type: core_1.Injectable }
];
MyService.ctorParameters = () => [
{ type: ClassInject }
];`);
expect(output).not.toContain('tslib');
});
it('should downlevel decorators for @Directive decorated class', () => {
const {output} = transform(`
import {Directive} from '@angular/core';
export class ClassInject {};
@Directive()
export class MyDir {
constructor(v: ClassInject) {}
}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain(dedent`
MyDir.decorators = [
{ type: core_1.Directive }
];
MyDir.ctorParameters = () => [
{ type: ClassInject }
];`);
expect(output).not.toContain('tslib');
});
it('should downlevel decorators for @Component decorated class', () => {
const {output} = transform(`
import {Component} from '@angular/core';
export class ClassInject {};
@Component({template: 'hello'})
export class MyComp {
constructor(v: ClassInject) {}
}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain(dedent`
MyComp.decorators = [
{ type: core_1.Component, args: [{ template: 'hello' },] }
];
MyComp.ctorParameters = () => [
{ type: ClassInject }
];`);
expect(output).not.toContain('tslib');
});
it('should downlevel decorators for @Pipe decorated class', () => {
const {output} = transform(`
import {Pipe} from '@angular/core';
export class ClassInject {};
@Pipe({selector: 'hello'})
export class MyPipe {
constructor(v: ClassInject) {}
}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain(dedent`
MyPipe.decorators = [
{ type: core_1.Pipe, args: [{ selector: 'hello' },] }
];
MyPipe.ctorParameters = () => [
{ type: ClassInject }
];`);
expect(output).not.toContain('tslib');
});
it('should not downlevel non-Angular class decorators', () => {
const {output} = transform(`
@SomeUnknownDecorator()
export class MyClass {}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain(dedent`
MyClass = tslib_1.__decorate([
SomeUnknownDecorator()
], MyClass);
`);
expect(output).not.toContain('MyClass.decorators');
});
it('should downlevel Angular-decorated class member', () => {
const {output} = transform(`
import {Input} from '@angular/core';
export class MyDir {
@Input() disabled: boolean = false;
}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain(dedent`
MyDir.propDecorators = {
disabled: [{ type: core_1.Input }]
};
`);
expect(output).not.toContain('tslib');
});
it('should not downlevel class member with unknown decorator', () => {
const {output} = transform(`
export class MyDir {
@SomeDecorator() disabled: boolean = false;
}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain(dedent`
tslib_1.__decorate([
SomeDecorator()
], MyDir.prototype, "disabled", void 0);
`);
expect(output).not.toContain('MyClass.propDecorators');
});
// Angular is not concerned with type information for decorated class members. Instead,
// the type is omitted. This also helps with server side rendering as DOM globals which
// are used as types, do not load at runtime. https://github.com/angular/angular/issues/30586.
it('should downlevel Angular-decorated class member but not preserve type', () => {
context.writeFile('/other-file.ts', `export class MyOtherClass {}`);
const {output} = transform(`
import {Input} from '@angular/core';
import {MyOtherClass} from './other-file';
export class MyDir {
@Input() trigger: HTMLElement;
@Input() fromOtherFile: MyOtherClass;
}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain(dedent`
MyDir.propDecorators = {
trigger: [{ type: core_1.Input }],
fromOtherFile: [{ type: core_1.Input }]
};
`);
expect(output).not.toContain('HTMLElement');
expect(output).not.toContain('MyOtherClass');
});
it('should capture constructor type metadata with `emitDecoratorMetadata` enabled', () => {
context.writeFile('/other-file.ts', `export class MyOtherClass {}`);
const {output} = transform(
`
import {Directive} from '@angular/core';
import {MyOtherClass} from './other-file';
@Directive()
export class MyDir {
constructor(other: MyOtherClass) {}
}
`,
{emitDecoratorMetadata: true});
expect(diagnostics.length).toBe(0);
expect(output).toContain('const other_file_1 = require("./other-file");');
expect(output).toContain(dedent`
MyDir.decorators = [
{ type: core_1.Directive }
];
MyDir.ctorParameters = () => [
{ type: other_file_1.MyOtherClass }
];
`);
});
it('should capture constructor type metadata with `emitDecoratorMetadata` disabled', () => {
context.writeFile('/other-file.ts', `export class MyOtherClass {}`);
const {output, dtsOutput} = transform(
`
import {Directive} from '@angular/core';
import {MyOtherClass} from './other-file';
@Directive()
export class MyDir {
constructor(other: MyOtherClass) {}
}
`,
{emitDecoratorMetadata: false});
expect(diagnostics.length).toBe(0);
expect(output).toContain('const other_file_1 = require("./other-file");');
expect(output).toContain(dedent`
MyDir.decorators = [
{ type: core_1.Directive }
];
MyDir.ctorParameters = () => [
{ type: other_file_1.MyOtherClass }
];
`);
expect(dtsOutput).toContain('import');
});
it('should properly serialize constructor parameter with external qualified name type', () => {
context.writeFile('/other-file.ts', `export class MyOtherClass {}`);
const {output} = transform(`
import {Directive} from '@angular/core';
import * as externalFile from './other-file';
@Directive()
export class MyDir {
constructor(other: externalFile.MyOtherClass) {}
}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain('const externalFile = require("./other-file");');
expect(output).toContain(dedent`
MyDir.decorators = [
{ type: core_1.Directive }
];
MyDir.ctorParameters = () => [
{ type: externalFile.MyOtherClass }
];
`);
});
it('should properly serialize constructor parameter with local qualified name type', () => {
const {output} = transform(`
import {Directive} from '@angular/core';
namespace other {
export class OtherClass {}
};
@Directive()
export class MyDir {
constructor(other: other.OtherClass) {}
}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain('var other;');
expect(output).toContain(dedent`
MyDir.decorators = [
{ type: core_1.Directive }
];
MyDir.ctorParameters = () => [
{ type: other.OtherClass }
];
`);
});
it('should properly downlevel constructor parameter decorators', () => {
const {output} = transform(`
import {Inject, Directive, DOCUMENT} from '@angular/core';
@Directive()
export class MyDir {
constructor(@Inject(DOCUMENT) document: Document) {}
}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain(dedent`
MyDir.decorators = [
{ type: core_1.Directive }
];
MyDir.ctorParameters = () => [
{ type: Document, decorators: [{ type: core_1.Inject, args: [core_1.DOCUMENT,] }] }
];
`);
});
it('should properly downlevel constructor parameters with union type', () => {
const {output} = transform(`
import {Optional, Directive, NgZone} from '@angular/core';
@Directive()
export class MyDir {
constructor(@Optional() ngZone: NgZone|null) {}
}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain(dedent`
MyDir.decorators = [
{ type: core_1.Directive }
];
MyDir.ctorParameters = () => [
{ type: core_1.NgZone, decorators: [{ type: core_1.Optional }] }
];
`);
});
it('should add @nocollapse if closure compiler is enabled', () => {
isClosureEnabled = true;
const {output} = transform(`
import {Directive} from '@angular/core';
export class ClassInject {};
@Directive()
export class MyDir {
constructor(v: ClassInject) {}
}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain(dedent`
MyDir.decorators = [
{ type: core_1.Directive }
];
/** @nocollapse */
MyDir.ctorParameters = () => [
{ type: ClassInject }
];
`);
expect(output).not.toContain('tslib');
});
it('should not retain unused type imports due to decorator downleveling with ' +
'`emitDecoratorMetadata` enabled.',
() => {
context.writeFile('/external.ts', `
export class ErrorHandler {}
export class ClassInject {}
`);
const {output} = transform(
`
import {Directive} from '@angular/core';
import {ErrorHandler, ClassInject} from './external';
@Directive()
export class MyDir {
private _errorHandler: ErrorHandler;
constructor(v: ClassInject) {}
}
`,
{module: ts.ModuleKind.ES2015, emitDecoratorMetadata: true});
expect(diagnostics.length).toBe(0);
expect(output).not.toContain('tslib');
expect(output).not.toContain('ErrorHandler');
});
it('should not retain unused type imports due to decorator downleveling with ' +
'`emitDecoratorMetadata` disabled',
() => {
context.writeFile('/external.ts', `
export class ErrorHandler {}
export class ClassInject {}
`);
const {output} = transform(
`
import {Directive} from '@angular/core';
import {ErrorHandler, ClassInject} from './external';
@Directive()
export class MyDir {
private _errorHandler: ErrorHandler;
constructor(v: ClassInject) {}
}
`,
{module: ts.ModuleKind.ES2015, emitDecoratorMetadata: false});
expect(diagnostics.length).toBe(0);
expect(output).not.toContain('tslib');
expect(output).not.toContain('ErrorHandler');
});
it('should not generate invalid reference due to conflicting parameter name', () => {
context.writeFile('/external.ts', `
export class Dep {
greet() {}
}
`);
const {output} = transform(
`
import {Directive} from '@angular/core';
import {Dep} from './external';
@Directive()
export class MyDir {
constructor(Dep: Dep) {
Dep.greet();
}
}
`,
{emitDecoratorMetadata: false});
expect(diagnostics.length).toBe(0);
expect(output).not.toContain('tslib');
expect(output).toContain(`external_1 = require("./external");`);
expect(output).toContain(dedent`
MyDir.decorators = [
{ type: core_1.Directive }
];
MyDir.ctorParameters = () => [
{ type: external_1.Dep }
];
`);
});
it('should be able to serialize circular constructor parameter type', () => {
const {output} = transform(`
import {Directive, Optional, Inject, SkipSelf} from '@angular/core';
@Directive()
export class MyDir {
constructor(@Optional() @SkipSelf() @Inject(MyDir) parentDir: MyDir|null) {}
}
`);
expect(diagnostics.length).toBe(0);
expect(output).toContain(dedent`
MyDir.decorators = [
{ type: core_1.Directive }
];
MyDir.ctorParameters = () => [
{ type: MyDir, decorators: [{ type: core_1.Optional }, { type: core_1.SkipSelf }, { type: core_1.Inject, args: [MyDir,] }] }
];
`);
});
it('should create diagnostic if property name is non-serializable', () => {
transform(`
import {Directive, ViewChild, TemplateRef} from '@angular/core';
@Directive()
export class MyDir {
@ViewChild(TemplateRef) ['some' + 'name']: TemplateRef<any>|undefined;
}
`);
expect(diagnostics.length).toBe(1);
expect(diagnostics[0].messageText as string)
.toBe(`Cannot process decorators for class element with non-analyzable name.`);
});
it('should not capture constructor parameter types when not resolving to a value', () => {
context.writeFile('/external.ts', `
export interface IState {}
export type IOverlay = {hello: true}&IState;
export default interface {
hello: false;
}
`);
const {output} = transform(`
import {Directive, Inject} from '@angular/core';
import * as angular from './external';
import {IOverlay} from './external';
import TypeFromDefaultImport from './external';
@Directive()
export class MyDir {
constructor(@Inject('$state') param: angular.IState,
@Inject('$overlay') other: IOverlay,
@Inject('$default') default: TypeFromDefaultImport) {}
}
`);
expect(diagnostics.length).toBe(0);
expect(output).not.toContain('external');
expect(output).toContain(dedent`
MyDir.decorators = [
{ type: core_1.Directive }
];
MyDir.ctorParameters = () => [
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$state',] }] },
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$overlay',] }] },
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$default',] }] }
];
`);
});
it('should allow preceding custom transformers to strip decorators', () => {
const stripAllDecoratorsTransform: ts.TransformerFactory<ts.SourceFile> = context => {
return (sourceFile: ts.SourceFile) => {
const visitNode = (node: ts.Node): ts.Node => {
if (ts.isClassDeclaration(node) || ts.isClassElement(node)) {
const cloned = ts.getMutableClone(node);
cloned.decorators = undefined;
return cloned;
}
return ts.visitEachChild(node, visitNode, context);
};
return visitNode(sourceFile) as ts.SourceFile;
};
};
const {output} = transform(
`
import {Directive} from '@angular/core';
export class MyInjectedClass {}
@Directive()
export class MyDir {
constructor(someToken: MyInjectedClass) {}
}
`,
{}, [stripAllDecoratorsTransform]);
expect(diagnostics.length).toBe(0);
expect(output).not.toContain('MyDir.decorators');
expect(output).not.toContain('MyDir.ctorParameters');
expect(output).not.toContain('tslib');
});
});
/** Template string function that can be used to dedent a given string literal. */
export function dedent(strings: TemplateStringsArray, ...values: any[]) {
let joinedString = '';
for (let i = 0; i < values.length; i++) {
joinedString += `${strings[i]}${values[i]}`;
}
joinedString += strings[strings.length - 1];
return omitLeadingWhitespace(joinedString);
}
/** Omits the leading whitespace for each line of the given text. */
function omitLeadingWhitespace(text: string): string {
return text.replace(/^\s+/gm, '');
}