From b560e02cdff35e1c6d7ad5923809df6c2b8a4cd8 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Fri, 19 Jan 2024 20:29:24 +0100 Subject: [PATCH] refactor(devtools): Add hydration informations (#53910) This commit adds hydration informations to the devtools. * List of hydrated/hydrated components * Shows hydration overlays * Shows hydration errors for NG0500, 501 & 502 PR Close #53910 --- .../ng-devtools-backend/src/lib/BUILD.bazel | 25 + .../src/lib/client-event-subscribers.spec.ts | 77 +++ .../src/lib/client-event-subscribers.ts | 24 +- .../component-inspector.spec.ts | 5 + .../component-inspector.ts | 72 ++- .../src/lib/component-tree.ts | 2 +- .../src/lib/directive-forest/BUILD.bazel | 2 + .../src/lib/directive-forest/ltree.ts | 2 + .../src/lib/directive-forest/render-tree.ts | 21 + .../src/lib/highlighter.spec.ts | 104 ++++ .../src/lib/highlighter.ts | 190 +++++-- .../src/lib/hooks/identity-tracker.ts | 1 + .../src/lib/hooks/index.ts | 16 +- .../devtools-tabs.component.html | 1 + .../devtools-tabs/devtools-tabs.component.ts | 2 + .../directive-explorer/BUILD.bazel | 4 + .../directive-explorer.component.html | 13 +- .../directive-explorer.component.scss | 11 +- .../directive-explorer.component.ts | 25 +- .../directive-explorer.spec.ts | 74 ++- .../component-data-source.spec.ts | 14 + .../component-data-source/index.ts | 4 +- .../directive-forest.component.html | 43 +- .../directive-forest.component.scss | 71 ++- .../index-forest/index-forest.spec.ts | 12 + .../directive-forest/index-forest/index.ts | 3 +- .../property-resolver/BUILD.bazel | 1 + .../element-property-resolver.spec.ts | 5 +- .../devtools-tabs/injector-tree/BUILD.bazel | 1 + .../injector-tree/injector-tree-fns.spec.ts | 467 +++++++++++++++++- .../src/lib/devtools.component.html | 2 +- .../ng-devtools/src/lib/devtools.component.ts | 4 +- .../projects/protocol/src/lib/messages.ts | 14 + .../shared-utils/src/lib/angular-check.ts | 8 + devtools/tsconfig.json | 2 +- packages/core/src/core_private_export.ts | 2 +- packages/core/src/hydration/error_handling.ts | 31 +- packages/core/src/hydration/utils.ts | 108 +++- .../core/src/render3/instructions/element.ts | 7 +- .../platform-server/test/hydration_spec.ts | 6 +- 40 files changed, 1347 insertions(+), 129 deletions(-) create mode 100644 devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.spec.ts diff --git a/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel index 063ad2079c2..367ee7622cb 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel @@ -40,6 +40,10 @@ ts_test_library( ts_library( name = "highlighter", srcs = ["highlighter.ts"], + deps = [ + "//devtools/projects/protocol", + "//packages/core", + ], ) ts_library( @@ -100,6 +104,27 @@ ts_library( srcs = ["version.ts"], ) +karma_web_test_suite( + name = "client_event_subscribers_test", + deps = [ + ":client_event_subscribers_test_lib", + ], +) + +ts_test_library( + name = "client_event_subscribers_test_lib", + srcs = [ + "client-event-subscribers.spec.ts", + ], + deps = [ + ":client_event_subscribers", + "//devtools/projects/ng-devtools-backend/src/lib/hooks", + "//devtools/projects/protocol", + "//devtools/projects/shared-utils", + "@npm//rxjs", + ], +) + ts_library( name = "client_event_subscribers", srcs = ["client-event-subscribers.ts"], diff --git a/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.spec.ts b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.spec.ts new file mode 100644 index 00000000000..38dcde69fe9 --- /dev/null +++ b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.spec.ts @@ -0,0 +1,77 @@ +/** + * @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 {Events, MessageBus} from 'protocol'; +import {subscribeToClientEvents} from './client-event-subscribers'; +import {appIsAngular, appIsAngularIvy, appIsSupportedAngularVersion} from 'shared-utils'; +import {DirectiveForestHooks} from './hooks/hooks'; +import {of} from 'rxjs'; + +describe('ClientEventSubscriber', () => { + let messageBusMock: MessageBus; + let appNode: HTMLElement | null = null; + + beforeEach(() => { + // mock isAngular et al + appNode = mockAngular(); + + messageBusMock = jasmine.createSpyObj>('messageBus', [ + 'on', + 'once', + 'emit', + 'destroy', + ]); + }); + + afterEach(() => { + // clearing the dom after each test + if (appNode) { + document.body.removeChild(appNode); + appNode = null; + } + }); + + it('is it Angular ready (testing purposed)', () => { + expect(appIsAngular()).withContext('isAng').toBe(true); + expect(appIsSupportedAngularVersion()).withContext('appIsSupportedAngularVersion').toBe(true); + expect(appIsAngularIvy()).withContext('appIsAngularIvy').toBe(true); + }); + + it('should setup inspector', () => { + subscribeToClientEvents(messageBusMock, {directiveForestHooks: MockDirectiveForestHooks}); + + expect(messageBusMock.on).toHaveBeenCalledWith('inspectorStart', jasmine.any(Function)); + expect(messageBusMock.on).toHaveBeenCalledWith('inspectorEnd', jasmine.any(Function)); + expect(messageBusMock.on).toHaveBeenCalledWith('createHighlightOverlay', jasmine.any(Function)); + expect(messageBusMock.on).toHaveBeenCalledWith('removeHighlightOverlay', jasmine.any(Function)); + expect(messageBusMock.on).toHaveBeenCalledWith('createHydrationOverlay', jasmine.any(Function)); + expect(messageBusMock.on).toHaveBeenCalledWith('removeHydrationOverlay', jasmine.any(Function)); + }); +}); + +function mockAngular() { + const appNode = document.createElement('app'); + appNode.setAttribute('ng-version', '17.0.0'); + (appNode as any).__ngContext__ = true; + document.body.appendChild(appNode); + + (window as any) = { + ng: { + getComponent: () => {}, + }, + }; + return appNode; +} + +class MockDirectiveForestHooks extends DirectiveForestHooks { + profiler = { + subscribe: () => {}, + changeDetection$: of(), + } as any as DirectiveForestHooks['profiler']; + initialize = () => {}; +} diff --git a/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts index d4fded2b913..d41a1cbddea 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts @@ -25,6 +25,7 @@ import { appIsAngularIvy, appIsSupportedAngularVersion, getAngularVersion, + isHydrationEnabled, } from 'shared-utils'; import {ComponentInspector} from './component-inspector/component-inspector'; @@ -50,9 +51,15 @@ import {ComponentTreeNode} from './interfaces'; import {ngDebugDependencyInjectionApiIsSupported} from './ng-debug-api/ng-debug-api'; import {setConsoleReference} from './set-console-reference'; import {serializeDirectiveState} from './state-serializer/state-serializer'; -import {hasDiDebugAPIs, runOutsideAngular} from './utils'; +import {runOutsideAngular} from './utils'; +import {DirectiveForestHooks} from './hooks/hooks'; -export const subscribeToClientEvents = (messageBus: MessageBus): void => { +export const subscribeToClientEvents = ( + messageBus: MessageBus, + depsForTestOnly?: { + directiveForestHooks?: typeof DirectiveForestHooks; + }, +): void => { messageBus.on('shutdown', shutdownCallback(messageBus)); messageBus.on( @@ -86,7 +93,7 @@ export const subscribeToClientEvents = (messageBus: MessageBus): void => // update requests, instead we want to request an update at most // once every 250ms runOutsideAngular(() => { - initializeOrGetDirectiveForestHooks() + initializeOrGetDirectiveForestHooks(depsForTestOnly) .profiler.changeDetection$.pipe(debounceTime(250)) .subscribe(() => messageBus.emit('componentTreeDirty')); }); @@ -219,7 +226,12 @@ const checkForAngular = (messageBus: MessageBus): void => { } messageBus.emit('ngAvailability', [ - {version: ngVersion.toString(), devMode: appIsAngularInDevMode(), ivy: appIsIvy}, + { + version: ngVersion.toString(), + devMode: appIsAngularInDevMode(), + ivy: appIsIvy, + hydration: isHydrationEnabled(), + }, ]); }; @@ -243,6 +255,9 @@ const setupInspector = (messageBus: MessageBus) => { inspector.highlightByPosition(position); }); messageBus.on('removeHighlightOverlay', unHighlight); + + messageBus.on('createHydrationOverlay', inspector.highlightHydrationNodes); + messageBus.on('removeHydrationOverlay', inspector.removeHydrationHighlights); }; export interface SerializableDirectiveInstanceType extends DirectiveType { @@ -281,6 +296,7 @@ const prepareForestForSerialization = ( id: initializeOrGetDirectiveForestHooks().getDirectiveId(d.instance)!, })), children: prepareForestForSerialization(node.children, includeResolutionPath), + hydration: node.hydration, }; serializedNodes.push(serializedNode); diff --git a/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.spec.ts b/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.spec.ts index cd6c90208b5..538fa7346bb 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.spec.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {initializeOrGetDirectiveForestHooks} from '../hooks'; import {ComponentInspector} from './component-inspector'; describe('ComponentInspector', () => { @@ -42,4 +43,8 @@ describe('ComponentInspector', () => { expect(mouseEventSpy.stopImmediatePropagation).toHaveBeenCalledTimes(1); expect(mouseEventSpy.preventDefault).toHaveBeenCalledTimes(1); }); + + it('should always retrieve the same forest hook', () => { + expect(initializeOrGetDirectiveForestHooks()).toBe(initializeOrGetDirectiveForestHooks()); + }); }); diff --git a/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts b/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts index aff2976adec..f2fca08ebe6 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts @@ -6,10 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {ElementPosition} from 'protocol'; +import {ElementPosition, HydrationStatus} from 'protocol'; import {findNodeInForest} from '../component-tree'; -import {findComponentAndHost, highlight, unHighlight} from '../highlighter'; +import { + findComponentAndHost, + highlightHydrationElement, + highlightSelectedElement, + removeHydrationHighlights, + unHighlight, +} from '../highlighter'; import {initializeOrGetDirectiveForestHooks} from '../hooks'; import {ComponentTreeNode} from '../interfaces'; @@ -74,7 +80,7 @@ export class ComponentInspector { unHighlight(); if (this._selectedComponent.component && this._selectedComponent.host) { - highlight(this._selectedComponent.host); + highlightSelectedElement(this._selectedComponent.host); this._onComponentEnter( initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!, ); @@ -99,7 +105,65 @@ export class ComponentInspector { const forest: ComponentTreeNode[] = initializeOrGetDirectiveForestHooks().getDirectiveForest(); const elementToHighlight: HTMLElement | null = findNodeInForest(position, forest); if (elementToHighlight) { - highlight(elementToHighlight); + highlightSelectedElement(elementToHighlight); } } + + highlightHydrationNodes(): void { + const forest: ComponentTreeNode[] = initializeOrGetDirectiveForestHooks().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); + } + } + + removeHydrationHighlights() { + removeHydrationHighlights(); + } +} + +/** + * 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 []; + }); } diff --git a/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts b/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts index 8b65b523e42..e7ec71550d5 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts @@ -63,7 +63,7 @@ export function getInjectorId() { return `${injectorId++}`; } -function getInjectorMetadata(injector: Injector) { +export function getInjectorMetadata(injector: Injector) { return ngDebugClient().ɵgetInjectorMetadata(injector); } diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel index dbb1903aa8e..6092878f65b 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel @@ -15,6 +15,8 @@ ts_library( "//devtools/projects/ng-devtools-backend/src/lib:utils", "//devtools/projects/ng-devtools-backend/src/lib:version", "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", + "//devtools/projects/protocol", + "//packages/core", "@npm//semver-dsl", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/ltree.ts b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/ltree.ts index 06aed8a3aec..c91b8c60b1c 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/ltree.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/ltree.ts @@ -90,6 +90,7 @@ export class LTreeStrategy { element, directives: [], component: null, + hydration: null, // We know there is no hydration if we use the LTreeStrategy }; } for (let i = tNode.directiveStart; i < tNode.directiveEnd; i++) { @@ -114,6 +115,7 @@ export class LTreeStrategy { element, directives, component, + hydration: null, // We know there is no hydration if we use the LTreeStrategy }; } diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/render-tree.ts b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/render-tree.ts index 37608ea7b9d..754c2c321e7 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/render-tree.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/render-tree.ts @@ -6,6 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {ɵHydratedNode as HydrationNode} from '@angular/core'; +import {HydrationStatus} from 'protocol'; + import {ComponentTreeNode} from '../interfaces'; import {ngDebugClient} from '../ng-debug-api/ng-debug-api'; import {isCustomElement} from '../utils'; @@ -31,6 +34,7 @@ const extractViewTree = ( }), element: domNode.nodeName.toLowerCase(), nativeElement: domNode, + hydration: hydrationStatus(domNode as HydrationNode), }; if (!(domNode instanceof Element)) { result.push(componentTreeNode); @@ -59,6 +63,23 @@ const extractViewTree = ( return result; }; +function hydrationStatus(node: HydrationNode): HydrationStatus { + switch (node.__ngDebugHydrationInfo__?.status) { + case 'hydrated': + return {status: 'hydrated'}; + case 'skipped': + return {status: 'skipped'}; + case 'mismatched': + return { + status: 'mismatched', + expectedNodeDetails: node.__ngDebugHydrationInfo__.expectedNodeDetails, + actualNodeDetails: node.__ngDebugHydrationInfo__.actualNodeDetails, + }; + default: + return null; + } +} + export class RTreeStrategy { supports(): boolean { return (['getDirectiveMetadata', 'getComponent', 'getDirectives'] as const).every( diff --git a/devtools/projects/ng-devtools-backend/src/lib/highlighter.spec.ts b/devtools/projects/ng-devtools-backend/src/lib/highlighter.spec.ts index 09229266ba6..b9364ff8c74 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/highlighter.spec.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/highlighter.spec.ts @@ -99,4 +99,108 @@ describe('highlighter', () => { expect(highlighter.inDoc(node)).toBeTruthy(); }); }); + + describe('highlightHydrationElement', () => { + afterEach(() => { + document.body.innerHTML = ''; + delete (window as any).ng; + }); + + it('should show hydration overlay with svg', () => { + const appNode = document.createElement('app'); + appNode.style.width = '500px'; + appNode.style.height = '400px'; + appNode.style.display = 'block'; + document.body.appendChild(appNode); + (window as any).ng = { + getComponent: (el: any) => el, + }; + + highlighter.highlightHydrationElement(appNode, {status: 'hydrated'}); + + expect(document.body.querySelectorAll('div').length).toBe(2); + expect(document.body.querySelectorAll('svg').length).toBe(1); + + const overlay = document.body.querySelector('.ng-devtools-overlay'); + expect(overlay?.getBoundingClientRect().width).toBe(500); + expect(overlay?.getBoundingClientRect().height).toBe(400); + + highlighter.removeHydrationHighlights(); + expect(document.body.querySelectorAll('div').length).toBe(0); + expect(document.body.querySelectorAll('svg').length).toBe(0); + }); + + it('should show hydration overlay without svg (too small)', () => { + const appNode = document.createElement('app'); + appNode.style.width = '25px'; + appNode.style.height = '20px'; + appNode.style.display = 'block'; + document.body.appendChild(appNode); + (window as any).ng = { + getComponent: (el: any) => el, + }; + + highlighter.highlightHydrationElement(appNode, {status: 'hydrated'}); + + expect(document.body.querySelectorAll('div').length).toBe(1); + expect(document.body.querySelectorAll('svg').length).toBe(0); + + const overlay = document.body.querySelector('.ng-devtools-overlay'); + expect(overlay?.getBoundingClientRect().width).toBe(25); + expect(overlay?.getBoundingClientRect().height).toBe(20); + + highlighter.removeHydrationHighlights(); + expect(document.body.querySelectorAll('div').length).toBe(0); + expect(document.body.querySelectorAll('svg').length).toBe(0); + }); + + it('should show hydration overlay and selected component overlay at the same time ', () => { + const appNode = document.createElement('app'); + appNode.style.width = '25px'; + appNode.style.height = '20px'; + appNode.style.display = 'block'; + document.body.appendChild(appNode); + (window as any).ng = { + getComponent: (el: any) => el, + }; + + highlighter.highlightHydrationElement(appNode, {status: 'hydrated'}); + highlighter.highlightSelectedElement(appNode); + + expect(document.body.querySelectorAll('.ng-devtools-overlay').length).toBe(2); + highlighter.removeHydrationHighlights(); + expect(document.body.querySelectorAll('.ng-devtools-overlay').length).toBe(1); + highlighter.unHighlight(); + expect(document.body.querySelectorAll('.ng-devtools-overlay').length).toBe(0); + + highlighter.highlightHydrationElement(appNode, {status: 'hydrated'}); + highlighter.highlightSelectedElement(appNode); + highlighter.unHighlight(); + expect(document.body.querySelectorAll('.ng-devtools-overlay').length).toBe(1); + highlighter.removeHydrationHighlights(); + expect(document.body.querySelectorAll('.ng-devtools-overlay').length).toBe(0); + }); + }); + + describe('highlightSelectedElement', () => { + it('should show overlay', () => { + const appNode = document.createElement('app'); + appNode.style.width = '25px'; + appNode.style.height = '20px'; + appNode.style.display = 'block'; + document.body.appendChild(appNode); + (window as any).ng = { + getComponent: (el: any) => new (class FakeComponent {})(), + }; + + highlighter.highlightSelectedElement(appNode); + + const overlay = document.body.querySelectorAll('.ng-devtools-overlay'); + expect(overlay.length).toBe(1); + expect(overlay[0].innerHTML).toContain('FakeComponent'); + + highlighter.unHighlight(); + expect(document.body.querySelectorAll('.ng-devtools-overlay').length).toBe(0); + }); + }); }); diff --git a/devtools/projects/ng-devtools-backend/src/lib/highlighter.ts b/devtools/projects/ng-devtools-backend/src/lib/highlighter.ts index ae0f1be1816..381f0b1b523 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/highlighter.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/highlighter.ts @@ -6,31 +6,47 @@ * found in the LICENSE file at https://angular.io/license */ -let overlay: any; -let overlayContent: HTMLElement; +import type {ɵGlobalDevModeUtils as GlobalDevModeUtils, Type} from '@angular/core'; +import {HydrationStatus} from 'protocol'; -declare const ng: any; +let hydrationOverlayItems: HTMLElement[] = []; +let selectedElementOverlay: HTMLElement | null = null; -interface Type extends Function { - new (...args: any[]): T; -} +declare const ng: GlobalDevModeUtils['ng']; const DEV_TOOLS_HIGHLIGHT_NODE_ID = '____ngDevToolsHighlight'; -function init(): void { - if (overlay) { - return; - } - overlay = document.createElement('div'); - overlay.style.backgroundColor = 'rgba(104, 182, 255, 0.35)'; +const OVERLAY_CONTENT_MARGIN = 4; +const MINIMAL_OVERLAY_CONTENT_SIZE = { + width: 30 + OVERLAY_CONTENT_MARGIN * 2, + height: 20 + OVERLAY_CONTENT_MARGIN * 2, +}; + +type RgbColor = readonly [red: number, green: number, blue: number]; +const COLORS = { + blue: [104, 182, 255], + red: [255, 0, 64], + grey: [128, 128, 128], +} satisfies Record; + +// Those are the SVG we inline in case the overlay label is to long for the container component. +const HYDRATION_SVG = ` +`; +const HYDRATION_SKIPPED_SVG = ``; +const HYDRATION_ERROR_SVG = ``; + +function createOverlay(color: RgbColor): {overlay: HTMLElement; overlayContent: HTMLElement} { + const overlay = document.createElement('div'); + overlay.className = 'ng-devtools-overlay'; + overlay.style.backgroundColor = toCSSColor(...color, 0.35); overlay.style.position = 'fixed'; overlay.style.zIndex = '2147483647'; overlay.style.pointerEvents = 'none'; overlay.style.display = 'flex'; overlay.style.borderRadius = '3px'; overlay.id = DEV_TOOLS_HIGHLIGHT_NODE_ID; - overlayContent = document.createElement('div'); - overlayContent.style.backgroundColor = 'rgba(104, 182, 255, 0.9)'; + const overlayContent = document.createElement('div'); + overlayContent.style.backgroundColor = toCSSColor(...color, 0.9); overlayContent.style.position = 'absolute'; overlayContent.style.fontFamily = 'monospace'; overlayContent.style.fontSize = '11px'; @@ -38,11 +54,13 @@ function init(): void { overlayContent.style.borderRadius = '3px'; overlayContent.style.color = 'white'; overlay.appendChild(overlayContent); + return {overlay, overlayContent}; } -export const findComponentAndHost = ( - el: Node | undefined, -): {component: any; host: HTMLElement | null} => { +export function findComponentAndHost(el: Node | undefined): { + component: any; + host: HTMLElement | null; +} { if (!el) { return {component: null, host: null}; } @@ -57,41 +75,46 @@ export const findComponentAndHost = ( el = el.parentElement; } return {component: null, host: null}; -}; +} // Todo(aleksanderbodurri): this should not be part of the highlighter, move this somewhere else export function getDirectiveName(dir: Type | undefined | null): string { return dir ? dir.constructor.name : 'unknown'; } -export function highlight(el: HTMLElement): void { - const cmp = findComponentAndHost(el).component; - const rect = getComponentRect(el); +export function highlightSelectedElement(el: Node): void { + selectedElementOverlay = addHighlightForElement(el); +} - init(); - if (rect) { - const content: Node[] = []; - const name = getDirectiveName(cmp); - if (name) { - const pre = document.createElement('span'); - pre.style.opacity = '0.6'; - pre.innerText = '<'; - const text = document.createTextNode(name); - const post = document.createElement('span'); - post.style.opacity = '0.6'; - post.innerText = '>'; - content.push(pre, text, post); - } - showOverlay(rect, content); +export function highlightHydrationElement(el: Node, status: HydrationStatus) { + let overlay: HTMLElement | null = null; + if (status?.status === 'skipped') { + overlay = addHighlightForElement(el, COLORS.grey, status?.status); + } else if (status?.status === 'mismatched') { + overlay = addHighlightForElement(el, COLORS.red, status?.status); + } else if (status?.status === 'hydrated') { + overlay = addHighlightForElement(el, COLORS.blue, status?.status); + } + + if (overlay) { + hydrationOverlayItems.push(overlay); } } export function unHighlight(): void { - if (overlay && overlay.parentNode) { - document.body.removeChild(overlay); + if (selectedElementOverlay) { + document.body.removeChild(selectedElementOverlay); + selectedElementOverlay = null; } } +export function removeHydrationHighlights(): void { + hydrationOverlayItems.forEach((overlay) => { + document.body.removeChild(overlay); + }); + hydrationOverlayItems = []; +} + export function inDoc(node: any): boolean { if (!node) { return false; @@ -103,6 +126,46 @@ export function inDoc(node: any): boolean { ); } +function addHighlightForElement( + el: Node, + color: RgbColor = COLORS.blue, + overlayType?: NonNullable['status'], +): HTMLElement | null { + const cmp = findComponentAndHost(el).component; + const rect = getComponentRect(el); + if (rect?.height === 0 || rect?.width === 0) { + // display nothing in case the component is not visible + return null; + } + + const {overlay, overlayContent} = createOverlay(color); + if (!rect) return null; + + const content: Node[] = []; + const componentName = getDirectiveName(cmp); + + // We display an icon inside the overlay if the container computer is wide enough + if (overlayType) { + if ( + rect.width > MINIMAL_OVERLAY_CONTENT_SIZE.width && + rect.height > MINIMAL_OVERLAY_CONTENT_SIZE.height + ) { + // 30x20 + 8px margin + const svg = createOverlaySvgElement(overlayType!); + content.push(svg); + } + } else if (componentName) { + const middleText = document.createTextNode(componentName); + const pre = document.createElement('span'); + pre.innerText = `<`; + const post = document.createElement('span'); + post.innerText = `>`; + content.push(pre, middleText, post); + } + showOverlay(overlay, overlayContent, rect, content, overlayType ? 'inside' : 'outside'); + return overlay; +} + function getComponentRect(el: Node): DOMRect | undefined { if (!(el instanceof HTMLElement)) { return; @@ -113,27 +176,48 @@ function getComponentRect(el: Node): DOMRect | undefined { return el.getBoundingClientRect(); } -function showOverlay(dimensions: DOMRect, content: Node[]): void { +function showOverlay( + overlay: HTMLElement, + overlayContent: HTMLElement, + dimensions: DOMRect, + content: Node[], + labelPosition: 'inside' | 'outside', +): void { const {width, height, top, left} = dimensions; overlay.style.width = ~~width + 'px'; overlay.style.height = ~~height + 'px'; overlay.style.top = ~~top + 'px'; overlay.style.left = ~~left + 'px'; - positionOverlayContent(dimensions); + positionOverlayContent(overlayContent, dimensions, labelPosition); overlayContent.replaceChildren(); - content.forEach((child) => overlayContent.appendChild(child)); + if (content.length) { + content.forEach((child) => overlayContent.appendChild(child)); + } else { + // If the overlay label has no content, remove it from the DOM. + overlay.removeChild(overlayContent); + } document.body.appendChild(overlay); } -function positionOverlayContent(dimensions: DOMRect) { +function positionOverlayContent( + overlayContent: HTMLElement, + dimensions: DOMRect, + labelPosition: 'inside' | 'outside', +) { const {innerWidth: viewportWidth, innerHeight: viewportHeight} = window; const style = overlayContent.style; const yOffset = 23; const yOffsetValue = `-${yOffset}px`; + if (labelPosition === 'inside') { + style.top = `${OVERLAY_CONTENT_MARGIN}px`; + style.right = `${OVERLAY_CONTENT_MARGIN}px`; + return; + } + // Clear any previous positioning styles. style.top = style.bottom = style.left = style.right = ''; @@ -161,3 +245,25 @@ function positionOverlayContent(dimensions: DOMRect) { style.right = `${Math.max(dimensions.right - viewportWidth, 0)}px`; } } + +function toCSSColor(red: number, green: number, blue: number, alpha = 1): string { + return `rgba(${red},${green},${blue},${alpha})`; +} + +function createOverlaySvgElement(type: NonNullable['status']): Node { + let icon: string; + if (type === 'hydrated') { + icon = HYDRATION_SVG; + } else if (type === 'mismatched') { + icon = HYDRATION_ERROR_SVG; + } else if (type === 'skipped') { + icon = HYDRATION_SKIPPED_SVG; + } else { + throw new Error(`No icon specified for type ${type}`); + } + + const svg = new DOMParser().parseFromString(icon, 'image/svg+xml').childNodes[0] as SVGElement; + svg.style.fill = 'white'; + svg.style.height = '1.5em'; + return svg; +} diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/identity-tracker.ts b/devtools/projects/ng-devtools-backend/src/lib/hooks/identity-tracker.ts index 06a23ae2bed..c461ac4979a 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/identity-tracker.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/hooks/identity-tracker.ts @@ -131,6 +131,7 @@ const indexTree = ({position, ...d})), children: node.children.map((n, i) => indexTree(n, i, position)), nativeElement: node.nativeElement, + hydration: node.hydration, } as IndexedNode; }; diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/index.ts b/devtools/projects/ng-devtools-backend/src/lib/hooks/index.ts index 30b04dfbcb2..9073821da6c 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/index.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/hooks/index.ts @@ -50,11 +50,23 @@ export const disableTimingAPI = () => (timingAPIFlag = false); const timingAPIEnabled = () => timingAPIFlag; let directiveForestHooks: DirectiveForestHooks; -export const initializeOrGetDirectiveForestHooks = () => { + +export const initializeOrGetDirectiveForestHooks = ( + depsForTestOnly: { + directiveForestHooks?: typeof DirectiveForestHooks; + } = {}, +) => { + // Allow for overriding the DirectiveForestHooks implementation for testing purposes. + if (depsForTestOnly.directiveForestHooks) { + directiveForestHooks = new depsForTestOnly.directiveForestHooks(); + } + if (directiveForestHooks) { return directiveForestHooks; + } else { + directiveForestHooks = new DirectiveForestHooks(); } - directiveForestHooks = new DirectiveForestHooks(); + directiveForestHooks.profiler.subscribe({ onChangeDetectionStart(component: any): void { if (!timingAPIEnabled()) { diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.html index 5dc10d2e821..c3d7becfa26 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.html @@ -31,6 +31,7 @@
diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.ts index b889b36db4d..212f9762bd3 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.ts @@ -50,6 +50,8 @@ type Tabs = 'Components' | 'Profiler' | 'Router Tree' | 'Injector Tree'; }) export class DevToolsTabsComponent implements OnInit, AfterViewInit { @Input() angularVersion: string | undefined = undefined; + @Input() isHydrationEnabled = false; + @ViewChild(DirectiveExplorerComponent) directiveExplorer!: DirectiveExplorerComponent; @ViewChild('navBar', {static: true}) navbar!: MatTabNav; diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/BUILD.bazel b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/BUILD.bazel index 46920a0b902..68d22f35ca6 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/BUILD.bazel +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/BUILD.bazel @@ -43,10 +43,14 @@ ts_test_library( srcs = ["directive-explorer.spec.ts"], deps = [ ":directive-explorer", + "//devtools/projects/ng-devtools/src/lib/application-operations", "//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest", "//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest", "//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver", "//devtools/projects/protocol", + "//packages/core", + "//packages/core/testing", + "//packages/platform-browser", ], ) diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.html index de55cef3fe0..da5a29ac089 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.html @@ -21,12 +21,12 @@ (mouseOverNode)="highlight($event)" (handleSelect)="handleSelect($event)" [parents]="parents" - /> + /> } - +
+ @if(isHydrationEnabled) { +
+ Show hydration overlays +
+ }
diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.scss b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.scss index 5e8c4a13eb6..a8900f8a3a9 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.scss +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.scss @@ -12,8 +12,17 @@ } } +.prop-split { + display: flex; + flex-direction: column; +} + .property-tab-wrapper { + flex: 1; overflow: auto; - height: 100%; width: 100%; } + +.hydration { + padding: 4px 8px; +} \ No newline at end of file diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.ts index 97d9b1d6f0c..26b641da38d 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.component.ts @@ -44,6 +44,8 @@ import { } from './property-resolver/element-property-resolver'; import {PropertyTabComponent} from './property-tab/property-tab.component'; import {SplitAreaDirective} from '../../vendor/angular-split/lib/component/splitArea.directive'; +import {MatSlideToggle} from '@angular/material/slide-toggle'; +import {FormsModule} from '@angular/forms'; const sameDirectives = (a: IndexedNode, b: IndexedNode) => { if ((a.component && !b.component) || (!a.component && b.component)) { @@ -78,10 +80,13 @@ const sameDirectives = (a: IndexedNode, b: IndexedNode) => { DirectiveForestComponent, BreadcrumbsComponent, PropertyTabComponent, + MatSlideToggle, + FormsModule, ], }) export class DirectiveExplorerComponent implements OnInit, OnDestroy { @Input() showCommentNodes = false; + @Input() isHydrationEnabled = false; @Output() toggleInspector = new EventEmitter(); @ViewChild(DirectiveForestComponent) directiveForest!: DirectiveForestComponent; @@ -94,11 +99,13 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy { forest!: DevToolsNode[]; splitDirection: 'horizontal' | 'vertical' = 'horizontal'; parents: FlatNode[] | null = null; + showHydrationNodeHighlights: boolean = false; private _resizeObserver = new ResizeObserver((entries) => this._ngZone.run(() => { - const resizedEntry = entries[0]; + this.refreshHydrationNodeHighlightsIfNeeded(); + const resizedEntry = entries[0]; if (resizedEntry.target === this.splitElementRef.nativeElement) { this.splitDirection = resizedEntry.contentRect.width <= 500 ? 'vertical' : 'horizontal'; } @@ -177,6 +184,7 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy { if (!this._refreshRetryTimeout) { this._refreshRetryTimeout = setTimeout(() => this.refresh(), 500); } + this.refreshHydrationNodeHighlightsIfNeeded(); } viewSource(directiveName: string): void { @@ -272,4 +280,19 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy { const objectPath = constructPathOfKeysToPropertyValue(node.prop); this._appOperations.inspect(directivePosition, objectPath); } + + hightlightHydrationNodes() { + this._messageBus.emit('createHydrationOverlay'); + } + + removeHydrationNodesHightlights() { + this._messageBus.emit('removeHydrationOverlay'); + } + + refreshHydrationNodeHighlightsIfNeeded() { + if (this.showHydrationNodeHighlights) { + this.removeHydrationNodesHightlights(); + this.hightlightHydrationNodes(); + } + } } diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.spec.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.spec.ts index 5483851f8da..b6b9e0514e3 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.spec.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-explorer.spec.ts @@ -6,36 +6,54 @@ * found in the LICENSE file at https://angular.io/license */ -import {PropertyQueryTypes} from 'protocol'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Events, MessageBus, PropertyQueryTypes} from 'protocol'; + +import {ApplicationOperations} from '../../application-operations'; import {DirectiveExplorerComponent} from './directive-explorer.component'; +import {DirectiveForestComponent} from './directive-forest/directive-forest.component'; import {IndexedNode} from './directive-forest/index-forest'; import {ElementPropertyResolver} from './property-resolver/element-property-resolver'; import SpyObj = jasmine.SpyObj; +import {By} from '@angular/platform-browser'; describe('DirectiveExplorerComponent', () => { - let messageBusMock: any; + let messageBusMock: SpyObj>; + let fixture: ComponentFixture; let comp: DirectiveExplorerComponent; - let applicationOperationsSpy: any; - let cdr: any; - let ngZone: any; + let applicationOperationsSpy: SpyObj; beforeEach(() => { - applicationOperationsSpy = jasmine.createSpyObj('_appOperations', [ + applicationOperationsSpy = jasmine.createSpyObj('_appOperations', [ 'viewSource', 'selectDomElement', ]); - messageBusMock = jasmine.createSpyObj('messageBus', ['on', 'once', 'emit', 'destroy']); - cdr = jasmine.createSpyObj('_cdr', ['detectChanges']); - ngZone = jasmine.createSpyObj('_ngZone', ['run']); - comp = new DirectiveExplorerComponent( - applicationOperationsSpy, - messageBusMock, - new ElementPropertyResolver(messageBusMock), - cdr, - ngZone, - ); + messageBusMock = jasmine.createSpyObj>('messageBus', [ + 'on', + 'once', + 'emit', + 'destroy', + ]); + + TestBed.configureTestingModule({ + providers: [ + {provide: ApplicationOperations, useValue: applicationOperationsSpy}, + {provide: MessageBus, useValue: messageBusMock}, + { + provide: ElementPropertyResolver, + useValue: new ElementPropertyResolver(messageBusMock), + }, + ], + }).overrideComponent(DirectiveExplorerComponent, { + add: {schemas: [CUSTOM_ELEMENTS_SCHEMA]}, + remove: {imports: [DirectiveForestComponent]}, + }); + + fixture = TestBed.createComponent(DirectiveExplorerComponent); + comp = fixture.componentInstance; }); it('should create instance from class', () => { @@ -104,4 +122,28 @@ describe('DirectiveExplorerComponent', () => { ]); }); }); + + describe('hydration', () => { + it('should highlight hydration nodes', () => { + comp.hightlightHydrationNodes(); + expect(messageBusMock.emit).toHaveBeenCalledTimes(1); + expect(messageBusMock.emit).toHaveBeenCalledWith('createHydrationOverlay'); + + comp.removeHydrationNodesHightlights(); + expect(messageBusMock.emit).toHaveBeenCalledTimes(2); + expect(messageBusMock.emit).toHaveBeenCalledWith('removeHydrationOverlay'); + }); + + it('should show hydration slide toggle', () => { + comp.isHydrationEnabled = true; + fixture.detectChanges(); + const toggle = fixture.debugElement.query(By.css('mat-slide-toggle')); + expect(toggle).toBeTruthy(); + + comp.isHydrationEnabled = false; + fixture.detectChanges(); + const toggle2 = fixture.debugElement.query(By.css('mat-slide-toggle')); + expect(toggle2).toBeFalsy(); + }); + }); }); diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/component-data-source/component-data-source.spec.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/component-data-source/component-data-source.spec.ts index 1fe718e6628..8602acd8c91 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/component-data-source/component-data-source.spec.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/component-data-source/component-data-source.spec.ts @@ -20,6 +20,7 @@ const tree1: DevToolsNode = { }, ], component: null, + hydration: null, children: [ { children: [], @@ -30,6 +31,7 @@ const tree1: DevToolsNode = { }, directives: [], element: 'bar', + hydration: null, nativeElement: document.createElement('bar'), }, ], @@ -45,6 +47,7 @@ const tree2: DevToolsNode = { }, ], component: null, + hydration: null, children: [ { children: [], @@ -55,6 +58,7 @@ const tree2: DevToolsNode = { }, directives: [], element: 'bar', + hydration: null, nativeElement: document.createElement('bar'), }, { @@ -66,6 +70,7 @@ const tree2: DevToolsNode = { }, directives: [], element: 'qux', + hydration: null, nativeElement: document.createElement('qux'), }, ], @@ -81,6 +86,7 @@ const tree3: DevToolsNode = { }, ], component: null, + hydration: null, children: [ { children: [], @@ -91,6 +97,7 @@ const tree3: DevToolsNode = { }, directives: [], element: '#comment', + hydration: null, nativeElement: document.createComment('bar'), }, { @@ -102,6 +109,7 @@ const tree3: DevToolsNode = { }, directives: [], element: '#comment', + hydration: null, nativeElement: document.createComment('bar'), }, ], @@ -110,6 +118,7 @@ const tree3: DevToolsNode = { const tree4: DevToolsNode = { element: 'app', + hydration: null, directives: [ { id: 1, @@ -135,6 +144,7 @@ const tree4: DevToolsNode = { }, directives: [], element: 'bar', + hydration: null, nativeElement: document.createComment('bar'), }, ], @@ -145,6 +155,7 @@ const tree4: DevToolsNode = { }, directives: [], element: '#comment', + hydration: null, nativeElement: document.createComment('bar'), }, ], @@ -155,6 +166,7 @@ const tree4: DevToolsNode = { }, directives: [], element: '#comment', + hydration: null, nativeElement: document.createComment('bar'), }, ], @@ -165,6 +177,7 @@ const tree4: DevToolsNode = { }, directives: [], element: '#comment', + hydration: null, nativeElement: document.createComment('bar'), }, ], @@ -175,6 +188,7 @@ const tree4: DevToolsNode = { }, directives: [], element: '#comment', + hydration: null, nativeElement: document.createComment('bar'), }, ], diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/component-data-source/index.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/component-data-source/index.ts index 37e24d8a75a..ad4fb74fe24 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/component-data-source/index.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/component-data-source/index.ts @@ -10,7 +10,7 @@ import {CollectionViewer, DataSource} from '@angular/cdk/collections'; import {FlatTreeControl} from '@angular/cdk/tree'; import {DefaultIterableDiffer, TrackByFunction} from '@angular/core'; import {MatTreeFlattener} from '@angular/material/tree'; -import {DevToolsNode} from 'protocol'; +import {DevToolsNode, HydrationStatus} from 'protocol'; import {BehaviorSubject, merge, Observable} from 'rxjs'; import {map} from 'rxjs/operators'; @@ -27,6 +27,7 @@ export interface FlatNode { level: number; original: IndexedNode; newItem?: boolean; + hydration: HydrationStatus; } const expandable = (node: IndexedNode) => !!node.children && node.children.length > 0; @@ -92,6 +93,7 @@ export class ComponentDataSource extends DataSource { directives: node.directives.map((d) => d.name).join(', '), original: node, level, + hydration: node.hydration, }; this._nodeToFlat.set(node, flatNode); return flatNode; diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest.component.html index fb833ba8eb9..9c390af1084 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest.component.html @@ -3,16 +3,16 @@ (filter)="handleFilter($event)" (nextMatched)="nextMatched()" (prevMatched)="prevMatched()" - > +>
+ > +
@if (node.expandable) { } {{ node.name }} + @if (node.directives) { [{{ node.directives }}] } @if (isSelected(node)) { == $ng0 } + + @switch(node.hydration?.status) { + @case('hydrated') { + water_drop + } + @case('skipped') { + invert_colors_off + } + @case('mismatched') { + error_outline + } + }
+ @if(node.hydration?.status === 'mismatched' && (node.hydration.expectedNodeDetails || node.hydration.actualNodeDetails)) { +
+ @if(node.hydration.expectedNodeDetails) { +
Expected Dom:
+
{{node.hydration.expectedNodeDetails}}
+ } + @if(node.hydration.actualNodeDetails) { +
Actual Dom:
+
{{node.hydration.actualNodeDetails}}
+ } +
+ } +
+
+
diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest.component.scss b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest.component.scss index 973e60183c3..15306a13018 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest.component.scss +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest.component.scss @@ -15,13 +15,17 @@ white-space: nowrap; text-overflow: ellipsis; - & > button { - outline: none; - border: 0; - padding: 0; - position: absolute; - background-color: transparent; - top: 2px; + .tree-node-info { + display: flex; + + & > button { + outline: none; + border: 0; + padding: 0; + position: absolute; + background-color: transparent; + top: 2px; + } } .mat-icon { @@ -57,6 +61,23 @@ &.highlighted { background-color: #cfe8fc; } + + .hydration { + margin: auto 8px auto auto; + font-size: 14px; + color: #60a6fc; + + position: sticky; + right: 0; + + &.skipped { + color: #9a9a9a; + } + + &.mismatched { + color: #ff0040; + } + } } } @@ -122,6 +143,23 @@ } } +.hydration-error { + color: #e62222; + margin-left: 16px; + padding: 0 8px 8px 0px; + + pre { + margin: 0; + background: rgba(1, 1, 1, 0.05); + padding: 8px; + border-radius: 8px; + + &:not(:last-child) { + margin-bottom: 16px; + } + } +} + :host-context(.dark-theme) { .tree-node { color: #5cadd3; @@ -154,5 +192,24 @@ &.highlighted { background-color: #073d69; } + + .hydration { + color: #60a6fc; + + &.skipped { + color: #9a9a9a;; + } + + &.mismatched { + color: #ff0040; + } + } + + .hydration-error { + color: #ea7171; + pre { + background: rgb(1, 1, 1, 0.2); + } + } } } diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest/index-forest.spec.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest/index-forest.spec.ts index 5d08c22191c..7bc7476d7e3 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest/index-forest.spec.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest/index-forest.spec.ts @@ -19,6 +19,7 @@ describe('indexForest', () => { { element: 'Parent1', directives: [], + hydration: null, component: { isElement: false, name: 'Cmp1', @@ -27,6 +28,7 @@ describe('indexForest', () => { children: [ { element: 'Child1_1', + hydration: null, directives: [ { name: 'Dir1', @@ -43,6 +45,7 @@ describe('indexForest', () => { { element: 'Child1_2', directives: [], + hydration: null, component: { isElement: false, name: 'Cmp2', @@ -56,6 +59,7 @@ describe('indexForest', () => { element: 'Parent2', directives: [], component: null, + hydration: null, children: [ { element: 'Child2_1', @@ -65,6 +69,7 @@ describe('indexForest', () => { id: 1, }, ], + hydration: null, component: null, children: [], }, @@ -81,6 +86,7 @@ describe('indexForest', () => { }, ], component: null, + hydration: null, children: [], }, ], @@ -91,6 +97,7 @@ describe('indexForest', () => { element: 'Parent1', directives: [], position: [0], + hydration: null, component: { isElement: false, name: 'Cmp1', @@ -111,6 +118,7 @@ describe('indexForest', () => { }, ], component: null, + hydration: null, children: [], }, { @@ -122,6 +130,7 @@ describe('indexForest', () => { name: 'Cmp2', id: 1, }, + hydration: null, children: [], }, ], @@ -131,6 +140,7 @@ describe('indexForest', () => { directives: [], component: null, position: [1], + hydration: null, children: [ { element: 'Child2_1', @@ -142,6 +152,7 @@ describe('indexForest', () => { }, ], component: null, + hydration: null, children: [], }, { @@ -159,6 +170,7 @@ describe('indexForest', () => { ], component: null, children: [], + hydration: null, }, ], }, diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest/index.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest/index.ts index 1469e38ca92..d3b99d8f36f 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest/index.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest/index.ts @@ -25,7 +25,8 @@ const indexTree = ( component: node.component, directives: node.directives.map((d, i) => ({name: d.name, id: d.id})), children: node.children.map((n, i) => indexTree(n, i, position)), - } as IndexedNode; + hydration: node.hydration, + }; }; export const indexForest = (forest: DevToolsNode[]) => forest.map((n, i) => indexTree(n, i)); diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver/BUILD.bazel b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver/BUILD.bazel index 1ac56ef95de..461b99a5c82 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver/BUILD.bazel +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver/BUILD.bazel @@ -36,6 +36,7 @@ ts_test_library( ], deps = [ ":property-resolver", + "//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest", "//devtools/projects/protocol", "@npm//@angular/cdk", "@npm//@angular/material", diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver/element-property-resolver.spec.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver/element-property-resolver.spec.ts index 9dbb0152e39..ebb445ec770 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver/element-property-resolver.spec.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver/element-property-resolver.spec.ts @@ -8,14 +8,17 @@ import {Properties, PropType} from 'protocol'; +import {IndexedNode} from '../directive-forest/index-forest'; + import {ElementPropertyResolver} from './element-property-resolver'; -const mockIndexedNode = { +const mockIndexedNode: IndexedNode = { component: { name: 'FooCmp', id: 0, isElement: false, }, + hydration: null, directives: [ { id: 1, diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/injector-tree/BUILD.bazel b/devtools/projects/ng-devtools/src/lib/devtools-tabs/injector-tree/BUILD.bazel index 234defb137c..bfc097a3e50 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/injector-tree/BUILD.bazel +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/injector-tree/BUILD.bazel @@ -65,6 +65,7 @@ ts_test_library( deps = [ ":injector_tree_fns", "//devtools/projects/ng-devtools/src/lib/devtools-tabs/dependency-injection:injector_tree_visualizer", + "//devtools/projects/protocol", ], ) diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/injector-tree/injector-tree-fns.spec.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/injector-tree/injector-tree-fns.spec.ts index cac3a7a1755..e88fabed380 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/injector-tree/injector-tree-fns.spec.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/injector-tree/injector-tree-fns.spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {ComponentType, DevToolsNode} from 'protocol'; import { InjectorTreeD3Node, InjectorTreeNode, @@ -109,6 +110,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-root', 'component': {'name': 'app-root', 'isElement': false, 'id': 0}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'router-outlet', @@ -122,10 +124,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'app-demo-component', 'component': {'name': 'app-demo-component', 'isElement': false, 'id': 2}, + 'hydration': null, 'directives': [], 'children': [ { @@ -143,11 +147,13 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'app-todo-demo', 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'a', @@ -167,6 +173,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'a', @@ -186,6 +193,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'router-outlet', @@ -205,10 +213,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'app-todos', 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, + 'hydration': null, 'directives': [], 'children': [ { @@ -232,6 +242,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'a', @@ -254,6 +265,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'a', @@ -276,6 +288,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'app-todo', @@ -305,6 +318,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, ], 'resolutionPath': [ @@ -324,11 +338,13 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], + 'hydration': null, 'children': [ { 'element': 'div', @@ -353,6 +369,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, ], 'resolutionPath': [ @@ -394,6 +411,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, ], 'resolutionPath': [ @@ -441,6 +459,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, ], 'resolutionPath': [ @@ -467,6 +486,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 1}], 'children': [], 'resolutionPath': [ @@ -483,6 +503,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': 'app-demo-component', 'component': {'name': 'app-demo-component', 'isElement': false, 'id': 2}, + 'hydration': null, 'directives': [], 'children': [ { @@ -500,10 +521,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'app-todo-demo', 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, + 'hydration': null, 'directives': [], 'children': [ { @@ -524,6 +547,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'a', @@ -543,6 +567,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'router-outlet', @@ -562,10 +587,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'app-todos', 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, + 'hydration': null, 'directives': [], 'children': [ { @@ -589,6 +616,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'a', @@ -611,6 +639,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'a', @@ -633,11 +662,13 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], + 'hydration': null, 'children': [ { 'element': 'div', @@ -662,6 +693,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, ], 'resolutionPath': [ @@ -686,6 +718,8 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], + 'hydration': null, + 'children': [ { 'element': 'div', @@ -710,6 +744,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, ], 'resolutionPath': [ @@ -751,6 +786,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, ], 'resolutionPath': [ @@ -798,6 +834,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, ], 'resolutionPath': [ @@ -820,6 +857,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'router-outlet', 'component': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], + 'hydration': null, 'children': [], 'resolutionPath': [ {'id': '5', 'type': 'element', 'name': '_RouterOutlet'}, @@ -841,6 +879,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': 'app-todo-demo', 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, + 'hydration': null, 'directives': [], 'children': [ { @@ -861,6 +900,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'a', @@ -880,6 +920,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'router-outlet', @@ -899,11 +940,13 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'app-todos', 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'a', @@ -926,6 +969,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'a', @@ -948,6 +992,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'a', @@ -970,11 +1015,13 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], + 'hydration': null, 'children': [ { 'element': 'div', @@ -999,6 +1046,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, ], 'resolutionPath': [ @@ -1023,6 +1071,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], + 'hydration': null, 'children': [ { 'element': 'div', @@ -1047,6 +1096,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, ], 'resolutionPath': [ @@ -1088,6 +1138,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, ], 'resolutionPath': [ @@ -1130,6 +1181,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -1171,6 +1223,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { {'id': '3', 'type': 'environment', 'name': 'Platform: core'}, {'id': '4', 'type': 'null', 'name': 'Null Injector'}, ], + 'hydration': null, }, 'path': [ {'id': '1', 'type': 'element', 'name': '_AppComponent'}, @@ -1182,6 +1235,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -1209,10 +1263,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todos', 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -1235,6 +1291,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -1257,6 +1314,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -1280,11 +1338,13 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], + 'hydration': null, 'children': [], 'resolutionPath': [ {'id': '18', 'type': 'element', 'name': '_TooltipDirective'}, @@ -1328,11 +1388,13 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], + 'hydration': null, 'children': [], 'resolutionPath': [ {'id': '21', 'type': 'element', 'name': '_TooltipDirective'}, @@ -1376,6 +1438,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': '#comment', 'component': null, 'directives': [{'name': '_NgForOf', 'id': 18}], + 'hydration': null, 'children': [], 'resolutionPath': [ {'id': '20', 'type': 'element', 'name': '_NgForOf'}, @@ -1422,6 +1485,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -1452,6 +1516,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -1482,6 +1547,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -1513,11 +1579,13 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], + 'hydration': null, 'children': [], 'resolutionPath': [ {'id': '18', 'type': 'element', 'name': '_TooltipDirective'}, @@ -1569,6 +1637,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -1604,12 +1673,14 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '21', 'type': 'element', 'name': '_TooltipDirective'}, {'id': '22', 'type': 'element', 'name': '_TodoComponent'}, @@ -1660,6 +1731,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -1694,6 +1766,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -1723,6 +1796,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { { 'node': { 'element': 'app-heavy', + 'hydration': null, 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], @@ -1755,12 +1829,14 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'name': '_AppComponent', 'node': { 'element': 'app-root', + 'hydration': null, 'component': {'name': 'app-root', 'isElement': false, 'id': 0}, 'directives': [], 'children': [ { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 1}], 'children': [], 'resolutionPath': [ @@ -1775,10 +1851,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-demo-component', 'component': {'name': 'app-demo-component', 'isElement': false, 'id': 2}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], 'resolutionPath': [ @@ -1796,10 +1874,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo-demo', 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -1819,6 +1899,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -1838,6 +1919,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -1858,10 +1940,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todos', 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -1884,6 +1968,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -1906,6 +1991,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -1929,10 +2015,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -1977,10 +2065,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -2025,6 +2115,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': '#comment', 'component': null, 'directives': [{'name': '_NgForOf', 'id': 18}], + 'hydration': null, 'children': [], 'resolutionPath': [ {'id': '20', 'type': 'element', 'name': '_NgForOf'}, @@ -2079,6 +2170,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '24', 'type': 'element', 'name': '_HeavyComponent'}, {'id': '6', 'type': 'element', 'name': '_DemoAppComponent'}, @@ -2120,12 +2212,14 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-demo-component', 'component': {'name': 'app-demo-component', 'isElement': false, 'id': 2}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'router-outlet', 'component': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '5', 'type': 'element', 'name': '_RouterOutlet'}, {'id': '6', 'type': 'element', 'name': '_DemoAppComponent'}, @@ -2141,10 +2235,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo-demo', 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -2166,6 +2262,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'component': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '11', 'type': 'element', 'name': '_RouterLink'}, {'id': '9', 'type': 'element', 'name': '_AppTodoComponent'}, @@ -2185,6 +2282,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'component': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '12', 'type': 'element', 'name': '_RouterOutlet'}, {'id': '9', 'type': 'element', 'name': '_AppTodoComponent'}, @@ -2203,12 +2301,14 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todos', 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'a', 'component': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '13', 'type': 'element', 'name': '_RouterLink'}, {'id': '14', 'type': 'element', 'name': '_TodosComponent'}, @@ -2231,6 +2331,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'component': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '16', 'type': 'element', 'name': '_RouterLink'}, {'id': '14', 'type': 'element', 'name': '_TodosComponent'}, @@ -2253,6 +2354,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'component': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '17', 'type': 'element', 'name': '_RouterLink'}, {'id': '14', 'type': 'element', 'name': '_TodosComponent'}, @@ -2274,12 +2376,14 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '18', 'type': 'element', 'name': '_TooltipDirective'}, {'id': '19', 'type': 'element', 'name': '_TodoComponent'}, @@ -2322,10 +2426,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -2371,6 +2477,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'component': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '20', 'type': 'element', 'name': '_NgForOf'}, {'id': '14', 'type': 'element', 'name': '_TodosComponent'}, @@ -2422,6 +2529,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { { 'element': 'app-heavy', 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, + 'hydration': null, 'directives': [], 'children': [], 'resolutionPath': [ @@ -2456,6 +2564,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': 'app-todo-demo', 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, + 'hydration': null, 'directives': [], 'children': [ { @@ -2463,6 +2572,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'component': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '8', 'type': 'element', 'name': '_RouterLink'}, {'id': '9', 'type': 'element', 'name': '_AppTodoComponent'}, @@ -2482,6 +2592,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'component': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '11', 'type': 'element', 'name': '_RouterLink'}, {'id': '9', 'type': 'element', 'name': '_AppTodoComponent'}, @@ -2499,6 +2610,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -2519,10 +2631,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todos', 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -2545,6 +2659,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -2568,6 +2683,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'a', 'component': null, 'directives': [{'name': '_RouterLink', 'id': 11}], + 'hydration': null, 'children': [], 'resolutionPath': [ {'id': '17', 'type': 'element', 'name': '_RouterLink'}, @@ -2590,10 +2706,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -2638,10 +2756,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -2686,6 +2806,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': '#comment', 'component': null, 'directives': [{'name': '_NgForOf', 'id': 18}], + 'hydration': null, 'children': [], 'resolutionPath': [ {'id': '20', 'type': 'element', 'name': '_NgForOf'}, @@ -2744,12 +2865,14 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'name': '_TodosComponent', 'node': { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -2772,6 +2895,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -2794,6 +2918,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -2817,10 +2942,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -2865,10 +2992,12 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -2912,6 +3041,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -2957,12 +3087,14 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'name': '_TodoComponent', 'node': { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -3013,6 +3145,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -3046,12 +3179,14 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'name': '_TodoComponent', 'node': { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -3102,6 +3237,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'node': { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -3139,6 +3275,7 @@ describe('transformInjectorResolutionPathsIntoTree', () => { 'name': '_HeavyComponent', 'node': { 'element': 'app-heavy', + 'hydration': null, 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], @@ -3169,10 +3306,11 @@ describe('transformInjectorResolutionPathsIntoTree', () => { describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { it("should be able to split injector paths into element and environment paths, and expose the mapping from each element to it's environment path", () => { - const injectorPaths = [ + const injectorPaths: InjectorPath[] = [ { 'node': { 'element': 'app-root', + 'hydration': null, 'component': {'name': 'app-root', 'isElement': false, 'id': 0}, 'directives': [], 'children': [ @@ -3180,6 +3318,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'element': 'router-outlet', 'component': null, 'directives': [{'name': '_RouterOutlet', 'id': 1}], + 'hydration': null, 'children': [], 'resolutionPath': [ {'id': '0', 'type': 'element', 'name': '_RouterOutlet'}, @@ -3191,6 +3330,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-demo-component', + 'hydration': null, 'component': {'name': 'app-demo-component', 'isElement': false, 'id': 2}, 'directives': [], 'children': [ @@ -3199,6 +3339,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'component': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '5', 'type': 'element', 'name': '_RouterOutlet'}, {'id': '6', 'type': 'element', 'name': '_DemoAppComponent'}, @@ -3212,12 +3353,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo-demo', + 'hydration': null, 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -3237,6 +3380,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -3256,6 +3400,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -3274,12 +3419,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -3302,6 +3449,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -3324,6 +3472,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -3345,12 +3494,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -3393,12 +3544,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -3442,6 +3595,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -3494,6 +3648,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-zippy', + 'hydration': null, 'component': {'name': 'app-zippy', 'isElement': true, 'id': 19}, 'directives': [], 'children': [], @@ -3507,6 +3662,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-heavy', + 'hydration': null, 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], @@ -3549,6 +3705,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 1}], 'children': [], 'resolutionPath': [ @@ -3567,12 +3724,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-demo-component', + 'hydration': null, 'component': {'name': 'app-demo-component', 'isElement': false, 'id': 2}, 'directives': [], 'children': [ { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], 'resolutionPath': [ @@ -3588,12 +3747,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo-demo', + 'hydration': null, 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -3613,6 +3774,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -3632,6 +3794,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -3650,12 +3813,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -3678,6 +3843,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -3700,6 +3866,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -3721,12 +3888,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -3769,12 +3938,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -3818,6 +3989,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -3870,6 +4042,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-zippy', + 'hydration': null, 'component': {'name': 'app-zippy', 'isElement': true, 'id': 19}, 'directives': [], 'children': [], @@ -3883,6 +4056,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-heavy', + 'hydration': null, 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], @@ -3919,6 +4093,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], 'resolutionPath': [ @@ -3942,12 +4117,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-todo-demo', + 'hydration': null, 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -3967,6 +4144,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -3986,6 +4164,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -4006,10 +4185,12 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'element': 'app-todos', 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -4032,6 +4213,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -4054,6 +4236,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -4075,12 +4258,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -4123,12 +4308,15 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, + 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -4172,6 +4360,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -4235,6 +4424,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -4264,6 +4454,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -4293,6 +4484,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -4321,12 +4513,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -4349,6 +4543,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -4371,6 +4566,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -4392,12 +4588,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -4440,12 +4638,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -4489,6 +4689,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -4540,6 +4741,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -4574,6 +4776,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -4608,6 +4811,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -4641,12 +4845,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -4703,6 +4909,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -4740,12 +4947,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -4802,6 +5011,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -4840,6 +5050,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -4873,6 +5084,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-zippy', + 'hydration': null, 'component': {'name': 'app-zippy', 'isElement': true, 'id': 19}, 'directives': [], 'children': [], @@ -4893,6 +5105,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-heavy', + 'hydration': null, 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], @@ -4917,16 +5130,18 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, ]; - const expectedElementPaths = [ + const expectedElementPaths: InjectorPath[] = [ { 'node': { 'element': 'app-root', + 'hydration': null, 'component': {'name': 'app-root', 'isElement': false, 'id': 0}, 'directives': [], 'children': [ { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 1}], 'children': [], 'resolutionPath': [ @@ -4939,12 +5154,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-demo-component', + 'hydration': null, 'component': {'name': 'app-demo-component', 'isElement': false, 'id': 2}, 'directives': [], 'children': [ { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], 'resolutionPath': [ @@ -4960,12 +5177,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo-demo', + 'hydration': null, 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -4985,6 +5204,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -5004,6 +5224,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -5022,12 +5243,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -5050,6 +5273,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -5072,6 +5296,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -5093,12 +5318,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -5141,12 +5368,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -5190,6 +5419,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -5242,6 +5472,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-zippy', + 'hydration': null, 'component': {'name': 'app-zippy', 'isElement': true, 'id': 19}, 'directives': [], 'children': [], @@ -5255,6 +5486,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-heavy', + 'hydration': null, 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], @@ -5294,6 +5526,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 1}], 'children': [], 'resolutionPath': [ @@ -5311,10 +5544,12 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'element': 'app-demo-component', 'component': {'name': 'app-demo-component', 'isElement': false, 'id': 2}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], 'resolutionPath': [ @@ -5331,11 +5566,13 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'app-todo-demo', 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, + 'hydration': null, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -5355,6 +5592,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -5374,6 +5612,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -5393,11 +5632,13 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'app-todos', 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, + 'hydration': null, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -5420,6 +5661,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -5442,6 +5684,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -5463,12 +5706,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -5511,12 +5756,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -5560,6 +5807,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -5615,6 +5863,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'component': {'name': 'app-zippy', 'isElement': true, 'id': 19}, 'directives': [], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '23', 'type': 'element', 'name': '_ZippyComponent'}, {'id': '7', 'type': 'environment', 'name': '_DemoAppModule'}, @@ -5628,6 +5877,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], + 'hydration': null, 'resolutionPath': [ {'id': '24', 'type': 'element', 'name': '_HeavyComponent'}, {'id': '6', 'type': 'element', 'name': '_DemoAppComponent'}, @@ -5658,6 +5908,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'router-outlet', + 'hydration': null, 'component': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], @@ -5680,12 +5931,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-todo-demo', + 'hydration': null, 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -5705,6 +5958,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -5724,6 +5978,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -5742,12 +5997,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -5770,6 +6027,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -5792,6 +6050,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -5813,12 +6072,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -5861,12 +6122,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -5910,6 +6173,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -5969,6 +6233,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'a', + 'hydration': null, 'component': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], @@ -5996,6 +6261,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -6022,6 +6288,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -6047,12 +6314,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -6075,6 +6344,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -6097,6 +6367,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -6120,10 +6391,12 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -6168,11 +6441,13 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], + 'hydration': null, 'children': [], 'resolutionPath': [ {'id': '21', 'type': 'element', 'name': '_TooltipDirective'}, @@ -6216,6 +6491,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'element': '#comment', 'component': null, 'directives': [{'name': '_NgForOf', 'id': 18}], + 'hydration': null, 'children': [], 'resolutionPath': [ {'id': '20', 'type': 'element', 'name': '_NgForOf'}, @@ -6262,6 +6538,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -6292,6 +6569,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -6322,6 +6600,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -6351,12 +6630,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -6409,6 +6690,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -6444,10 +6726,12 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -6500,6 +6784,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -6534,6 +6819,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -6563,6 +6849,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-zippy', + 'hydration': null, 'component': {'name': 'app-zippy', 'isElement': true, 'id': 19}, 'directives': [], 'children': [], @@ -6579,6 +6866,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-heavy', + 'hydration': null, 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], @@ -6601,16 +6889,18 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, ]; - const expectedEnvironmentPaths = [ + const expectedEnvironmentPaths: InjectorPath[] = [ { 'node': { 'element': 'app-root', + 'hydration': null, 'component': {'name': 'app-root', 'isElement': false, 'id': 0}, 'directives': [], 'children': [ { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 1}], 'children': [], 'resolutionPath': [ @@ -6624,12 +6914,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'app-demo-component', 'component': {'name': 'app-demo-component', 'isElement': false, 'id': 2}, + 'hydration': null, 'directives': [], 'children': [ { 'element': 'router-outlet', 'component': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], + 'hydration': null, 'children': [], 'resolutionPath': [ {'id': '5', 'type': 'element', 'name': '_RouterOutlet'}, @@ -6644,12 +6936,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo-demo', + 'hydration': null, 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -6669,6 +6963,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -6688,6 +6983,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -6706,12 +7002,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -6734,6 +7032,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -6756,6 +7055,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -6777,12 +7077,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -6827,10 +7129,12 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'element': 'app-todo', 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], + 'hydration': null, 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -6874,6 +7178,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -6926,6 +7231,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-zippy', + 'hydration': null, 'component': {'name': 'app-zippy', 'isElement': true, 'id': 19}, 'directives': [], 'children': [], @@ -6940,6 +7246,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'app-heavy', 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, + 'hydration': null, 'directives': [], 'children': [], 'resolutionPath': [ @@ -6978,6 +7285,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 1}], 'children': [], 'resolutionPath': [ @@ -6993,12 +7301,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-demo-component', + 'hydration': null, 'component': {'name': 'app-demo-component', 'isElement': false, 'id': 2}, 'directives': [], 'children': [ { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], 'resolutionPath': [ @@ -7014,12 +7324,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo-demo', + 'hydration': null, 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -7039,6 +7351,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -7058,6 +7371,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -7077,11 +7391,13 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'app-todos', 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, + 'hydration': null, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -7104,6 +7420,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -7126,6 +7443,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -7147,12 +7465,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -7195,12 +7515,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -7244,6 +7566,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -7296,6 +7619,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-zippy', + 'hydration': null, 'component': {'name': 'app-zippy', 'isElement': true, 'id': 19}, 'directives': [], 'children': [], @@ -7309,6 +7633,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-heavy', + 'hydration': null, 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], @@ -7342,6 +7667,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'router-outlet', + 'hydration': null, 'component': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], @@ -7364,12 +7690,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-todo-demo', + 'hydration': null, 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -7389,6 +7717,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -7408,6 +7737,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -7426,12 +7756,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -7454,6 +7786,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -7476,6 +7809,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -7497,12 +7831,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -7545,12 +7881,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -7594,6 +7932,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -7654,6 +7993,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -7680,6 +8020,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -7705,6 +8046,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'router-outlet', + 'hydration': null, 'component': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], @@ -7731,12 +8073,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -7759,6 +8103,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -7781,6 +8126,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -7802,12 +8148,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -7850,12 +8198,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -7899,6 +8249,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -7946,6 +8297,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -7976,6 +8328,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -8006,6 +8359,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -8035,12 +8389,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -8092,6 +8448,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -8123,12 +8480,14 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -8180,6 +8539,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -8212,6 +8572,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { 'node': { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -8241,6 +8602,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-zippy', + 'hydration': null, 'component': {'name': 'app-zippy', 'isElement': true, 'id': 19}, 'directives': [], 'children': [], @@ -8260,6 +8622,7 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { { 'node': { 'element': 'app-heavy', + 'hydration': null, 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], @@ -8370,15 +8733,17 @@ describe('splitInjectorPathsIntoElementAndEnvironmentPaths', () => { describe('grabInjectorPathsFromDirectiveForest', () => { it('should be able to get a list of injector paths from a directive forest', () => { - const directiveForest = [ + const directiveForest: DevToolsNode[] = [ { 'element': 'app-root', + 'hydration': null, 'component': {'name': 'app-root', 'isElement': false, 'id': 0}, 'directives': [], 'children': [ { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 1}], 'children': [], 'resolutionPath': [ @@ -8391,12 +8756,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-demo-component', + 'hydration': null, 'component': {'name': 'app-demo-component', 'isElement': false, 'id': 2}, 'directives': [], 'children': [ { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], 'resolutionPath': [ @@ -8412,12 +8779,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todo-demo', + 'hydration': null, 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -8437,6 +8806,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -8456,6 +8826,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -8476,10 +8847,12 @@ describe('grabInjectorPathsFromDirectiveForest', () => { 'element': 'app-todos', 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], + 'hydration': null, 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -8502,6 +8875,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -8524,6 +8898,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -8545,12 +8920,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -8593,12 +8970,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -8642,6 +9021,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -8694,6 +9074,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-zippy', + 'hydration': null, 'component': {'name': 'app-zippy', 'isElement': true, 'id': 19}, 'directives': [], 'children': [], @@ -8707,6 +9088,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-heavy', + 'hydration': null, 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], @@ -8742,16 +9124,18 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, ]; - const expectedInjectorPaths = [ + const expectedInjectorPaths: InjectorPath[] = [ { 'node': { 'element': 'app-root', + 'hydration': null, 'component': {'name': 'app-root', 'isElement': false, 'id': 0}, 'directives': [], 'children': [ { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 1}], 'children': [], 'resolutionPath': [ @@ -8764,12 +9148,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-demo-component', + 'hydration': null, 'component': {'name': 'app-demo-component', 'isElement': false, 'id': 2}, 'directives': [], 'children': [ { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], 'resolutionPath': [ @@ -8785,12 +9171,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todo-demo', + 'hydration': null, 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -8810,6 +9198,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -8829,6 +9218,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -8847,12 +9237,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -8875,6 +9267,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -8897,6 +9290,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -8918,12 +9312,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -8966,12 +9362,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -9015,6 +9413,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -9067,6 +9466,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-zippy', + 'hydration': null, 'component': {'name': 'app-zippy', 'isElement': true, 'id': 19}, 'directives': [], 'children': [], @@ -9080,6 +9480,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-heavy', + 'hydration': null, 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], @@ -9123,6 +9524,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'node': { 'element': 'router-outlet', + 'hydration': null, 'component': null, 'directives': [{'name': '_RouterOutlet', 'id': 1}], 'children': [], @@ -9145,12 +9547,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'node': { 'element': 'app-demo-component', + 'hydration': null, 'component': {'name': 'app-demo-component', 'isElement': false, 'id': 2}, 'directives': [], 'children': [ { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], 'resolutionPath': [ @@ -9166,12 +9570,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todo-demo', + 'hydration': null, 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -9191,6 +9597,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -9210,6 +9617,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -9228,12 +9636,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -9256,6 +9666,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -9278,6 +9689,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -9299,12 +9711,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -9347,12 +9761,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -9396,6 +9812,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -9448,6 +9865,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-zippy', + 'hydration': null, 'component': {'name': 'app-zippy', 'isElement': true, 'id': 19}, 'directives': [], 'children': [], @@ -9461,6 +9879,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-heavy', + 'hydration': null, 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], @@ -9500,6 +9919,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { 'node': { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 3}], 'children': [], 'resolutionPath': [ @@ -9527,12 +9947,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'node': { 'element': 'app-todo-demo', + 'hydration': null, 'component': {'name': 'app-todo-demo', 'isElement': false, 'id': 4}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], 'resolutionPath': [ @@ -9552,6 +9974,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -9571,6 +9994,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -9589,12 +10013,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -9617,6 +10043,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -9639,6 +10066,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -9660,12 +10088,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -9708,12 +10138,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -9757,6 +10189,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -9823,6 +10256,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'node': { 'element': 'a', + 'hydration': null, 'component': null, 'directives': [{'name': '_RouterLink', 'id': 5}], 'children': [], @@ -9858,6 +10292,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 6}], 'children': [], 'resolutionPath': [ @@ -9892,6 +10327,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { 'node': { 'element': 'router-outlet', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterOutlet', 'id': 7}], 'children': [], 'resolutionPath': [ @@ -9925,12 +10361,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'node': { 'element': 'app-todos', + 'hydration': null, 'component': {'name': 'app-todos', 'isElement': false, 'id': 8}, 'directives': [], 'children': [ { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -9953,6 +10391,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -9975,6 +10414,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -9996,12 +10436,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -10044,12 +10486,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { }, { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -10093,6 +10537,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -10149,6 +10594,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 9}], 'children': [], 'resolutionPath': [ @@ -10189,6 +10635,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 10}], 'children': [], 'resolutionPath': [ @@ -10229,6 +10676,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { 'node': { 'element': 'a', 'component': null, + 'hydration': null, 'directives': [{'name': '_RouterLink', 'id': 11}], 'children': [], 'resolutionPath': [ @@ -10268,12 +10716,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'node': { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 12}, 'directives': [{'name': '_TooltipDirective', 'id': 13}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -10336,6 +10786,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { 'node': { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 14}], 'children': [], 'resolutionPath': [ @@ -10379,12 +10830,14 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'node': { 'element': 'app-todo', + 'hydration': null, 'component': {'name': 'app-todo', 'isElement': false, 'id': 15}, 'directives': [{'name': '_TooltipDirective', 'id': 16}], 'children': [ { 'element': 'div', 'component': null, + 'hydration': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], 'resolutionPath': [ @@ -10446,6 +10899,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'node': { 'element': 'div', + 'hydration': null, 'component': null, 'directives': [{'name': '_TooltipDirective', 'id': 17}], 'children': [], @@ -10491,6 +10945,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { 'node': { 'element': '#comment', 'component': null, + 'hydration': null, 'directives': [{'name': '_NgForOf', 'id': 18}], 'children': [], 'resolutionPath': [ @@ -10530,6 +10985,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'node': { 'element': 'app-zippy', + 'hydration': null, 'component': {'name': 'app-zippy', 'isElement': true, 'id': 19}, 'directives': [], 'children': [], @@ -10552,6 +11008,7 @@ describe('grabInjectorPathsFromDirectiveForest', () => { { 'node': { 'element': 'app-heavy', + 'hydration': null, 'component': {'name': 'app-heavy', 'isElement': false, 'id': 20}, 'directives': [], 'children': [], diff --git a/devtools/projects/ng-devtools/src/lib/devtools.component.html b/devtools/projects/ng-devtools/src/lib/devtools.component.html index 39d9082a794..9b7f315b551 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools.component.html @@ -3,7 +3,7 @@ @if (angularIsInDevMode) { @if (supportedVersion) {
- +
} @else {

diff --git a/devtools/projects/ng-devtools/src/lib/devtools.component.ts b/devtools/projects/ng-devtools/src/lib/devtools.component.ts index b406b879a9a..4a50fdf0723 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools.component.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools.component.ts @@ -34,6 +34,7 @@ export class DevToolsComponent implements OnInit, OnDestroy { angularExists: boolean | null = null; angularVersion: string | boolean | undefined = undefined; angularIsInDevMode = true; + hydration: boolean = false; ivy!: boolean; private readonly _firefoxStyleName = 'firefox_styles.css'; @@ -56,12 +57,13 @@ export class DevToolsComponent implements OnInit, OnDestroy { ngOnInit(): void { this._themeService.initializeThemeWatcher(); - this._messageBus.once('ngAvailability', ({version, devMode, ivy}) => { + this._messageBus.once('ngAvailability', ({version, devMode, ivy, hydration}) => { this.angularExists = !!version; this.angularVersion = version; this.angularIsInDevMode = devMode; this.ivy = ivy; this._interval$.unsubscribe(); + this.hydration = hydration; }); const browserStyleName = this._platform.FIREFOX diff --git a/devtools/projects/protocol/src/lib/messages.ts b/devtools/projects/protocol/src/lib/messages.ts index 277bebc8c8a..20f3c64a002 100644 --- a/devtools/projects/protocol/src/lib/messages.ts +++ b/devtools/projects/protocol/src/lib/messages.ts @@ -19,6 +19,15 @@ export interface ComponentType { id: number; } +export type HydrationStatus = + | null + | {status: 'hydrated' | 'skipped'} + | { + status: 'mismatched'; + expectedNodeDetails: string | null; + actualNodeDetails: string | null; + }; + export interface DevToolsNode { element: string; directives: DirType[]; @@ -26,6 +35,7 @@ export interface DevToolsNode children: DevToolsNode[]; nativeElement?: Node; resolutionPath?: SerializedInjector[]; + hydration: HydrationStatus; } export interface SerializedInjector { @@ -218,6 +228,7 @@ export interface Events { version: string | undefined | boolean; devMode: boolean; ivy: boolean; + hydration: boolean; }) => void; inspectorStart: () => void; @@ -244,6 +255,9 @@ export interface Events { createHighlightOverlay: (position: ElementPosition) => void; removeHighlightOverlay: () => void; + createHydrationOverlay: () => void; + removeHydrationOverlay: () => void; + highlightComponent: (id: number) => void; selectComponent: (id: number) => void; removeComponentHighlight: () => void; diff --git a/devtools/projects/shared-utils/src/lib/angular-check.ts b/devtools/projects/shared-utils/src/lib/angular-check.ts index d87a929a07d..f5ec2c2bdc8 100644 --- a/devtools/projects/shared-utils/src/lib/angular-check.ts +++ b/devtools/projects/shared-utils/src/lib/angular-check.ts @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import {ɵHydratedNode as HydrationNode} from '@angular/core'; + declare const ng: any; export const appIsAngularInDevMode = (): boolean => { @@ -56,3 +58,9 @@ export const getAngularVersion = (): string | null => { } return el.getAttribute('ng-version'); }; + +export function isHydrationEnabled(): boolean { + return Array.from(document.querySelectorAll('[ng-version]')).some( + (rootNode) => (rootNode as HydrationNode)?.__ngDebugHydrationInfo__, + ); +} diff --git a/devtools/tsconfig.json b/devtools/tsconfig.json index fae55a04b1e..da771d30e23 100644 --- a/devtools/tsconfig.json +++ b/devtools/tsconfig.json @@ -24,7 +24,7 @@ "esModuleInterop": true, "importHelpers": true, "target": "es2020", - "lib": ["es2020", "dom"], + "lib": ["es2020", "dom", "dom.iterable"], "typeRoots": [ "./devtools/node_modules/@types", ], diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 00d35972081..2e6fb948a2b 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -27,7 +27,7 @@ export {formatRuntimeError as ɵformatRuntimeError, RuntimeError as ɵRuntimeErr export {annotateForHydration as ɵannotateForHydration} from './hydration/annotate'; export {withDomHydration as ɵwithDomHydration} from './hydration/api'; export {IS_HYDRATION_DOM_REUSE_ENABLED as ɵIS_HYDRATION_DOM_REUSE_ENABLED} from './hydration/tokens'; -export {SSR_CONTENT_INTEGRITY_MARKER as ɵSSR_CONTENT_INTEGRITY_MARKER} from './hydration/utils'; +export {HydratedNode as ɵHydratedNode, HydrationInfo as ɵHydrationInfo, readHydrationInfo as ɵreadHydrationInfo, SSR_CONTENT_INTEGRITY_MARKER as ɵSSR_CONTENT_INTEGRITY_MARKER} from './hydration/utils'; export {CurrencyIndex as ɵCurrencyIndex, ExtraLocaleDataIndex as ɵExtraLocaleDataIndex, findLocaleData as ɵfindLocaleData, getLocaleCurrencyCode as ɵgetLocaleCurrencyCode, getLocalePluralCase as ɵgetLocalePluralCase, LocaleDataIndex as ɵLocaleDataIndex, registerLocaleData as ɵregisterLocaleData, unregisterAllLocaleData as ɵunregisterLocaleData} from './i18n/locale_data_api'; export {DEFAULT_LOCALE_ID as ɵDEFAULT_LOCALE_ID} from './i18n/localization'; export {Writable as ɵWritable} from './interface/type'; diff --git a/packages/core/src/hydration/error_handling.ts b/packages/core/src/hydration/error_handling.ts index 2d306586d95..f6411c643e1 100644 --- a/packages/core/src/hydration/error_handling.ts +++ b/packages/core/src/hydration/error_handling.ts @@ -10,8 +10,11 @@ import {RuntimeError, RuntimeErrorCode} from '../errors'; import {getDeclarationComponentDef} from '../render3/instructions/element_validation'; import {TNode, TNodeType} from '../render3/interfaces/node'; import {RNode} from '../render3/interfaces/renderer_dom'; -import {LView, TVIEW} from '../render3/interfaces/view'; +import {HOST, LView, TVIEW} from '../render3/interfaces/view'; import {getParentRElement} from '../render3/node_manipulation'; +import {unwrapRNode} from '../render3/util/view_utils'; + +import {markRNodeAsHavingHydrationMismatch} from './utils'; const AT_THIS_LOCATION = '<-- AT THIS LOCATION'; @@ -48,7 +51,7 @@ function getFriendlyStringFromTNodeType(tNodeType: TNodeType): string { * Validates that provided nodes match during the hydration process. */ export function validateMatchingNode( - node: RNode, nodeType: number, tagName: string|null, lView: LView, tNode: TNode, + node: RNode|null, nodeType: number, tagName: string|null, lView: LView, tNode: TNode, isViewContainerAnchor = false): void { if (!node || ((node as Node).nodeType !== nodeType || @@ -60,21 +63,25 @@ export function validateMatchingNode( const hostComponentDef = getDeclarationComponentDef(lView); const componentClassName = hostComponentDef?.type?.name; - const expected = `Angular expected this DOM:\n\n${ - describeExpectedDom(lView, tNode, isViewContainerAnchor)}\n\n`; + const expectedDom = describeExpectedDom(lView, tNode, isViewContainerAnchor); + const expected = `Angular expected this DOM:\n\n${expectedDom}\n\n`; let actual = ''; - if (!node) { // No node found during hydration. header += `the node was not found.\n\n`; + + // Since the node is missing, we use the closest node to attach the error to + markRNodeAsHavingHydrationMismatch(unwrapRNode(lView[HOST]!), expectedDom); } else { const actualNode = shortRNodeDescription( (node as Node).nodeType, (node as HTMLElement).tagName ?? null, (node as HTMLElement).textContent ?? null); header += `found ${actualNode}.\n\n`; - actual = `Actual DOM is:\n\n${describeDomFromNode(node)}\n\n`; + const actualDom = describeDomFromNode(node); + actual = `Actual DOM is:\n\n${actualDom}\n\n`; + markRNodeAsHavingHydrationMismatch(node, expectedDom, actualDom); } const footer = getHydrationErrorFooter(componentClassName); @@ -94,6 +101,8 @@ export function validateSiblingNodeExists(node: RNode|null): void { const footer = getHydrationErrorFooter(); const message = header + actual + footer; + + markRNodeAsHavingHydrationMismatch(node!, '', actual); throw new RuntimeError(RuntimeErrorCode.HYDRATION_MISSING_SIBLINGS, message); } } @@ -109,10 +118,15 @@ export function validateNodeExists( let expected = ''; let footer = ''; if (lView !== null && tNode !== null) { - expected = `${describeExpectedDom(lView, tNode, false)}\n\n`; + expected = describeExpectedDom(lView, tNode, false); footer = getHydrationErrorFooter(); + + // Since the node is missing, we use the closest node to attach the error to + markRNodeAsHavingHydrationMismatch(unwrapRNode(lView[HOST]!), expected, ''); } - throw new RuntimeError(RuntimeErrorCode.HYDRATION_MISSING_NODE, header + expected + footer); + + throw new RuntimeError( + RuntimeErrorCode.HYDRATION_MISSING_NODE, `${header}${expected}\n\n${footer}`); } } @@ -141,6 +155,7 @@ export function nodeNotFoundAtPathError(host: Node, path: string): Error { `using the "${path}" path, starting from the ${describeRNode(host)} node.\n\n`; const footer = getHydrationErrorFooter(); + markRNodeAsHavingHydrationMismatch(host); throw new RuntimeError(RuntimeErrorCode.HYDRATION_MISSING_NODE, header + footer); } diff --git a/packages/core/src/hydration/utils.ts b/packages/core/src/hydration/utils.ts index 110cffa497b..d987e6b29d8 100644 --- a/packages/core/src/hydration/utils.ts +++ b/packages/core/src/hydration/utils.ts @@ -1,4 +1,3 @@ - /** * @license * Copyright Google LLC All Rights Reserved. @@ -17,7 +16,7 @@ import {HEADER_OFFSET, LView, TVIEW, TViewType} from '../render3/interfaces/view import {makeStateKey, TransferState} from '../transfer_state'; import {assertDefined} from '../util/assert'; -import {CONTAINERS, DehydratedView, DISCONNECTED_NODES, ELEMENT_CONTAINERS, MULTIPLIER, NUM_ROOT_NODES, SerializedContainerView, SerializedView} from './interfaces'; +import {CONTAINERS, DehydratedView, DISCONNECTED_NODES, ELEMENT_CONTAINERS, MULTIPLIER, NUM_ROOT_NODES, SerializedContainerView, SerializedView,} from './interfaces'; /** * The name of the key used in the TransferState collection, @@ -43,7 +42,6 @@ export const NGH_ATTR_NAME = 'ngh'; export const SSR_CONTENT_INTEGRITY_MARKER = 'nghm'; export const enum TextNodeMarker { - /** * The contents of the text comment added to nodes that would otherwise be * empty when serialized by the server and passed to the client. The empty @@ -75,7 +73,10 @@ export const enum TextNodeMarker { let _retrieveHydrationInfoImpl: typeof retrieveHydrationInfoImpl = () => null; export function retrieveHydrationInfoImpl( - rNode: RElement, injector: Injector, isRootView = false): DehydratedView|null { + rNode: RElement, + injector: Injector, + isRootView = false, + ): DehydratedView|null { let nghAttrValue = rNode.getAttribute(NGH_ATTR_NAME); if (nghAttrValue == null) return null; @@ -95,7 +96,8 @@ export function retrieveHydrationInfoImpl( // We've read one of the ngh ids, keep the remaining one, so that // we can set it back on the DOM element. - const remainingNgh = isRootView ? componentViewNgh : (rootViewNgh ? `|${rootViewNgh}` : ''); + const rootNgh = rootViewNgh ? `|${rootViewNgh}` : ''; + const remainingNgh = isRootView ? componentViewNgh : rootNgh; let data: SerializedView = {}; // An element might have an empty `ngh` attribute value (e.g. ``), @@ -167,7 +169,10 @@ export function enableRetrieveHydrationInfoImpl() { * and accessing a corresponding slot in TransferState storage. */ export function retrieveHydrationInfo( - rNode: RElement, injector: Injector, isRootView = false): DehydratedView|null { + rNode: RElement, + injector: Injector, + isRootView = false, + ): DehydratedView|null { return _retrieveHydrationInfoImpl(rNode, injector, isRootView); } @@ -216,7 +221,7 @@ export function processTextNodeMarkersBeforeHydration(node: HTMLElement) { const isTextNodeMarker = content === TextNodeMarker.EmptyNode || content === TextNodeMarker.Separator; return isTextNodeMarker ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; - } + }, }); let currentNode: Comment; // We cannot modify the DOM while using the commentIterator, @@ -225,7 +230,7 @@ export function processTextNodeMarkersBeforeHydration(node: HTMLElement) { // applying the changes to the DOM: either inserting an empty node // or just removing the marker if it was used as a separator. const nodes = []; - while (currentNode = commentNodesIterator.nextNode() as Comment) { + while ((currentNode = commentNodesIterator.nextNode() as Comment)) { nodes.push(currentNode); } for (const node of nodes) { @@ -241,9 +246,35 @@ export function processTextNodeMarkersBeforeHydration(node: HTMLElement) { * Internal type that represents a claimed node. * Only used in dev mode. */ -type ClaimedNode = { - __claimed?: boolean; +export enum HydrationStatus { + Hydrated = 'hydrated', + Skipped = 'skipped', + Mismatched = 'mismatched', +} + +// clang-format off +export type HydrationInfo = { + status: HydrationStatus.Hydrated|HydrationStatus.Skipped; +}|{ + status: HydrationStatus.Mismatched; + actualNodeDetails: string|null; + expectedNodeDetails: string|null }; +// clang-format on + +const HYDRATION_INFO_KEY = '__ngDebugHydrationInfo__'; + +export type HydratedNode = { + [HYDRATION_INFO_KEY]?: HydrationInfo; +}; + +function patchHydrationInfo(node: RNode, info: HydrationInfo) { + (node as HydratedNode)[HYDRATION_INFO_KEY] = info; +} + +export function readHydrationInfo(node: RNode): HydrationInfo|null { + return (node as HydratedNode)[HYDRATION_INFO_KEY] ?? null; +} /** * Marks a node as "claimed" by hydration process. @@ -254,21 +285,64 @@ export function markRNodeAsClaimedByHydration(node: RNode, checkIfAlreadyClaimed if (!ngDevMode) { throw new Error( 'Calling `markRNodeAsClaimedByHydration` in prod mode ' + - 'is not supported and likely a mistake.'); + 'is not supported and likely a mistake.', + ); } if (checkIfAlreadyClaimed && isRNodeClaimedForHydration(node)) { throw new Error('Trying to claim a node, which was claimed already.'); } - (node as ClaimedNode).__claimed = true; + patchHydrationInfo(node, {status: HydrationStatus.Hydrated}); ngDevMode.hydratedNodes++; } +export function markRNodeAsSkippedByHydration(node: RNode) { + if (!ngDevMode) { + throw new Error( + 'Calling `markRNodeAsSkippedByHydration` in prod mode ' + + 'is not supported and likely a mistake.', + ); + } + patchHydrationInfo(node, {status: HydrationStatus.Skipped}); + ngDevMode.componentsSkippedHydration++; +} + +export function markRNodeAsHavingHydrationMismatch( + node: RNode, + expectedNodeDetails: string|null = null, + actualNodeDetails: string|null = null, +) { + if (!ngDevMode) { + throw new Error( + 'Calling `markRNodeAsMismatchedByHydration` in prod mode ' + + 'is not supported and likely a mistake.', + ); + } + + // The RNode can be a standard HTMLElement + // The devtools component tree only displays Angular components & directives + // Therefore we attach the debug info to the closest a claimed node. + while (node && readHydrationInfo(node)?.status !== HydrationStatus.Hydrated) { + node = node?.parentNode as RNode; + } + + if (node) { + patchHydrationInfo(node, { + status: HydrationStatus.Mismatched, + expectedNodeDetails, + actualNodeDetails, + }); + } +} + export function isRNodeClaimedForHydration(node: RNode): boolean { - return !!(node as ClaimedNode).__claimed; + return readHydrationInfo(node)?.status === HydrationStatus.Hydrated; } export function setSegmentHead( - hydrationInfo: DehydratedView, index: number, node: RNode|null): void { + hydrationInfo: DehydratedView, + index: number, + node: RNode|null, + ): void { hydrationInfo.segmentHeads ??= {}; hydrationInfo.segmentHeads[index] = node; } @@ -298,7 +372,9 @@ export function getNgContainerSize(hydrationInfo: DehydratedView, index: number) } export function getSerializedContainerViews( - hydrationInfo: DehydratedView, index: number): SerializedContainerView[]|null { + hydrationInfo: DehydratedView, + index: number, + ): SerializedContainerView[]|null { return hydrationInfo.data[CONTAINERS]?.[index] ?? null; } @@ -324,7 +400,7 @@ export function isDisconnectedNode(hydrationInfo: DehydratedView, index: number) // Check if we are processing disconnected info for the first time. if (typeof hydrationInfo.disconnectedNodes === 'undefined') { const nodeIds = hydrationInfo.data[DISCONNECTED_NODES]; - hydrationInfo.disconnectedNodes = nodeIds ? (new Set(nodeIds)) : null; + hydrationInfo.disconnectedNodes = nodeIds ? new Set(nodeIds) : null; } return !!hydrationInfo.disconnectedNodes?.has(index); } diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index 450b1b5a66b..5965ec3f132 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -9,7 +9,7 @@ import {invalidSkipHydrationHost, validateMatchingNode, validateNodeExists} from '../../hydration/error_handling'; import {locateNextRNode} from '../../hydration/node_lookup_utils'; import {hasSkipHydrationAttrOnRElement, hasSkipHydrationAttrOnTNode} from '../../hydration/skip_hydration'; -import {getSerializedContainerViews, isDisconnectedNode, markRNodeAsClaimedByHydration, setSegmentHead} from '../../hydration/utils'; +import {getSerializedContainerViews, isDisconnectedNode, markRNodeAsClaimedByHydration, markRNodeAsSkippedByHydration, setSegmentHead} from '../../hydration/utils'; import {assertDefined, assertEqual, assertIndexInRange} from '../../util/assert'; import {assertFirstCreatePass, assertHasParent} from '../assert'; import {attachPatchData} from '../context_discovery'; @@ -17,7 +17,7 @@ import {registerPostOrderHooks} from '../hooks'; import {hasClassInput, hasStyleInput, TAttributes, TElementNode, TNode, TNodeFlags, TNodeType} from '../interfaces/node'; import {Renderer} from '../interfaces/renderer'; import {RElement} from '../interfaces/renderer_dom'; -import {hasI18n, isComponentHost, isContentQueryHost, isDirectiveHost} from '../interfaces/type_checks'; +import {isComponentHost, isContentQueryHost, isDirectiveHost} from '../interfaces/type_checks'; import {HEADER_OFFSET, HYDRATION, LView, RENDERER, TView} from '../interfaces/view'; import {assertTNodeType} from '../node_assert'; import {appendChild, clearElementContents, createElementNode, setupStaticAttributes} from '../node_manipulation'; @@ -242,7 +242,8 @@ function locateOrCreateElementNodeImpl( // so there's no duplicate content after render clearElementContents(native); - ngDevMode && ngDevMode.componentsSkippedHydration++; + ngDevMode && markRNodeAsSkippedByHydration(native); + } else if (ngDevMode) { // If this is not a component host, throw an error. // Hydration can be skipped on per-component basis only. diff --git a/packages/platform-server/test/hydration_spec.ts b/packages/platform-server/test/hydration_spec.ts index 28b4e2e0164..df2a700d839 100644 --- a/packages/platform-server/test/hydration_spec.ts +++ b/packages/platform-server/test/hydration_spec.ts @@ -12,7 +12,7 @@ import {CommonModule, DOCUMENT, isPlatformServer, NgComponentOutlet, NgFor, NgIf import {MockPlatformLocation} from '@angular/common/testing'; import {afterRender, ApplicationRef, Component, ComponentRef, ContentChildren, createComponent, destroyPlatform, Directive, ElementRef, EnvironmentInjector, ErrorHandler, getPlatform, inject, Injectable, Input, NgZone, PLATFORM_ID, Provider, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ɵsetDocument, ɵwhenStable as whenStable} from '@angular/core'; import {Console} from '@angular/core/src/console'; -import {SSR_CONTENT_INTEGRITY_MARKER} from '@angular/core/src/hydration/utils'; +import {HydrationStatus, readHydrationInfo, SSR_CONTENT_INTEGRITY_MARKER} from '@angular/core/src/hydration/utils'; import {getComponentDef} from '@angular/core/src/render3/definition'; import {NoopNgZone} from '@angular/core/src/zone/ng_zone'; import {TestBed} from '@angular/core/testing'; @@ -129,7 +129,7 @@ function verifyAllNodesClaimedForHydration(el: HTMLElement, exceptions: HTMLElem return; } - if (!(el as any).__claimed) { + if (readHydrationInfo(el)?.status !== HydrationStatus.Hydrated) { fail('Hydration error: the node is *not* hydrated: ' + el.outerHTML); } verifyAllChildNodesClaimedForHydration(el, exceptions); @@ -150,7 +150,7 @@ function verifyAllChildNodesClaimedForHydration(el: HTMLElement, exceptions: HTM * hydration feature can be turned off. */ function verifyNoNodesWereClaimedForHydration(el: HTMLElement) { - if ((el as any).__claimed) { + if (readHydrationInfo(el)?.status === HydrationStatus.Hydrated) { fail( 'Unexpected state: the following node was hydrated, when the test ' + 'expects the node to be re-created instead: ' + el.outerHTML);