@@ -45,21 +45,31 @@ import {NgZone} from '../zone';
45
45
import { ViewEncapsulation } from '../metadata/view' ;
46
46
import { NG_COMP_DEF } from './fields' ;
47
47
48
+ /** Represents `import.meta` plus some information that's not in the built-in types. */
49
+ type ImportMetaExtended = ImportMeta & {
50
+ hot ?: {
51
+ send ?: ( name : string , payload : unknown ) => void ;
52
+ } ;
53
+ } ;
54
+
48
55
/**
49
56
* Replaces the metadata of a component type and re-renders all live instances of the component.
50
57
* @param type Class whose metadata will be replaced.
51
58
* @param applyMetadata Callback that will apply a new set of metadata on the `type` when invoked.
52
59
* @param environment Syntehtic namespace imports that need to be passed along to the callback.
53
60
* @param locals Local symbols from the source location that have to be exposed to the callback.
61
+ * @param importMeta `import.meta` from the call site of the replacement function. Optional since
62
+ * it isn't used internally.
54
63
* @param id ID to the class being replaced. **Not** the same as the component definition ID.
55
- * Optional since the ID might not be available internally.
64
+ * Optional since the ID might not be available internally.
56
65
* @codeGenApi
57
66
*/
58
67
export function ɵɵreplaceMetadata (
59
68
type : Type < unknown > ,
60
69
applyMetadata : ( ...args : [ Type < unknown > , unknown [ ] , ...unknown [ ] ] ) => void ,
61
70
namespaces : unknown [ ] ,
62
71
locals : unknown [ ] ,
72
+ importMeta : ImportMetaExtended | null = null ,
63
73
id : string | null = null ,
64
74
) {
65
75
ngDevMode && assertComponentDef ( type ) ;
@@ -87,7 +97,7 @@ export function ɵɵreplaceMetadata(
87
97
// Note: we have the additional check, because `IsRoot` can also indicate
88
98
// a component created through something like `createComponent`.
89
99
if ( isRootView ( root ) && root [ PARENT ] === null ) {
90
- recreateMatchingLViews ( newDef , oldDef , root ) ;
100
+ recreateMatchingLViews ( importMeta , id , newDef , oldDef , root ) ;
91
101
}
92
102
}
93
103
}
@@ -132,10 +142,14 @@ function mergeWithExistingDefinition(
132
142
133
143
/**
134
144
* Finds all LViews matching a specific component definition and recreates them.
145
+ * @param importMeta `import.meta` information.
146
+ * @param id HMR ID of the component.
135
147
* @param oldDef Component definition to search for.
136
148
* @param rootLView View from which to start the search.
137
149
*/
138
150
function recreateMatchingLViews (
151
+ importMeta : ImportMetaExtended | null ,
152
+ id : string | null ,
139
153
newDef : ComponentDef < unknown > ,
140
154
oldDef : ComponentDef < unknown > ,
141
155
rootLView : LView ,
@@ -152,7 +166,7 @@ function recreateMatchingLViews(
152
166
// produce false positives when using inheritance.
153
167
if ( tView === oldDef . tView ) {
154
168
ngDevMode && assertComponentDef ( oldDef . type ) ;
155
- recreateLView ( newDef , oldDef , rootLView ) ;
169
+ recreateLView ( importMeta , id , newDef , oldDef , rootLView ) ;
156
170
return ;
157
171
}
158
172
@@ -162,14 +176,14 @@ function recreateMatchingLViews(
162
176
if ( isLContainer ( current ) ) {
163
177
// The host can be an LView if a component is injecting `ViewContainerRef`.
164
178
if ( isLView ( current [ HOST ] ) ) {
165
- recreateMatchingLViews ( newDef , oldDef , current [ HOST ] ) ;
179
+ recreateMatchingLViews ( importMeta , id , newDef , oldDef , current [ HOST ] ) ;
166
180
}
167
181
168
182
for ( let j = CONTAINER_HEADER_OFFSET ; j < current . length ; j ++ ) {
169
- recreateMatchingLViews ( newDef , oldDef , current [ j ] ) ;
183
+ recreateMatchingLViews ( importMeta , id , newDef , oldDef , current [ j ] ) ;
170
184
}
171
185
} else if ( isLView ( current ) ) {
172
- recreateMatchingLViews ( newDef , oldDef , current ) ;
186
+ recreateMatchingLViews ( importMeta , id , newDef , oldDef , current ) ;
173
187
}
174
188
}
175
189
}
@@ -190,11 +204,15 @@ function clearRendererCache(factory: RendererFactory, def: ComponentDef<unknown>
190
204
191
205
/**
192
206
* Recreates an LView in-place from a new component definition.
207
+ * @param importMeta `import.meta` information.
208
+ * @param id HMR ID for the component.
193
209
* @param newDef Definition from which to recreate the view.
194
210
* @param oldDef Previous component definition being swapped out.
195
211
* @param lView View to be recreated.
196
212
*/
197
213
function recreateLView (
214
+ importMeta : ImportMetaExtended | null ,
215
+ id : string | null ,
198
216
newDef : ComponentDef < unknown > ,
199
217
oldDef : ComponentDef < unknown > ,
200
218
lView : LView < unknown > ,
@@ -272,9 +290,34 @@ function recreateLView(
272
290
273
291
// The callback isn't guaranteed to be inside the Zone so we need to bring it in ourselves.
274
292
if ( zone === null ) {
275
- recreate ( ) ;
293
+ executeWithInvalidateFallback ( importMeta , id , recreate ) ;
276
294
} else {
277
- zone . run ( recreate ) ;
295
+ zone . run ( ( ) => executeWithInvalidateFallback ( importMeta , id , recreate ) ) ;
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Runs an HMR-related function and falls back to
301
+ * invalidating the HMR data if it throws an error.
302
+ */
303
+ function executeWithInvalidateFallback (
304
+ importMeta : ImportMetaExtended | null ,
305
+ id : string | null ,
306
+ callback : ( ) => void ,
307
+ ) {
308
+ try {
309
+ callback ( ) ;
310
+ } catch ( e ) {
311
+ const errorMessage = ( e as { message ?: string } ) . message ;
312
+
313
+ // If we have all the necessary information and APIs to send off the invalidation
314
+ // request, send it before rethrowing so the dev server can decide what to do.
315
+ if ( id !== null && errorMessage ) {
316
+ importMeta ?. hot ?. send ?.( 'angular:invalidate' , { id, message : errorMessage , error : true } ) ;
317
+ }
318
+
319
+ // Throw the error in case the page doesn't get refreshed.
320
+ throw e ;
278
321
}
279
322
}
280
323
0 commit comments