From 818c51496861720a5039e8bff28ec1887c01327d Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Wed, 23 Oct 2019 11:56:30 -0700 Subject: [PATCH] feat(ivy): add a runtime feature to copy cmp/dir definitions (#33362) This commit adds CopyDefinitionFeature, which supports the case where an entire decorator (@Component or @Directive) is inherited from parent to child. The existing inheritance feature, InheritDefinitionFeature, supports merging of parent and child definitions when both were originally present. This merges things like inputs, outputs, host bindings, etc. CopyDefinitionFeature, on the other hand, compensates for a definition that was missing entirely on the child class, by copying fields that aren't ordinarily inherited (like the template function itself). This feature is intended to only be used as part of ngcc code generation. PR Close #33362 --- .../compiler/src/render3/r3_identifiers.ts | 3 + .../core/src/core_render3_private_export.ts | 1 + .../features/copy_definition_feature.ts | 93 +++++++++++++++++++ .../features/inherit_definition_feature.ts | 2 +- packages/core/src/render3/index.ts | 2 + packages/core/src/render3/jit/environment.ts | 1 + .../copy_definition_feature_spec.ts | 86 +++++++++++++++++ tools/public_api_guard/core/core.d.ts | 2 + 8 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/render3/features/copy_definition_feature.ts create mode 100644 packages/core/test/acceptance/copy_definition_feature_spec.ts diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 6b648511aafcf..286659d8b17c5 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -284,6 +284,9 @@ export class Identifiers { static InheritDefinitionFeature: o.ExternalReference = {name: 'ɵɵInheritDefinitionFeature', moduleName: CORE}; + static CopyDefinitionFeature: + o.ExternalReference = {name: 'ɵɵCopyDefinitionFeature', moduleName: CORE}; + static ProvidersFeature: o.ExternalReference = {name: 'ɵɵProvidersFeature', moduleName: CORE}; static listener: o.ExternalReference = {name: 'ɵɵlistener', moduleName: CORE}; diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index eff2b5a2bcbe6..739d8af84f7f6 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -40,6 +40,7 @@ export { ɵɵsetNgModuleScope, ɵɵtemplateRefExtractor, ɵɵProvidersFeature, + ɵɵCopyDefinitionFeature, ɵɵInheritDefinitionFeature, ɵɵNgOnChangesFeature, LifecycleHooksFeature as ɵLifecycleHooksFeature, diff --git a/packages/core/src/render3/features/copy_definition_feature.ts b/packages/core/src/render3/features/copy_definition_feature.ts new file mode 100644 index 0000000000000..3d4bc0f2569c0 --- /dev/null +++ b/packages/core/src/render3/features/copy_definition_feature.ts @@ -0,0 +1,93 @@ +/** + * @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 {ComponentDef, DirectiveDef} from '../interfaces/definition'; +import {isComponentDef} from '../interfaces/type_checks'; + +import {getSuperType} from './inherit_definition_feature'; + +/** + * Fields which exist on either directive or component definitions, and need to be copied from + * parent to child classes by the `ɵɵCopyDefinitionFeature`. + */ +const COPY_DIRECTIVE_FIELDS: (keyof DirectiveDef)[] = [ + // The child class should use the providers of its parent. + 'providersResolver', + + // Not listed here are any fields which are handled by the `ɵɵInheritDefinitionFeature`, such + // as inputs, outputs, and host binding functions. +]; + +/** + * Fields which exist only on component definitions, and need to be copied from parent to child + * classes by the `ɵɵCopyDefinitionFeature`. + * + * The type here allows any field of `ComponentDef` which is not also a property of `DirectiveDef`, + * since those should go in `COPY_DIRECTIVE_FIELDS` above. + */ +const COPY_COMPONENT_FIELDS: Exclude, keyof DirectiveDef>[] = [ + // The child class should use the template function of its parent, including all template + // semantics. + 'template', + 'decls', + 'consts', + 'vars', + 'onPush', + 'ngContentSelectors', + + // The child class should use the CSS styles of its parent, including all styling semantics. + 'styles', + 'encapsulation', + + // The child class should be checked by the runtime in the same way as its parent. + 'schemas', +]; + +/** + * Copies the fields not handled by the `ɵɵInheritDefinitionFeature` from the supertype of a + * definition. + * + * This exists primarily to support ngcc migration of an existing View Engine pattern, where an + * entire decorator is inherited from a parent to a child class. When ngcc detects this case, it + * generates a skeleton definition on the child class, and applies this feature. + * + * The `ɵɵCopyDefinitionFeature` then copies any needed fields from the parent class' definition, + * including things like the component template function. + * + * @param definition The definition of a child class which inherits from a parent class with its + * own definition. + * + * @codeGenApi + */ +export function ɵɵCopyDefinitionFeature(definition: DirectiveDef| ComponentDef): void { + let superType = getSuperType(definition.type) !; + + let superDef: DirectiveDef|ComponentDef|undefined = undefined; + if (isComponentDef(definition)) { + // Don't use getComponentDef/getDirectiveDef. This logic relies on inheritance. + superDef = superType.ɵcmp !; + } else { + // Don't use getComponentDef/getDirectiveDef. This logic relies on inheritance. + superDef = superType.ɵdir !; + } + + // Needed because `definition` fields are readonly. + const defAny = (definition as any); + + // Copy over any fields that apply to either directives or components. + for (const field of COPY_DIRECTIVE_FIELDS) { + defAny[field] = superDef[field]; + } + + if (isComponentDef(superDef)) { + // Copy over any component-specific fields. + for (const field of COPY_COMPONENT_FIELDS) { + defAny[field] = superDef[field]; + } + } +} diff --git a/packages/core/src/render3/features/inherit_definition_feature.ts b/packages/core/src/render3/features/inherit_definition_feature.ts index 618b1fdb4f204..87c35e370b64c 100644 --- a/packages/core/src/render3/features/inherit_definition_feature.ts +++ b/packages/core/src/render3/features/inherit_definition_feature.ts @@ -14,7 +14,7 @@ import {isComponentDef} from '../interfaces/type_checks'; import {ɵɵNgOnChangesFeature} from './ng_onchanges_feature'; -function getSuperType(type: Type): Type& +export function getSuperType(type: Type): Type& {ɵcmp?: ComponentDef, ɵdir?: DirectiveDef} { return Object.getPrototypeOf(type.prototype).constructor; } diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 18dd27c5d6af7..0f56d2fc30de3 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -7,6 +7,7 @@ */ import {LifecycleHooksFeature, renderComponent, whenRendered} from './component'; import {ɵɵdefineBase, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdefineNgModule, ɵɵdefinePipe, ɵɵsetComponentScope, ɵɵsetNgModuleScope} from './definition'; +import {ɵɵCopyDefinitionFeature} from './features/copy_definition_feature'; import {ɵɵInheritDefinitionFeature} from './features/inherit_definition_feature'; import {ɵɵNgOnChangesFeature} from './features/ng_onchanges_feature'; import {ɵɵProvidersFeature} from './features/providers_feature'; @@ -210,6 +211,7 @@ export { ɵɵDirectiveDefWithMeta, DirectiveType, ɵɵNgOnChangesFeature, + ɵɵCopyDefinitionFeature, ɵɵInheritDefinitionFeature, ɵɵProvidersFeature, PipeDef, diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 8f60f5a4d8ab6..01b0f72c97fb9 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -46,6 +46,7 @@ export const angularCoreEnv: {[name: string]: Function} = 'ɵɵtemplateRefExtractor': r3.ɵɵtemplateRefExtractor, 'ɵɵNgOnChangesFeature': r3.ɵɵNgOnChangesFeature, 'ɵɵProvidersFeature': r3.ɵɵProvidersFeature, + 'ɵɵCopyDefinitionFeature': r3.ɵɵCopyDefinitionFeature, 'ɵɵInheritDefinitionFeature': r3.ɵɵInheritDefinitionFeature, 'ɵɵcontainer': r3.ɵɵcontainer, 'ɵɵnextContext': r3.ɵɵnextContext, diff --git a/packages/core/test/acceptance/copy_definition_feature_spec.ts b/packages/core/test/acceptance/copy_definition_feature_spec.ts new file mode 100644 index 0000000000000..ea4c220c70179 --- /dev/null +++ b/packages/core/test/acceptance/copy_definition_feature_spec.ts @@ -0,0 +1,86 @@ +/** + * @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 {Component, NgModule, ɵɵCopyDefinitionFeature as CopyDefinitionFeature, ɵɵInheritDefinitionFeature as InheritDefinitionFeature, ɵɵdefineComponent as defineComponent} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {onlyInIvy} from '@angular/private/testing'; + +describe('Ivy CopyDefinitionFeature', () => { + onlyInIvy('this feature is not required in View Engine') + .it('should copy the template function of a component definition from parent to child', + () => { + + // It would be nice if the base component could be JIT compiled. However, this creates + // a getter for ɵcmp which precludes adding a static definition of that field for the + // child class. + // TODO(alxhub): see if there's a cleaner way to do this. + class BaseComponent { + name !: string; + static ɵcmp = defineComponent({ + type: BaseComponent, + selectors: [['some-cmp']], + decls: 0, + vars: 0, + inputs: {name: 'name'}, + template: function BaseComponent_Template(rf, ctx) { ctx.rendered = true; }, + encapsulation: 2 + }); + static ɵfac = function BaseComponent_Factory(t: any) { + return new (t || BaseComponent)(); + }; + + rendered = false; + } + + class ChildComponent extends BaseComponent { + static ɵcmp = defineComponent({ + type: ChildComponent, + selectors: [['some-cmp']], + features: [InheritDefinitionFeature, CopyDefinitionFeature], + decls: 0, + vars: 0, + template: function ChildComponent_Template(rf, ctx) {}, + encapsulation: 2 + }); + static ɵfac = function ChildComponent_Factory(t: any) { + return new (t || ChildComponent)(); + }; + } + + @NgModule({ + declarations: [ChildComponent], + exports: [ChildComponent], + }) + class Module { + } + + @Component({ + selector: 'test-cmp', + template: '', + }) + class TestCmp { + } + + TestBed.configureTestingModule({ + declarations: [TestCmp], + imports: [Module], + }); + + const fixture = TestBed.createComponent(TestCmp); + + // The child component should have matched and been instantiated. + const child = fixture.debugElement.children[0].componentInstance as ChildComponent; + expect(child instanceof ChildComponent).toBe(true); + + // And the base class template function should've been called. + expect(child.rendered).toBe(true); + + // The input binding should have worked. + expect(child.name).toBe('Success!'); + }); +}); diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 4a0795c2fa0e5..6bc18b230fb23 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -756,6 +756,8 @@ export declare function ɵɵcontainerRefreshStart(index: number): void; export declare function ɵɵcontentQuery(directiveIndex: number, predicate: Type | string[], descend: boolean, read?: any): void; +export declare function ɵɵCopyDefinitionFeature(definition: DirectiveDef | ComponentDef): void; + export declare const ɵɵdefaultStyleSanitizer: StyleSanitizeFn; export declare function ɵɵdefineBase(baseDefinition: {