Skip to content

Commit 57f3550

Browse files
crisbetoAndrewKushnir
authored andcommitted
feat(core): add utility for resolving defer block information to ng global (angular#59184)
Adds the `getDeferBlocks` function to the global `ng` namespace which returns information about all `@defer` blocks inside of a DOM node. This information can be useful either directly in the browser console or to implement future functionality in the dev tools. PR Close angular#59184
1 parent 8a60312 commit 57f3550

File tree

5 files changed

+532
-2
lines changed

5 files changed

+532
-2
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*!
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {DeferBlockDetails, getDeferBlocks as getDeferBlocksInternal} from '../../defer/discovery';
10+
import {
11+
DEFER_BLOCK_STATE,
12+
DeferBlockInternalState,
13+
DeferBlockState,
14+
DeferBlockTrigger,
15+
LDeferBlockDetails,
16+
LOADING_AFTER_SLOT,
17+
MINIMUM_SLOT,
18+
SSR_UNIQUE_ID,
19+
TDeferBlockDetails,
20+
} from '../../defer/interfaces';
21+
import {DEHYDRATED_BLOCK_REGISTRY, DehydratedBlockRegistry} from '../../defer/registry';
22+
import {getLDeferBlockDetails} from '../../defer/utils';
23+
import {assertLView} from '../assert';
24+
import {collectNativeNodes} from '../collect_native_nodes';
25+
import {getLContext} from '../context_discovery';
26+
import {CONTAINER_HEADER_OFFSET} from '../interfaces/container';
27+
import {INJECTOR, LView, TVIEW} from '../interfaces/view';
28+
import {getNativeByTNode} from './view_utils';
29+
30+
/** Retrieved information about a `@defer` block. */
31+
interface DeferBlockData {
32+
/** Current state of the block. */
33+
state: 'placeholder' | 'loading' | 'complete' | 'error' | 'initial';
34+
35+
/** Hydration state of the block. */
36+
incrementalHydrationState: 'not-configured' | 'hydrated' | 'dehydrated';
37+
38+
/** Wherther the block has a connected `@error` block. */
39+
hasErrorBlock: boolean;
40+
41+
/** Information about the connected `@loading` block. */
42+
loadingBlock: {
43+
/** Whether the block is defined. */
44+
exists: boolean;
45+
46+
/** Minimum amount of milliseconds that the block should be shown. */
47+
minimumTime: number | null;
48+
49+
/** Amount of time after which the block should be shown. */
50+
afterTime: number | null;
51+
};
52+
53+
/** Information about the connected `@placeholder` block. */
54+
placeholderBlock: {
55+
/** Whether the block is defined. */
56+
exists: boolean;
57+
58+
/** Minimum amount of time that block should be shown. */
59+
minimumTime: number | null;
60+
};
61+
62+
/** Stringified version of the block's triggers. */
63+
triggers: string[];
64+
65+
/** Element root nodes that are currently being shown in the block. */
66+
rootNodes: Node[];
67+
}
68+
69+
/**
70+
* Gets all of the `@defer` blocks that are present inside the specified DOM node.
71+
* @param node Node in which to look for `@defer` blocks.
72+
*
73+
* @publicApi
74+
*/
75+
export function getDeferBlocks(node: Node): DeferBlockData[] {
76+
const results: DeferBlockData[] = [];
77+
const lView = getLContext(node)?.lView;
78+
79+
if (lView) {
80+
findDeferBlocks(node, lView, results);
81+
}
82+
83+
return results;
84+
}
85+
86+
/**
87+
* Finds all the `@defer` blocks inside a specific node and view.
88+
* @param node Node in which to search for blocks.
89+
* @param lView View within the node in which to search for blocks.
90+
* @param results Array to which to add blocks once they're found.
91+
*/
92+
function findDeferBlocks(node: Node, lView: LView, results: DeferBlockData[]) {
93+
const registry = lView[INJECTOR].get(DEHYDRATED_BLOCK_REGISTRY, null, {optional: true});
94+
const blocks: DeferBlockDetails[] = [];
95+
getDeferBlocksInternal(lView, blocks);
96+
97+
for (const details of blocks) {
98+
const native = getNativeByTNode(details.tNode, details.lView);
99+
const lDetails = getLDeferBlockDetails(details.lView, details.tNode);
100+
101+
// The LView from `getLContext` might be the view the element is placed in.
102+
// Filter out defer blocks that aren't inside the specified root node.
103+
if (!node.contains(native as Node)) {
104+
continue;
105+
}
106+
107+
const tDetails = details.tDetails;
108+
const renderedLView = getRendererLView(details);
109+
const rootNodes: Node[] = [];
110+
111+
if (renderedLView !== null) {
112+
collectNativeNodes(
113+
renderedLView[TVIEW],
114+
renderedLView,
115+
renderedLView[TVIEW].firstChild,
116+
rootNodes,
117+
);
118+
}
119+
120+
const data: DeferBlockData = {
121+
state: stringifyState(lDetails[DEFER_BLOCK_STATE]),
122+
incrementalHydrationState: inferHydrationState(tDetails, lDetails, registry),
123+
hasErrorBlock: tDetails.errorTmplIndex !== null,
124+
loadingBlock: {
125+
exists: tDetails.loadingTmplIndex !== null,
126+
minimumTime: tDetails.loadingBlockConfig?.[MINIMUM_SLOT] ?? null,
127+
afterTime: tDetails.loadingBlockConfig?.[LOADING_AFTER_SLOT] ?? null,
128+
},
129+
placeholderBlock: {
130+
exists: tDetails.placeholderTmplIndex !== null,
131+
minimumTime: tDetails.placeholderBlockConfig?.[MINIMUM_SLOT] ?? null,
132+
},
133+
triggers: tDetails.debug?.triggers ? Array.from(tDetails.debug.triggers).sort() : [],
134+
rootNodes,
135+
};
136+
137+
results.push(data);
138+
139+
// `getDeferBlocks` does not resolve nested defer blocks so we have to recurse manually.
140+
if (renderedLView !== null) {
141+
findDeferBlocks(node, renderedLView, results);
142+
}
143+
}
144+
}
145+
146+
/**
147+
* Turns the `DeferBlockState` into a string which is more readable than the enum form.
148+
*
149+
* @param lDetails Information about the
150+
* @returns
151+
*/
152+
function stringifyState(state: DeferBlockState | DeferBlockInternalState): DeferBlockData['state'] {
153+
switch (state) {
154+
case DeferBlockState.Complete:
155+
return 'complete';
156+
case DeferBlockState.Loading:
157+
return 'loading';
158+
case DeferBlockState.Placeholder:
159+
return 'placeholder';
160+
case DeferBlockState.Error:
161+
return 'error';
162+
case DeferBlockInternalState.Initial:
163+
return 'initial';
164+
default:
165+
throw new Error(`Unrecognized state ${state}`);
166+
}
167+
}
168+
169+
/**
170+
* Infers the hydration state of a specific defer block.
171+
* @param tDetails Static defer block information.
172+
* @param lDetails Instance defer block information.
173+
* @param registry Registry coordinating the hydration of defer blocks.
174+
*/
175+
function inferHydrationState(
176+
tDetails: TDeferBlockDetails,
177+
lDetails: LDeferBlockDetails,
178+
registry: DehydratedBlockRegistry | null,
179+
): DeferBlockData['incrementalHydrationState'] {
180+
if (
181+
registry === null ||
182+
lDetails[SSR_UNIQUE_ID] === null ||
183+
tDetails.hydrateTriggers === null ||
184+
tDetails.hydrateTriggers.has(DeferBlockTrigger.Never)
185+
) {
186+
return 'not-configured';
187+
}
188+
return registry.has(lDetails[SSR_UNIQUE_ID]) ? 'dehydrated' : 'hydrated';
189+
}
190+
191+
/**
192+
* Gets the current LView that is rendered out in a defer block.
193+
* @param details Instance information about the block.
194+
*/
195+
function getRendererLView(details: DeferBlockDetails): LView | null {
196+
// Defer block containers can only ever contain one view.
197+
// If they're empty, it means that nothing is rendered.
198+
if (details.lContainer.length <= CONTAINER_HEADER_OFFSET) {
199+
return null;
200+
}
201+
202+
const lView = details.lContainer[CONTAINER_HEADER_OFFSET];
203+
ngDevMode && assertLView(lView);
204+
return lView;
205+
}

packages/core/src/render3/util/global_utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {setProfiler} from '../profiler';
1212
import {isSignal} from '../reactivity/api';
1313

1414
import {applyChanges} from './change_detection_utils';
15+
import {getDeferBlocks} from './defer';
1516
import {
1617
getComponent,
1718
getContext,
@@ -67,6 +68,7 @@ const globalUtilsFunctions = {
6768
'ɵgetInjectorMetadata': getInjectorMetadata,
6869
'ɵsetProfiler': setProfiler,
6970
'ɵgetSignalGraph': getSignalGraph,
71+
'ɵgetDeferBlocks': getDeferBlocks,
7072

7173
'getDirectiveMetadata': getDirectiveMetadata,
7274
'getComponent': getComponent,

0 commit comments

Comments
 (0)