Skip to content

Commit

Permalink
fix(core): invalidate HMR component if replacement throws an error (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
crisbeto authored and atscott committed Feb 12, 2025
1 parent 6e99930 commit cab7a9b
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 8 deletions.
59 changes: 51 additions & 8 deletions packages/core/src/render3/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,31 @@ import {NgZone} from '../zone';
import {ViewEncapsulation} from '../metadata/view';
import {NG_COMP_DEF} from './fields';

/** Represents `import.meta` plus some information that's not in the built-in types. */
type ImportMetaExtended = ImportMeta & {
hot?: {
send?: (name: string, payload: unknown) => void;
};
};

/**
* Replaces the metadata of a component type and re-renders all live instances of the component.
* @param type Class whose metadata will be replaced.
* @param applyMetadata Callback that will apply a new set of metadata on the `type` when invoked.
* @param environment Syntehtic namespace imports that need to be passed along to the callback.
* @param locals Local symbols from the source location that have to be exposed to the callback.
* @param importMeta `import.meta` from the call site of the replacement function. Optional since
* it isn't used internally.
* @param id ID to the class being replaced. **Not** the same as the component definition ID.
* Optional since the ID might not be available internally.
* Optional since the ID might not be available internally.
* @codeGenApi
*/
export function ɵɵreplaceMetadata(
type: Type<unknown>,
applyMetadata: (...args: [Type<unknown>, unknown[], ...unknown[]]) => void,
namespaces: unknown[],
locals: unknown[],
importMeta: ImportMetaExtended | null = null,
id: string | null = null,
) {
ngDevMode && assertComponentDef(type);
Expand Down Expand Up @@ -87,7 +97,7 @@ export function ɵɵreplaceMetadata(
// Note: we have the additional check, because `IsRoot` can also indicate
// a component created through something like `createComponent`.
if (isRootView(root) && root[PARENT] === null) {
recreateMatchingLViews(newDef, oldDef, root);
recreateMatchingLViews(importMeta, id, newDef, oldDef, root);
}
}
}
Expand Down Expand Up @@ -132,10 +142,14 @@ function mergeWithExistingDefinition(

/**
* Finds all LViews matching a specific component definition and recreates them.
* @param importMeta `import.meta` information.
* @param id HMR ID of the component.
* @param oldDef Component definition to search for.
* @param rootLView View from which to start the search.
*/
function recreateMatchingLViews(
importMeta: ImportMetaExtended | null,
id: string | null,
newDef: ComponentDef<unknown>,
oldDef: ComponentDef<unknown>,
rootLView: LView,
Expand All @@ -152,7 +166,7 @@ function recreateMatchingLViews(
// produce false positives when using inheritance.
if (tView === oldDef.tView) {
ngDevMode && assertComponentDef(oldDef.type);
recreateLView(newDef, oldDef, rootLView);
recreateLView(importMeta, id, newDef, oldDef, rootLView);
return;
}

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

for (let j = CONTAINER_HEADER_OFFSET; j < current.length; j++) {
recreateMatchingLViews(newDef, oldDef, current[j]);
recreateMatchingLViews(importMeta, id, newDef, oldDef, current[j]);
}
} else if (isLView(current)) {
recreateMatchingLViews(newDef, oldDef, current);
recreateMatchingLViews(importMeta, id, newDef, oldDef, current);
}
}
}
Expand All @@ -190,11 +204,15 @@ function clearRendererCache(factory: RendererFactory, def: ComponentDef<unknown>

/**
* Recreates an LView in-place from a new component definition.
* @param importMeta `import.meta` information.
* @param id HMR ID for the component.
* @param newDef Definition from which to recreate the view.
* @param oldDef Previous component definition being swapped out.
* @param lView View to be recreated.
*/
function recreateLView(
importMeta: ImportMetaExtended | null,
id: string | null,
newDef: ComponentDef<unknown>,
oldDef: ComponentDef<unknown>,
lView: LView<unknown>,
Expand Down Expand Up @@ -272,9 +290,34 @@ function recreateLView(

// The callback isn't guaranteed to be inside the Zone so we need to bring it in ourselves.
if (zone === null) {
recreate();
executeWithInvalidateFallback(importMeta, id, recreate);
} else {
zone.run(recreate);
zone.run(() => executeWithInvalidateFallback(importMeta, id, recreate));
}
}

/**
* Runs an HMR-related function and falls back to
* invalidating the HMR data if it throws an error.
*/
function executeWithInvalidateFallback(
importMeta: ImportMetaExtended | null,
id: string | null,
callback: () => void,
) {
try {
callback();
} catch (e) {
const errorMessage = (e as {message?: string}).message;

// If we have all the necessary information and APIs to send off the invalidation
// request, send it before rethrowing so the dev server can decide what to do.
if (id !== null && errorMessage) {
importMeta?.hot?.send?.('angular:invalidate', {id, message: errorMessage, error: true});
}

// Throw the error in case the page doesn't get refreshed.
throw e;
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/test/acceptance/hmr_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2157,6 +2157,7 @@ describe('hot module replacement', () => {
},
[angularCoreEnv],
[],
null,
'',
);
}
Expand Down

0 comments on commit cab7a9b

Please sign in to comment.