77 */
88
99import { Type } from '../interface/type' ;
10- import { assertDefined , assertNotEqual } from '../util/assert' ;
10+ import { assertDefined , assertEqual , assertNotEqual } from '../util/assert' ;
1111import { assertLView } from './assert' ;
1212import { getComponentDef } from './def_getters' ;
1313import { assertComponentDef } from './errors' ;
@@ -45,6 +45,7 @@ import {destroyLView, removeViewFromDOM} from './node_manipulation';
4545import { RendererFactory } from './interfaces/renderer' ;
4646import { NgZone } from '../zone' ;
4747import { ViewEncapsulation } from '../metadata/view' ;
48+ import { NG_COMP_DEF } from './fields' ;
4849
4950/**
5051 * Replaces the metadata of a component type and re-renders all live instances of the component.
@@ -61,7 +62,7 @@ export function ɵɵreplaceMetadata(
6162 locals : unknown [ ] ,
6263) {
6364 ngDevMode && assertComponentDef ( type ) ;
64- const oldDef = getComponentDef ( type ) ! ;
65+ const currentDef = getComponentDef ( type ) ! ;
6566
6667 // The reason `applyMetadata` is a callback that is invoked (almost) immediately is because
6768 // the compiler usually produces more code than just the component definition, e.g. there
@@ -70,6 +71,13 @@ export function ɵɵreplaceMetadata(
7071 // them at the right time.
7172 applyMetadata . apply ( null , [ type , namespaces , ...locals ] ) ;
7273
74+ const { newDef, oldDef} = mergeWithExistingDefinition ( currentDef , getComponentDef ( type ) ! ) ;
75+
76+ // TODO(crisbeto): the `applyMetadata` call above will replace the definition on the type.
77+ // Ideally we should adjust the compiler output so the metadata is returned, however that'll
78+ // require some internal changes. We re-add the metadata here manually.
79+ ( type as any ) [ NG_COMP_DEF ] = newDef ;
80+
7381 // If a `tView` hasn't been created yet, it means that this component hasn't been instantianted
7482 // before. In this case there's nothing left for us to do aside from patching it in.
7583 if ( oldDef . tView ) {
@@ -78,18 +86,54 @@ export function ɵɵreplaceMetadata(
7886 // Note: we have the additional check, because `IsRoot` can also indicate
7987 // a component created through something like `createComponent`.
8088 if ( root [ FLAGS ] & LViewFlags . IsRoot && root [ PARENT ] === null ) {
81- recreateMatchingLViews ( oldDef , root ) ;
89+ recreateMatchingLViews ( newDef , oldDef , root ) ;
8290 }
8391 }
8492 }
8593}
8694
95+ /**
96+ * Merges two component definitions while preseving the original one in place.
97+ * @param currentDef Definition that should receive the new metadata.
98+ * @param newDef Source of the new metadata.
99+ */
100+ function mergeWithExistingDefinition (
101+ currentDef : ComponentDef < unknown > ,
102+ newDef : ComponentDef < unknown > ,
103+ ) {
104+ // Clone the current definition since we reference its original data further
105+ // down in the replacement process (e.g. when destroying the renderer).
106+ const clone = { ...currentDef } ;
107+
108+ // Assign the new metadata in place while preserving the object literal. It's important to
109+ // Keep the object in place, because there can be references to it, for example in the
110+ // `directiveDefs` of another definition.
111+ const replacement = Object . assign ( currentDef , newDef , {
112+ // We need to keep the existing directive and pipe defs, because they can get patched on
113+ // by a call to `setComponentScope` from a module file. That call won't make it into the
114+ // HMR replacement function, because it lives in an entirely different file.
115+ directiveDefs : clone . directiveDefs ,
116+ pipeDefs : clone . pipeDefs ,
117+
118+ // Preserve the old `setInput` function, because it has some state.
119+ // This is fine, because the component instance is preserved as well.
120+ setInput : clone . setInput ,
121+ } ) ;
122+
123+ ngDevMode && assertEqual ( replacement , currentDef , 'Expected definition to be merged in place' ) ;
124+ return { newDef : replacement , oldDef : clone } ;
125+ }
126+
87127/**
88128 * Finds all LViews matching a specific component definition and recreates them.
89129 * @param oldDef Component definition to search for.
90130 * @param rootLView View from which to start the search.
91131 */
92- function recreateMatchingLViews ( oldDef : ComponentDef < unknown > , rootLView : LView ) : void {
132+ function recreateMatchingLViews (
133+ newDef : ComponentDef < unknown > ,
134+ oldDef : ComponentDef < unknown > ,
135+ rootLView : LView ,
136+ ) : void {
93137 ngDevMode &&
94138 assertDefined (
95139 oldDef . tView ,
@@ -102,7 +146,7 @@ function recreateMatchingLViews(oldDef: ComponentDef<unknown>, rootLView: LView)
102146 // produce false positives when using inheritance.
103147 if ( tView === oldDef . tView ) {
104148 ngDevMode && assertComponentDef ( oldDef . type ) ;
105- recreateLView ( getComponentDef ( oldDef . type ) ! , oldDef , rootLView ) ;
149+ recreateLView ( newDef , oldDef , rootLView ) ;
106150 return ;
107151 }
108152
@@ -112,14 +156,14 @@ function recreateMatchingLViews(oldDef: ComponentDef<unknown>, rootLView: LView)
112156 if ( isLContainer ( current ) ) {
113157 // The host can be an LView if a component is injecting `ViewContainerRef`.
114158 if ( isLView ( current [ HOST ] ) ) {
115- recreateMatchingLViews ( oldDef , current [ HOST ] ) ;
159+ recreateMatchingLViews ( newDef , oldDef , current [ HOST ] ) ;
116160 }
117161
118162 for ( let j = CONTAINER_HEADER_OFFSET ; j < current . length ; j ++ ) {
119- recreateMatchingLViews ( oldDef , current [ j ] ) ;
163+ recreateMatchingLViews ( newDef , oldDef , current [ j ] ) ;
120164 }
121165 } else if ( isLView ( current ) ) {
122- recreateMatchingLViews ( oldDef , current ) ;
166+ recreateMatchingLViews ( newDef , oldDef , current ) ;
123167 }
124168 }
125169}
0 commit comments