diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index 0005d506c8..cb9eddd0ee 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -43,6 +43,7 @@ const CORE_SUPPORTED_SYMBOLS = new Set([ 'defineInjector', 'ɵdefineNgModule', 'inject', + 'ɵsetClassMetadata', 'ɵInjectableDef', 'ɵInjectorDef', 'ɵNgModuleDefWithMeta', diff --git a/packages/compiler/src/identifiers.ts b/packages/compiler/src/identifiers.ts index b1321675be..9211c2815c 100644 --- a/packages/compiler/src/identifiers.ts +++ b/packages/compiler/src/identifiers.ts @@ -123,6 +123,7 @@ export class Identifiers { moduleName: CORE, }; static createComponentFactory: o.ExternalReference = {name: 'ɵccf', moduleName: CORE}; + static setClassMetadata: o.ExternalReference = {name: 'ɵsetClassMetadata', moduleName: CORE}; } export function createTokenForReference(reference: any): CompileTokenMetadata { diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 9dcd436e01..59a3f1c8be 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -122,7 +122,8 @@ export { i18nMapping as ɵi18nMapping, I18nInstruction as ɵI18nInstruction, I18nExpInstruction as ɵI18nExpInstruction, - WRAP_RENDERER_FACTORY2 as ɵWRAP_RENDERER_FACTORY2 + WRAP_RENDERER_FACTORY2 as ɵWRAP_RENDERER_FACTORY2, + setClassMetadata as ɵsetClassMetadata, } from './render3/index'; export { Render3DebugRendererFactory2 as ɵRender3DebugRendererFactory2 } from './render3/debug'; diff --git a/packages/core/src/r3_symbols.ts b/packages/core/src/r3_symbols.ts index 419683d2ff..b5c702c389 100644 --- a/packages/core/src/r3_symbols.ts +++ b/packages/core/src/r3_symbols.ts @@ -23,8 +23,10 @@ export {InjectableDef as ɵInjectableDef, InjectorDef as ɵInjectorDef, defineIn export {inject} from './di/injector_compatibility'; export {NgModuleDef as ɵNgModuleDef, NgModuleDefWithMeta as ɵNgModuleDefWithMeta} from './metadata/ng_module'; export {defineNgModule as ɵdefineNgModule} from './render3/definition'; +export {setClassMetadata as ɵsetClassMetadata} from './render3/metadata'; export {NgModuleFactory as ɵNgModuleFactory} from './render3/ng_module_ref'; + /** * The existence of this constant (in this particular file) informs the Angular compiler that the * current program is actually @angular/core, which needs to be compiled specially. diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 5293a856bd..c441025c40 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -113,6 +113,10 @@ export { AttributeMarker } from './interfaces/node'; +export { + setClassMetadata, +} from './metadata'; + export { pipe, pipeBind1, diff --git a/packages/core/src/render3/metadata.ts b/packages/core/src/render3/metadata.ts new file mode 100644 index 0000000000..bb5b4be0ee --- /dev/null +++ b/packages/core/src/render3/metadata.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright Google Inc. 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 {Type} from '../type'; + +interface TypeWithMetadata extends Type { + decorators?: any[]; + ctorParameters?: any[]; + propDecorators?: {[field: string]: any}; +} + +/** + * Adds decorator, constructor, and property metadata to a given type via static metadata fields + * on the type. + * + * These metadata fields can later be read with Angular's `ReflectionCapabilities` API. + * + * Calls to `setClassMetadata` can be marked as pure, resulting in the metadata assignments being + * tree-shaken away during production builds. + */ +export function setClassMetadata( + type: Type, decorators: any[] | null, ctorParameters: any[] | null, + propDecorators: {[field: string]: any} | null): void { + const clazz = type as TypeWithMetadata; + if (decorators !== null) { + if (clazz.decorators !== undefined) { + clazz.decorators.push(...decorators); + } else { + clazz.decorators = decorators; + } + } + if (ctorParameters !== null) { + // Rather than merging, clobber the existing parameters. If other projects exist which use + // tsickle-style annotations and reflect over them in the same way, this could cause issues, + // but that is vanishingly unlikely. + clazz.ctorParameters = ctorParameters; + } + if (propDecorators !== null) { + // The property decorator objects are merged as it is possible different fields have different + // decorator types. Decorators on individual fields are not merged, as it's also incredibly + // unlikely that a field will be decorated both with an Angular decorator and a non-Angular + // decorator that's also been downleveled. + if (clazz.propDecorators !== undefined) { + clazz.propDecorators = {...clazz.propDecorators, ...propDecorators}; + } else { + clazz.propDecorators = propDecorators; + } + } +} diff --git a/packages/core/test/render3/metadata_spec.ts b/packages/core/test/render3/metadata_spec.ts new file mode 100644 index 0000000000..89d7f45ff4 --- /dev/null +++ b/packages/core/test/render3/metadata_spec.ts @@ -0,0 +1,66 @@ +/** + * @license + * Copyright Google Inc. 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 {setClassMetadata} from '../../src/render3/metadata'; +import {Type} from '../../src/type'; + +interface Decorator { + type: any; + args?: any[]; +} + +interface HasMetadata extends Type { + decorators?: Decorator[]; + ctorParameters: {type: any, decorators?: Decorator[]}[]; + propDecorators: {[field: string]: Decorator[]}; +} + +interface CtorParameter { + type: any; + decorators?: Decorator[]; +} + +function metadataOf(value: Type): HasMetadata { + return value as HasMetadata; +} + +describe('render3 setClassMetadata()', () => { + it('should set decorator metadata on a type', () => { + const Foo = metadataOf(class Foo{}); + setClassMetadata(Foo, [{type: 'test', args: ['arg']}], null, null); + expect(Foo.decorators).toEqual([{type: 'test', args: ['arg']}]); + }); + + it('should merge decorator metadata on a type', () => { + const Foo = metadataOf(class Foo{}); + Foo.decorators = [{type: 'first'}]; + setClassMetadata(Foo, [{type: 'test', args: ['arg']}], null, null); + expect(Foo.decorators).toEqual([{type: 'first'}, {type: 'test', args: ['arg']}]); + }); + + it('should set ctor parameter metadata on a type', () => { + const Foo = metadataOf(class Foo{}); + Foo.ctorParameters = [{type: 'initial'}]; + setClassMetadata(Foo, null, [{type: 'test'}], null); + expect(Foo.ctorParameters).toEqual([{type: 'test'}]); + }); + + it('should set parameter decorator metadata on a type', () => { + const Foo = metadataOf(class Foo{}); + setClassMetadata(Foo, null, null, {field: [{type: 'test', args: ['arg']}]}); + expect(Foo.propDecorators).toEqual({field: [{type: 'test', args: ['arg']}]}); + }); + + it('should merge parameter decorator metadata on a type', () => { + const Foo = metadataOf(class Foo{}); + Foo.propDecorators = {initial: [{type: 'first'}]}; + setClassMetadata(Foo, null, null, {field: [{type: 'test', args: ['arg']}]}); + expect(Foo.propDecorators) + .toEqual({field: [{type: 'test', args: ['arg']}], initial: [{type: 'first'}]}); + }); +});