From ca1e538752d2af7de5de43701e5296b0e5cf290c Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Tue, 30 Oct 2018 10:16:52 -0700 Subject: [PATCH] feat(ivy): setClassMetadata() for assigning decorator metadata (#26860) This commit introduces the setClassMetadata() private function, which adds metadata to a type in a way that can be accessed via Angular's ReflectionCapabilities. Currently, it writes to static fields as if the metadata being added was downleveled from decorators by tsickle. The plan is for ngtsc to emit code which calls this function, passing metadata on to the runtime for testing purposes. Calls to this function would then be tree-shaken away for production bundles. Testing strategy: proper operation of this function will be an integral part of TestBed metadata overriding. Angular core tests will fail if this is broken. PR Close #26860 --- .../src/ngtsc/translator/src/translator.ts | 1 + packages/compiler/src/identifiers.ts | 1 + .../core/src/core_render3_private_export.ts | 3 +- packages/core/src/r3_symbols.ts | 2 + packages/core/src/render3/index.ts | 4 ++ packages/core/src/render3/metadata.ts | 54 +++++++++++++++ packages/core/test/render3/metadata_spec.ts | 66 +++++++++++++++++++ 7 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/render3/metadata.ts create mode 100644 packages/core/test/render3/metadata_spec.ts 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'}]}); + }); +});