Skip to content

Commit cab7a9b

Browse files
crisbetoatscott
authored andcommittedFeb 12, 2025·
fix(core): invalidate HMR component if replacement throws an error (#59854)
Integrates angular/angular-cli#29510 which allows us to invalidate the data in the dev server for a component if a replacement threw an error. PR Close #59854
1 parent 6e99930 commit cab7a9b

File tree

2 files changed

+52
-8
lines changed

2 files changed

+52
-8
lines changed
 

‎packages/core/src/render3/hmr.ts

+51-8
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,31 @@ import {NgZone} from '../zone';
4545
import {ViewEncapsulation} from '../metadata/view';
4646
import {NG_COMP_DEF} from './fields';
4747

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+
4855
/**
4956
* Replaces the metadata of a component type and re-renders all live instances of the component.
5057
* @param type Class whose metadata will be replaced.
5158
* @param applyMetadata Callback that will apply a new set of metadata on the `type` when invoked.
5259
* @param environment Syntehtic namespace imports that need to be passed along to the callback.
5360
* @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.
5463
* @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.
5665
* @codeGenApi
5766
*/
5867
export function ɵɵreplaceMetadata(
5968
type: Type<unknown>,
6069
applyMetadata: (...args: [Type<unknown>, unknown[], ...unknown[]]) => void,
6170
namespaces: unknown[],
6271
locals: unknown[],
72+
importMeta: ImportMetaExtended | null = null,
6373
id: string | null = null,
6474
) {
6575
ngDevMode && assertComponentDef(type);
@@ -87,7 +97,7 @@ export function ɵɵreplaceMetadata(
8797
// Note: we have the additional check, because `IsRoot` can also indicate
8898
// a component created through something like `createComponent`.
8999
if (isRootView(root) && root[PARENT] === null) {
90-
recreateMatchingLViews(newDef, oldDef, root);
100+
recreateMatchingLViews(importMeta, id, newDef, oldDef, root);
91101
}
92102
}
93103
}
@@ -132,10 +142,14 @@ function mergeWithExistingDefinition(
132142

133143
/**
134144
* 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.
135147
* @param oldDef Component definition to search for.
136148
* @param rootLView View from which to start the search.
137149
*/
138150
function recreateMatchingLViews(
151+
importMeta: ImportMetaExtended | null,
152+
id: string | null,
139153
newDef: ComponentDef<unknown>,
140154
oldDef: ComponentDef<unknown>,
141155
rootLView: LView,
@@ -152,7 +166,7 @@ function recreateMatchingLViews(
152166
// produce false positives when using inheritance.
153167
if (tView === oldDef.tView) {
154168
ngDevMode && assertComponentDef(oldDef.type);
155-
recreateLView(newDef, oldDef, rootLView);
169+
recreateLView(importMeta, id, newDef, oldDef, rootLView);
156170
return;
157171
}
158172

@@ -162,14 +176,14 @@ function recreateMatchingLViews(
162176
if (isLContainer(current)) {
163177
// The host can be an LView if a component is injecting `ViewContainerRef`.
164178
if (isLView(current[HOST])) {
165-
recreateMatchingLViews(newDef, oldDef, current[HOST]);
179+
recreateMatchingLViews(importMeta, id, newDef, oldDef, current[HOST]);
166180
}
167181

168182
for (let j = CONTAINER_HEADER_OFFSET; j < current.length; j++) {
169-
recreateMatchingLViews(newDef, oldDef, current[j]);
183+
recreateMatchingLViews(importMeta, id, newDef, oldDef, current[j]);
170184
}
171185
} else if (isLView(current)) {
172-
recreateMatchingLViews(newDef, oldDef, current);
186+
recreateMatchingLViews(importMeta, id, newDef, oldDef, current);
173187
}
174188
}
175189
}
@@ -190,11 +204,15 @@ function clearRendererCache(factory: RendererFactory, def: ComponentDef<unknown>
190204

191205
/**
192206
* Recreates an LView in-place from a new component definition.
207+
* @param importMeta `import.meta` information.
208+
* @param id HMR ID for the component.
193209
* @param newDef Definition from which to recreate the view.
194210
* @param oldDef Previous component definition being swapped out.
195211
* @param lView View to be recreated.
196212
*/
197213
function recreateLView(
214+
importMeta: ImportMetaExtended | null,
215+
id: string | null,
198216
newDef: ComponentDef<unknown>,
199217
oldDef: ComponentDef<unknown>,
200218
lView: LView<unknown>,
@@ -272,9 +290,34 @@ function recreateLView(
272290

273291
// The callback isn't guaranteed to be inside the Zone so we need to bring it in ourselves.
274292
if (zone === null) {
275-
recreate();
293+
executeWithInvalidateFallback(importMeta, id, recreate);
276294
} 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;
278321
}
279322
}
280323

‎packages/core/test/acceptance/hmr_spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2157,6 +2157,7 @@ describe('hot module replacement', () => {
21572157
},
21582158
[angularCoreEnv],
21592159
[],
2160+
null,
21602161
'',
21612162
);
21622163
}

0 commit comments

Comments
 (0)
Please sign in to comment.