fix(compiler-cli): generating invalid setClassMetadata call in ES5 for class with custom decorator (#39527)

When a class with a custom decorator is transpiled to ES5, it looks something like this:

```
var SomeClass = (function() {
  function SomeClass() {...};
  var SomeClass_1 = __decorate([Decorator()], SomeClass);
  SomeClass = SomeClass_1;
  return SomeClass;
})();
```

The problem is that if the class also has an Angular decorator that refers to the class itself
(e.g. `{provide: someToken, useClass: SomeClass}`), the generated `setClassMetadata` code will
be emitted after the IIFE, but will still refer to the intermediate `SomeClass_1` variable from
inside the IIFE. This happens, because we generate the `setClassMetadata` call directly from
the source AST which contains identifiers that TS will rename when it emits the ES5 code.

These changes resolve the issue by looking through the metadata AST and cloning any `Identifier`
that is referring to the class. Since TS doesn't have references to the clone, it won't rename
it when transpiling to ES5.

Fixes #39509.

PR Close #39527
This commit is contained in:
Kristiyan Kostadinov 2020-11-01 12:33:52 +01:00 committed by Joey Perrott
parent be64f98076
commit 7e33cb9626
2 changed files with 65 additions and 2 deletions

View File

@ -39,8 +39,13 @@ export function generateSetClassMetadataCall(
}
const ngClassDecorators =
classDecorators.filter(dec => isAngularDecorator(dec, isCore))
.map(
(decorator: Decorator) => decoratorToMetadata(decorator, annotateForClosureCompiler));
.map(decorator => decoratorToMetadata(decorator, annotateForClosureCompiler))
// Since the `setClassMetadata` call is intended to be emitted after the class
// declaration, we have to strip references to the existing identifiers or
// TypeScript might generate invalid code when it emits to JS. In particular
// this can break when emitting a class to ES5 which has a custom decorator
// and is referenced inside of its own metadata (see #39509 for more information).
.map(decorator => removeIdentifierReferences(decorator, id.text));
if (ngClassDecorators.length === 0) {
return null;
}
@ -166,3 +171,19 @@ function decoratorToMetadata(
function isAngularDecorator(decorator: Decorator, isCore: boolean): boolean {
return isCore || (decorator.import !== null && decorator.import.from === '@angular/core');
}
/**
* Recursively recreates all of the `Identifier` descendant nodes with a particular name inside
* of an AST node, thus removing any references to them. Useful if a particular node has to be t
* aken from one place any emitted to another one exactly as it has been written.
*/
function removeIdentifierReferences<T extends ts.Node>(node: T, name: string): T {
const result = ts.transform(
node, [context => root => ts.visitNode(root, function walk(current: ts.Node): ts.Node {
return ts.isIdentifier(current) && current.text === name ?
ts.createIdentifier(current.text) :
ts.visitEachChild(current, walk, context);
})]);
return result.transformed[0];
}

View File

@ -8,6 +8,7 @@
import {AttributeMarker} from '@angular/compiler/src/core';
import {setup} from '@angular/compiler/test/aot/test_util';
import * as ts from 'typescript';
import {compile, expectEmit} from './mock_compile';
@ -3313,5 +3314,46 @@ describe('compiler compliance', () => {
const result = compile(files, angularFiles);
expectEmit(result.source, MyAppDeclaration, 'Invalid component definition');
});
it('should emit a valid setClassMetadata call in ES5 if a class with a custom decorator is referencing itself inside its own metadata',
() => {
const files = {
app: {
'spec.ts': `
import {Component, InjectionToken} from "@angular/core";
const token = new InjectionToken('token');
export function Custom() {
return function(target: any) {};
}
@Custom()
@Component({
template: '',
providers: [{ provide: token, useExisting: Comp }],
})
export class Comp {}
`
}
};
// The setClassMetadata call should look like this.
const setClassMetadata = `
(function() {
i0.ɵsetClassMetadata(Comp, [{
type: Component,
args: [{
template: '',
providers: [{ provide: token, useExisting: Comp }],
}]
}], null, null);
})();
`;
const result = compile(files, angularFiles, {target: ts.ScriptTarget.ES5});
expectEmit(result.source, setClassMetadata, 'Incorrect setClassMetadata call');
});
});
});