refactor(devtools): revamp highlighter by hawkgs · Pull Request #69517 · angular/angular · GitHub
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 13 additions & 29 deletions devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ import {
serializeResolutionPath,
updateState,
} from './component-tree/component-tree';
import {unHighlight} from './highlighter';
import {start as startProfiling, stop as stopProfiling} from './profiling/capture';
import {disableTimingAPI, enableTimingAPI} from './profiling/timing-api';
import {getProfiler, Profiler} from './profiling/profiler';
Expand All @@ -64,6 +63,8 @@ import {runOutsideAngular, unwrapSignal} from './utils/general';
import {sanitizeObject} from './utils/serialization';
import {SignalGraphRef} from './utils/signal-graph-ref';
import {getDirectiveForestManager} from './directive-forest/manager';
import {highlightHydrationNodes, removeHydrationHighlights} from './hydration-highlighting';
import {removeAllHighlights} from './highlighter';

type InspectorRef = {ref: ComponentInspector | null};

Expand All @@ -77,6 +78,8 @@ export const subscribeToClientEvents = (

messageBus.on('shutdown', shutdownCallback(messageBus));

messageBus.on('devtoolsShutdown', devtoolsShutdownCallback(inspector));

messageBus.on(
'getLatestComponentExplorerView',
getLatestComponentExplorerViewCallback(messageBus),
Expand Down Expand Up @@ -141,6 +144,11 @@ const shutdownCallback = (messageBus: MessageBus<Events>) => () => {
messageBus.destroy();
};

const devtoolsShutdownCallback = (inspector: InspectorRef) => () => {
inspector.ref?.stopInspecting();
removeAllHighlights();
};

const getLatestComponentExplorerViewCallback =
(messageBus: MessageBus<Events>) => (query?: ComponentExplorerViewQuery) => {
// We want to force re-indexing of the component tree.
Expand Down Expand Up @@ -373,10 +381,10 @@ const setupInspector = (messageBus: MessageBus<Events>): ComponentInspector => {
messageBus.on('createHighlightOverlay', (position: ElementPosition) => {
inspector.highlightByPosition(position);
});
messageBus.on('removeHighlightOverlay', unHighlight);
messageBus.on('removeHighlightOverlay', () => inspector.unhighlight());

messageBus.on('createHydrationOverlay', inspector.highlightHydrationNodes);
messageBus.on('removeHydrationOverlay', inspector.removeHydrationHighlights);
messageBus.on('createHydrationOverlay', highlightHydrationNodes);
messageBus.on('removeHydrationOverlay', removeHydrationHighlights);

return inspector;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ ts_project(
name = "component-inspector",
srcs = ["component-inspector.ts"],
deps = [
"//devtools/projects/ng-devtools-backend/src/lib:highlighter",
"//devtools/projects/ng-devtools-backend/src/lib:interfaces",
"//devtools/projects/ng-devtools-backend/src/lib/component-tree",
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest:manager",
"//devtools/projects/ng-devtools-backend/src/lib/highlighter",
"//devtools/projects/ng-devtools-backend/src/lib/profiling",
"//devtools/projects/protocol",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {ElementPosition, HydrationStatus} from '../../../../protocol';
import {ElementPosition} from '../../../../protocol';

import {findNodeInForest} from '../component-tree/component-tree';
import {
findComponentAndHost,
highlightHydrationElement,
highlightSelectedElement,
removeHydrationHighlights,
unHighlight,
} from '../highlighter';
findNodeInForest,
getDirectiveName,
} from '../component-tree/component-tree';
import {getDirectiveForestManager} from '../directive-forest/manager';
import {highlightElement} from '../highlighter';
import {Highlight, inspectElementHighlightTemplate} from '../highlighter/highlights';
import {ComponentTreeNode} from '../interfaces';

interface Type<T> extends Function {
Expand All @@ -34,6 +33,7 @@ export class ComponentInspector {
private readonly _onComponentEnter;
private readonly _onComponentSelect;
private readonly _onComponentLeave;
private currentHighlight: Highlight | null = null;

constructor(
componentOptions: ComponentInspectorOptions = {
Expand Down Expand Up @@ -71,6 +71,7 @@ export class ComponentInspector {
}
}

// TBD(hawkgs): Check if it can be converted to mouseenter
elementMouseOver(e: MouseEvent): void {
this.cancelEvent(e);

Expand All @@ -79,9 +80,9 @@ export class ComponentInspector {
this._selectedComponent = findComponentAndHost(el);
}

unHighlight();
this.unhighlight();
if (this._selectedComponent.component && this._selectedComponent.host) {
highlightSelectedElement(this._selectedComponent.host);
this.highlightElement(this._selectedComponent.host);
this._onComponentEnter(
getDirectiveForestManager().getDirectiveId(this._selectedComponent.component)!,
);
Expand All @@ -106,65 +107,20 @@ export class ComponentInspector {
const forest: ComponentTreeNode[] = getDirectiveForestManager().getDirectiveForest();
const elementToHighlight: HTMLElement | null = findNodeInForest(position, forest);
if (elementToHighlight) {
highlightSelectedElement(elementToHighlight);
this.highlightElement(elementToHighlight);
}
}

highlightHydrationNodes(): void {
const forest: ComponentTreeNode[] = getDirectiveForestManager().getDirectiveForest();

// drop the root nodes, we don't want to highlight it
const forestWithoutRoots = forest.flatMap((rootNode) => rootNode.children);

const errorNodes = findErrorNodesForHydrationOverlay(forestWithoutRoots);

// We get the first level of hydrated nodes
// nested mismatched nodes nested in hydrated nodes aren't includes
const nodes = findNodesForHydrationOverlay(forestWithoutRoots);

// This ensures top level mismatched nodes are removed as we have a dedicated array
const otherNodes = nodes.filter(({status}) => status?.status !== 'mismatched');

for (const {node, status} of [...otherNodes, ...errorNodes]) {
highlightHydrationElement(node, status);
}
unhighlight() {
this.currentHighlight?.destroy();
this.currentHighlight = null;
}

removeHydrationHighlights() {
removeHydrationHighlights();
private highlightElement(element: HTMLElement) {
this.unhighlight();
const cmp = findComponentAndHost(element).component;
this.currentHighlight = highlightElement(element, inspectElementHighlightTemplate, {
'component-name': [getDirectiveName(cmp)],
});
}
}

/**
* Returns the first level of hydrated nodes
* Note: Mismatched nodes nested in hydrated nodes aren't included
*/
function findNodesForHydrationOverlay(
forest: ComponentTreeNode[],
): {node: Node; status: HydrationStatus}[] {
return forest.flatMap((node) => {
if (node?.hydration?.status) {
// We highlight first level
return {node: node.nativeElement!, status: node.hydration};
}
if (node.children.length) {
return findNodesForHydrationOverlay(node.children);
}
return [];
});
}

function findErrorNodesForHydrationOverlay(
forest: ComponentTreeNode[],
): {node: Node; status: HydrationStatus}[] {
return forest.flatMap((node) => {
if (node?.hydration?.status === 'mismatched') {
// We highlight first level
return {node: node.nativeElement!, status: node.hydration};
}
if (node.children.length) {
return findNodesForHydrationOverlay(node.children);
}
return [];
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -825,3 +825,29 @@ export function serializeResolutionPath(resolutionPath: Injector[]): SerializedI

return serializedResolutionPath;
}

export function findComponentAndHost(el: Node | undefined): {
component: any;
host: HTMLElement | null;
} {
const ng = ngDebugClient();
if (!el) {
return {component: null, host: null};
}
while (el) {
const component = el instanceof HTMLElement && ng.getComponent!(el);
if (component) {
return {component, host: el as HTMLElement};
}
if (!el.parentElement) {
break;
}
el = el.parentElement;
}
return {component: null, host: null};
}

// Note(hawkgs): Duplicate in directive-forest due ot cyclic dependency.
export function getDirectiveName(dir: Type<unknown> | undefined | null): string {
return dir ? dir.constructor.name : 'unknown';
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ ts_project(
"//:node_modules/@angular/core",
"//:node_modules/@types/semver",
"//:node_modules/semver",
"//devtools/projects/ng-devtools-backend/src/lib:highlighter",
"//devtools/projects/ng-devtools-backend/src/lib:interfaces",
"//devtools/projects/ng-devtools-backend/src/lib:version",
"//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api",
Expand Down
Loading
Loading