fix(ngcc): generate correct metadata for classes with getter/setter properties (#33514)
While processing class metadata, ngtsc generates a `setClassMetadata()` call which (among other things) contains info about property decorators. Previously, processing getter/setter pairs with some of ngcc's `ReflectionHost`s resulted in multiple metadata entries for the same property, which resulted in duplicate object keys, which in turn causes an error in ES5 strict mode. This commit fixes it by ensuring that there are no duplicate property names in the `setClassMetadata()` calls. In addition, `generateSetClassMetadataCall()` is updated to treat `ClassMember#decorators: []` the same as `ClassMember.decorators: null` (i.e. omitting the `ClassMember` from the generated `setClassMetadata()` call). Alternatively, ngcc's `ReflectionHost`s could be updated to do this transformation (`decorators: []` --> `decorators: null`) when reflecting on class members, but this would require changes in many places and be less future-proof. For example, given a class such as: ```ts class Foo { @Input() get bar() { return 'bar'; } set bar(value: any) {} } ``` ...previously the generated `setClassMetadata()` call would look like: ```ts ɵsetClassMetadata(..., { bar: [{type: Input}], bar: [], }); ``` The same class will now result in a call like: ```ts ɵsetClassMetadata(..., { bar: [{type: Input}], }); ``` Fixes #30569 PR Close #33514
This commit is contained in:
parent
c79d50f38f
commit
704775168d
|
@ -112,6 +112,41 @@ runInEachFileSystem(() => {
|
|||
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should generate correct metadata for decorated getter/setter properties', () => {
|
||||
genNodeModules({
|
||||
'test-package': {
|
||||
'/index.ts': `
|
||||
import {Directive, Input, NgModule} from '@angular/core';
|
||||
|
||||
@Directive({selector: '[foo]'})
|
||||
export class FooDirective {
|
||||
@Input() get bar() { return 'bar'; }
|
||||
set bar(value: string) {}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [FooDirective],
|
||||
})
|
||||
export class FooModule {}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
mainNgcc({
|
||||
basePath: '/node_modules',
|
||||
targetEntryPointPath: 'test-package',
|
||||
propertiesToConsider: ['main'],
|
||||
});
|
||||
|
||||
const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`)).replace(/\s+/g, ' ');
|
||||
expect(jsContents)
|
||||
.toContain(
|
||||
'/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(FooDirective, ' +
|
||||
'[{ type: Directive, args: [{ selector: \'[foo]\' }] }], ' +
|
||||
'function () { return []; }, ' +
|
||||
'{ bar: [{ type: Input }] });');
|
||||
});
|
||||
|
||||
describe('in async mode', () => {
|
||||
it('should run ngcc without errors for fesm2015', async() => {
|
||||
const promise = mainNgcc({
|
||||
|
|
|
@ -264,7 +264,7 @@ A.ɵdir = ɵngcc0.ɵɵdefineDirective({ type: A, selectors: [["", "a", ""]] });`
|
|||
.toEqual(`/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
|
||||
type: Directive,
|
||||
args: [{ selector: '[a]' }]
|
||||
}], null, { foo: [] });`);
|
||||
}], null, null);`);
|
||||
});
|
||||
|
||||
it('should call removeDecorators with the source code, a map of class decorators that have been analyzed',
|
||||
|
|
|
@ -56,10 +56,20 @@ export function generateSetClassMetadataCall(
|
|||
|
||||
// Do the same for property decorators.
|
||||
let metaPropDecorators: ts.Expression = ts.createNull();
|
||||
const classMembers = reflection.getMembersOfClass(clazz).filter(
|
||||
member => !member.isStatic && member.decorators !== null && member.decorators.length > 0);
|
||||
const duplicateDecoratedMemberNames =
|
||||
classMembers.map(member => member.name).filter((name, i, arr) => arr.indexOf(name) < i);
|
||||
if (duplicateDecoratedMemberNames.length > 0) {
|
||||
// This should theoretically never happen, because the only way to have duplicate instance
|
||||
// member names is getter/setter pairs and decorators cannot appear in both a getter and the
|
||||
// corresponding setter.
|
||||
throw new Error(
|
||||
`Duplicate decorated properties found on class '${clazz.name.text}': ` +
|
||||
duplicateDecoratedMemberNames.join(', '));
|
||||
}
|
||||
const decoratedMembers =
|
||||
reflection.getMembersOfClass(clazz)
|
||||
.filter(member => !member.isStatic && member.decorators !== null)
|
||||
.map(member => classMemberToMetadata(member.name, member.decorators !, isCore));
|
||||
classMembers.map(member => classMemberToMetadata(member.name, member.decorators !, isCore));
|
||||
if (decoratedMembers.length > 0) {
|
||||
metaPropDecorators = ts.createObjectLiteral(decoratedMembers);
|
||||
}
|
||||
|
|
|
@ -64,6 +64,23 @@ runInEachFileSystem(() => {
|
|||
expect(res).toContain(`{ foo: [{ type: Input }], bar: [{ type: Input, args: ['value'] }] })`);
|
||||
});
|
||||
|
||||
it('should convert decorated field getter/setter metadata', () => {
|
||||
const res = compileAndPrint(`
|
||||
import {Component, Input} from '@angular/core';
|
||||
|
||||
@Component('metadata') class Target {
|
||||
@Input() get foo() { return this._foo; }
|
||||
set foo(value: string) { this._foo = value; }
|
||||
private _foo: string;
|
||||
|
||||
get bar() { return this._bar; }
|
||||
@Input('value') set bar(value: string) { this._bar = value; }
|
||||
private _bar: string;
|
||||
}
|
||||
`);
|
||||
expect(res).toContain(`{ foo: [{ type: Input }], bar: [{ type: Input, args: ['value'] }] })`);
|
||||
});
|
||||
|
||||
it('should not convert non-angular decorators to metadata', () => {
|
||||
const res = compileAndPrint(`
|
||||
declare function NotAComponent(...args: any[]): any;
|
||||
|
@ -86,12 +103,14 @@ runInEachFileSystem(() => {
|
|||
`
|
||||
};
|
||||
|
||||
const {program} = makeProgram([
|
||||
CORE, {
|
||||
name: _('/index.ts'),
|
||||
contents,
|
||||
}
|
||||
]);
|
||||
const {program} = makeProgram(
|
||||
[
|
||||
CORE, {
|
||||
name: _('/index.ts'),
|
||||
contents,
|
||||
}
|
||||
],
|
||||
{target: ts.ScriptTarget.ES2015});
|
||||
const host = new TypeScriptReflectionHost(program.getTypeChecker());
|
||||
const target = getDeclaration(program, _('/index.ts'), 'Target', ts.isClassDeclaration);
|
||||
const call = generateSetClassMetadataCall(target, host, NOOP_DEFAULT_IMPORT_RECORDER, false);
|
||||
|
|
Loading…
Reference in New Issue