Skip to content

Commit

Permalink
WIP: refactor(core): draft version of HMR logic for runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewKushnir committed Feb 28, 2024
1 parent e923545 commit 3a8759c
Show file tree
Hide file tree
Showing 5 changed files with 425 additions and 11 deletions.
113 changes: 113 additions & 0 deletions packages/core/src/render3/instructions/hmr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* @license
* Copyright Google LLC 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 {Type, ViewRef} from '../../core';
import {getComponentDef} from '../definition';
import {NG_COMP_DEF} from '../fields';
import {CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
import {ComponentDef} from '../interfaces/definition';
import {TElementNode, TNode} from '../interfaces/node';
import {RElement} from '../interfaces/renderer_dom';
import {isLContainer, isLView} from '../interfaces/type_checks';
import {CONTEXT, HEADER_OFFSET, HOST, LView, PARENT, TVIEW} from '../interfaces/view';
import {clearElementContents, destroyLView} from '../node_manipulation';
import {unwrapRNode} from '../util/view_utils';
import {ViewRef as R3ViewRef} from '../view_ref';

import {refreshView} from './change_detection';
import {renderView} from './render';
import {addComponentLogic} from './shared';

export function hmr(
component: Type<unknown>, newComponentDef: ComponentDef<unknown>, viewRef: ViewRef) {
visitLView((viewRef as R3ViewRef<any>)._lView, component, newComponentDef);
}

function visitLContainer(
lContainer: LContainer, component: Type<unknown>, newComponentDef: ComponentDef<unknown>) {
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
visitLView(lContainer[i] as LView, component, newComponentDef);
}
}

function visitLView(
lView: LView, component: Type<unknown>, newComponentDef: ComponentDef<unknown>) {
const tView = lView[TVIEW];
if (lView[CONTEXT] instanceof component) {
// This LView corresponds to a component that we want to refresh,
// perform the necessary updates and exit, since all child elements
// will be recreated.
applyHmrUpdate(lView, component, newComponentDef);
return;
}
for (let i = HEADER_OFFSET; i < tView.bindingStartIndex; i++) {
if (isLContainer(lView[i])) {
const lContainer = lView[i];
visitLContainer(lContainer, component, newComponentDef);
} else if (isLView(lView[i])) {
// This is a component, enter the `visitLView` recursively.
visitLView(lView[i], component, newComponentDef);
}
}
}

function getTNodeByLViewInstance(parentLView: LView, lView: LView): TNode {
const parentTView = parentLView[TVIEW];
for (let i = HEADER_OFFSET; i < parentTView.bindingStartIndex; i++) {
if (parentLView[i] === lView) {
return parentTView.data[i] as TNode;
}
}
throw new Error('Unexpected state: LView doesn\'t belong to a given parent LView.');
}

function applyHmrUpdate(
lView: LView<unknown>, component: Type<unknown>, newComponentDef: ComponentDef<unknown>) {
// Apply an updated component def to this component type.
// Carry over some fields from an old component def to a new one.
const oldComponentDef = getComponentDef(component);
(newComponentDef as any).id = oldComponentDef?.id;
(newComponentDef as any).type = oldComponentDef?.type;
(component as any)[NG_COMP_DEF] = newComponentDef;

const tView = lView[TVIEW];
const context = lView[CONTEXT]; // instance of the component

// TODO: this needs a better handling for LContainer cases.
const parentLView = lView[PARENT] as LView;
const tNode = getTNodeByLViewInstance(parentLView, lView);

// Update LView data structures.
// TODO: this would also trigger `ngOnDestroy` hooks,
// which we probably want to avoid during HMR?
destroyLView(tView, lView);

// Remove DOM nodes from a host element.
const rElement = unwrapRNode(lView[HOST]!);
const childNodes = Array.from((rElement as HTMLElement).childNodes);
for (let childNode of childNodes) {
// Note: avoid using `clearElementContents` here, since it retains emulated
// DOM in a weird state, which breaks things afterwards.
childNode.remove();
}

// Create a new LView and TView for an updated version of a component.
const componentLView = addComponentLogic(parentLView, tNode as TElementNode, newComponentDef);
const componentTView = componentLView[TVIEW];

// Update context to use an existing instance.
componentLView[CONTEXT] = context;

// Creation mode.
// TODO: we may want to disable lifecycle hooks.
renderView(componentTView, componentLView, context);

// Update mode (change detection).
// TODO: we may want to disable lifecycle hooks.
refreshView(componentTView, componentLView, componentTView.template, context);
}
23 changes: 13 additions & 10 deletions packages/core/src/render3/instructions/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1285,7 +1285,8 @@ export function configureViewWithDirective<T>(
tView, tNode, directiveIndex, allocExpando(tView, lView, def.hostVars, NO_CHANGE), def);
}

function addComponentLogic<T>(lView: LView, hostTNode: TElementNode, def: ComponentDef<T>): void {
export function addComponentLogic<T>(
lView: LView, hostTNode: TElementNode, def: ComponentDef<T>): LView {
const native = getNativeByTNode(hostTNode, lView) as RElement;
const tView = getOrCreateComponentTView(def);

Expand All @@ -1298,15 +1299,17 @@ function addComponentLogic<T>(lView: LView, hostTNode: TElementNode, def: Compon
} else if (def.onPush) {
lViewFlags = LViewFlags.Dirty;
}
const componentView = addToViewTree(
const componentLView = addToViewTree(
lView,
createLView(
lView, tView, null, lViewFlags, native, hostTNode as TElementNode, null,
rendererFactory.createRenderer(native, def), null, null, null));

// Component view will always be created before any injected LContainers,
// so this is a regular element, wrap it with the component view
lView[hostTNode.index] = componentView;
lView[hostTNode.index] = componentLView;

return componentLView;
}

export function elementAttributeInternal(
Expand Down Expand Up @@ -1495,22 +1498,22 @@ export function refreshContentQueries(tView: TView, lView: LView): void {
* This structure will be used to traverse through nested views to remove listeners
* and call onDestroy callbacks.
*
* @param lView The view where LView or LContainer should be added
* @param adjustedHostIndex Index of the view's host node in LView[], adjusted for header
* @param parentLView The view where LView or LContainer should be added
* @param lViewOrLContainer The LView or LContainer to add to the view tree
* @returns The state passed in
*/
export function addToViewTree<T extends LView|LContainer>(lView: LView, lViewOrLContainer: T): T {
export function addToViewTree<T extends LView|LContainer>(
parentLView: LView, lViewOrLContainer: T): T {
// TODO(benlesh/misko): This implementation is incorrect, because it always adds the LContainer
// to the end of the queue, which means if the developer retrieves the LContainers from RNodes out
// of order, the change detection will run out of order, as the act of retrieving the the
// LContainer from the RNode is what adds it to the queue.
if (lView[CHILD_HEAD]) {
lView[CHILD_TAIL]![NEXT] = lViewOrLContainer;
if (parentLView[CHILD_HEAD]) {
parentLView[CHILD_TAIL]![NEXT] = lViewOrLContainer;
} else {
lView[CHILD_HEAD] = lViewOrLContainer;
parentLView[CHILD_HEAD] = lViewOrLContainer;
}
lView[CHILD_TAIL] = lViewOrLContainer;
parentLView[CHILD_TAIL] = lViewOrLContainer;
return lViewOrLContainer;
}

Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/render3/jit/directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ export function compileComponent(type: Type<any>, metadata: Component): void {
addDirectiveFactoryDef(type, metadata);

Object.defineProperty(type, NG_COMP_DEF, {
set: (def: ComponentDef<unknown>) => {
if (!ngDevMode) {
// TODO: we should probably be even more restrictive here and only allow that
// when a certain flag is set prior to this call. This should limit a possibility
// of setting this field accidentally in other code.
throw new Error('Overriding a ComponentDef on a class in prod mode is not allowed!');
}
ngComponentDef = def;
},
get: () => {
if (ngComponentDef === null) {
const compiler =
Expand Down
Loading

0 comments on commit 3a8759c

Please sign in to comment.