/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts
690 строк
22 KB
Georgi Serev
refactor(devtools): rename timing API to performance track
29 июл 2026, 18:38
Не верифицирован
29 июл 2026, 18:38
184c479
Код
Авторство
О чём код?
/** * @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.dev/license */ import {ɵDebugSignalGraph as InternalDebugSignalGraph} from '@angular/core'; import {debounceTime} from 'rxjs/operators'; import { ComponentExplorerViewQuery, ComponentType, DebugSignalGraphNode, DevToolsNode, DirectivePosition, DirectiveType, ElementPosition, Events, MessageBus, ProfilerFrame, Route, SerializedInjector, SerializedProviderRecord, SignalNodePosition, TransferStateValue, } from '../../../protocol'; import { appIsAngularInDevMode, appIsAngularIvy, appIsSupportedAngularVersion, getAngularVersion, isHydrationEnabled, } from '../../../shared-utils'; import {ComponentInspector} from './component-inspector/component-inspector'; import { getDirectiveCdStrategy, getElementInjectorElement, getInjectorFromElementNode, getInjectorProviders, getInjectorResolutionPath, getLatestComponentState, idToInjector, isElementInjector, logValue, nodeInjectorToResolutionPath, queryDirectiveForest, serializeProviderRecord, serializeResolutionPath, updateState, } from './directive-forest/component-tree/component-tree'; import {start as startProfiling, stop as stopProfiling} from './profiling/capture'; import {disablePerformanceTrack, enablePerformanceTrack} from './profiling/performance-track'; import {getProfiler, Profiler} from './profiling/profiler'; import {ComponentTreeNode} from './shared/interfaces'; import { ngDebugClient, ngDebugDependencyInjectionApiIsSupported, } from './shared/ng-debug-api/ng-debug-api'; import {getSupportedApis} from './shared/ng-debug-api/supported-apis'; import { getRouterCallableConstructRef, parseRoutes, RoutePropertyType, } from './router-tree/router-tree'; import {setConsoleReference} from './console/set-console-reference'; import {serializeDirectiveState, serializeValue} from './shared/state-serializer/state-serializer'; import {runOutsideAngular, unwrapSignal} from './shared/utils/general'; import {sanitizeObject} from './shared/utils/serialization'; import {SignalGraphRef} from './shared/utils/signal-graph-ref'; import {getDirectiveForestManager} from './directive-forest/manager'; import { highlightHydrationNodes, removeHydrationHighlights, } from './hydration/hydration-highlighting'; import {removeAllHighlights} from './shared/highlighter'; type InspectorRef = {ref: ComponentInspector | null}; export const subscribeToClientEvents = ( messageBus: MessageBus<Events>, depsForTestOnly?: { profiler?: new (...args: any[]) => Profiler; }, ): void => { const inspector: InspectorRef = {ref: null}; messageBus.on('shutdown', shutdownCallback(messageBus)); messageBus.on('devtoolsShutdown', devtoolsShutdownCallback(inspector)); messageBus.on( 'getLatestComponentExplorerView', getLatestComponentExplorerViewCallback(messageBus), ); messageBus.on('queryNgAvailability', checkForAngularCallback(messageBus)); messageBus.on('startProfiling', startProfilingCallback(messageBus)); messageBus.on('stopProfiling', stopProfilingCallback(messageBus)); messageBus.on('setSelectedComponent', selectedComponentCallback(inspector)); messageBus.on('getNestedProperties', getNestedPropertiesCallback(messageBus)); messageBus.on('getRoutes', getRoutesCallback(messageBus)); messageBus.on('navigateRoute', navigateRouteCallback(messageBus)); messageBus.on('getSignalNestedProperties', getSignalNestedPropertiesCallback(messageBus)); messageBus.on('updateState', updateState); messageBus.on('logValue', logValue); messageBus.on('enablePerformanceTrack', enablePerformanceTrack); messageBus.on('disablePerformanceTrack', disablePerformanceTrack); messageBus.on('getInjectorProviders', getInjectorProvidersCallback(messageBus)); messageBus.on('logProvider', logProvider); messageBus.on('getTransferState', getTransferStateCallback(messageBus)); const SAFE_LOG_LEVELS = new Set(['log', 'info', 'warn', 'debug', 'error']); messageBus.on('log', ({message, level}) => { if (SAFE_LOG_LEVELS.has(level)) { console[level](`[Angular DevTools]: ${message}`); } else { console.warn(`[Angular DevTools]: Invalid log level attempted: ${level}`); } }); messageBus.on('getSignalGraph', getSignalGraphCallback(messageBus)); if (appIsAngularInDevMode() && appIsSupportedAngularVersion() && appIsAngularIvy()) { inspector.ref = setupInspector(messageBus); // Often websites have `scroll` event listener which triggers // Angular's change detection. We don't want to constantly send // update requests, instead we want to request an update at most // once every 250ms runOutsideAngular(() => { getProfiler(depsForTestOnly) .changeDetection$.pipe(debounceTime(250)) .subscribe(() => messageBus.emit('componentTreeDirty')); }); } }; // // Callback Definitions // const shutdownCallback = (messageBus: MessageBus<Events>) => () => { messageBus.destroy(); }; const devtoolsShutdownCallback = (inspector: InspectorRef) => () => { inspector.ref?.stopInspecting(); removeAllHighlights(); }; const getLatestComponentExplorerViewCallback = (messageBus: MessageBus<Events>) => (query?: ComponentExplorerViewQuery) => { // We want to force re-indexing of the component tree. // Pressing the refresh button means the user saw stuck UI. getDirectiveForestManager().indexForest(); const forest = prepareForestForSerialization( getDirectiveForestManager().getIndexedDirectiveForest(), ngDebugDependencyInjectionApiIsSupported(), ); if (!query) { messageBus.emit('latestComponentExplorerView', [{forest}]); return; } const state = getLatestComponentState(query, getDirectiveForestManager().getDirectiveForest()); if (state) { const {directiveProperties} = state; messageBus.emit('latestComponentExplorerView', [{forest, properties: directiveProperties}]); } else { // if the node is not found in the tree, we assume its gone and send the tree as is. messageBus.emit('latestComponentExplorerView', [{forest}]); } }; const checkForAngularCallback = (messageBus: MessageBus<Events>) => () => checkForAngular(messageBus); const getRoutesCallback = (messageBus: MessageBus<Events>) => () => getRoutes(messageBus); const navigateRouteCallback = (messageBus: MessageBus<Events>) => (path: string) => { const router: any = getRouterInstance(); // If the router is not found or the navigateByUrl method is not available, we can't navigate if (router) { ngDebugClient().ɵnavigateByUrl?.(router, path); } else { console.warn('Router not found or navigateByUrl method not available'); } }; /** * Opens the source code of a component or a directive in the editor. * @param constructName - The name of the class/function that represents a component, provider, guard * or other callable to view source for. * @param type - The type of the element to view source for component, provider, or directive. * @returns - The element instance of the component, provider, or directive. */ export const viewSourceFromRouter = (constructName: string, type: RoutePropertyType) => { const router: any = getRouterInstance(); if (router === null) { return; } return getRouterCallableConstructRef(router.config, type, constructName); }; const startProfilingCallback = (messageBus: MessageBus<Events>) => () => startProfiling((frame: ProfilerFrame) => { messageBus.emit('sendProfilerChunk', [frame]); }); const stopProfilingCallback = (messageBus: MessageBus<Events>) => () => { messageBus.emit('profilerResults', [stopProfiling()]); }; const selectedComponentCallback = (inspector: InspectorRef) => (position: ElementPosition) => { const node = queryDirectiveForest( position, getDirectiveForestManager().getIndexedDirectiveForest(), ); setConsoleReference({node, position}); inspector.ref?.highlightByPosition(position); }; const getNestedPropertiesCallback = (messageBus: MessageBus<Events>) => (position: DirectivePosition, propPath: string[]) => { const emitEmpty = () => messageBus.emit('nestedProperties', [position, {props: {}}, propPath]); const node = queryDirectiveForest( position.element, getDirectiveForestManager().getIndexedDirectiveForest(), ); if (!node) { return emitEmpty(); } const current = position.directive === undefined ? node.component : node.directives?.[position.directive]; if (!current) { return emitEmpty(); } let data = current.instance; for (const prop of propPath) { data = unwrapSignal(data[prop]); if (!data) { console.error('Cannot access the properties', propPath, 'of', node); } } messageBus.emit('nestedProperties', [ position, {props: serializeDirectiveState(data)}, propPath, ]); return; }; const getSignalNestedPropertiesCallback = (messageBus: MessageBus<Events>) => (position: SignalNodePosition, propPath: string[]) => { const emitEmpty = () => messageBus.emit('signalNestedProperties', [position, {props: {}}, propPath]); const node = queryDirectiveForest( position.element, getDirectiveForestManager().getIndexedDirectiveForest(), ); if (!node || !node.nativeElement) { return emitEmpty(); } const injector = getInjectorFromElementNode(node.nativeElement); if (!injector) { return emitEmpty(); } const ng = ngDebugClient(); let signalGraph: InternalDebugSignalGraph | undefined; // Considering that the inspection of signal value nested properties // usually involves multiple requests, we store the signal graph // during the first call. We keep only the last requested signal graph // to avoid filling the heap with graphs that may not be needed. if (componentSignalGraphRef.exists(node.nativeElement)) { signalGraph = componentSignalGraphRef.deref(node.nativeElement); } else { signalGraph = ng.ɵgetSignalGraph?.(injector); if (signalGraph) { componentSignalGraphRef.set(node.nativeElement, signalGraph); } } if (!signalGraph) { return emitEmpty(); } const current = signalGraph.nodes.find((n) => n.id === position.signalId); if (!current) { return emitEmpty(); } let data = current.value as object; for (const prop of propPath) { data = (data as Record<string, object>)[prop]; if (!data) { console.error('Cannot access the properties', propPath, 'of', node); } } messageBus.emit('signalNestedProperties', [ position, {props: serializeDirectiveState(data)}, propPath, ]); return; }; // // Subscribe Helpers // // todo: parse router tree with framework APIs after they are developed const getRoutes = (messageBus: MessageBus<Events>) => { const route = getRouterConfigFromRoot(); if (!route) return; const sanitizedRoute = sanitizeRouteData(route); messageBus.emit('updateRouterTree', [[sanitizedRoute]]); }; const getRouterConfigFromRoot = (): Route | void => { const router = getRouterInstance(); if (!router) { return; } return parseRoutes(router); }; const checkForAngular = (messageBus: MessageBus<Events>): void => { const ngVersion = getAngularVersion(); const appIsIvy = appIsAngularIvy(); if (!ngVersion) { return; } if (appIsIvy && appIsAngularInDevMode() && appIsSupportedAngularVersion()) { getDirectiveForestManager(); } const devMode = appIsAngularInDevMode(); messageBus.emit('ngAvailability', [ { version: ngVersion.toString(), devMode, ivy: appIsIvy, hydration: isHydrationEnabled(), supportedApis: devMode ? getSupportedApis() : null, }, ]); }; const setupInspector = (messageBus: MessageBus<Events>): ComponentInspector => { const inspector = new ComponentInspector({ onComponentEnter: (id: number) => { messageBus.emit('highlightComponent', [id]); }, onComponentLeave: () => { messageBus.emit('removeComponentHighlight'); }, onComponentSelect: (id: number) => { messageBus.emit('selectComponent', [id]); }, }); messageBus.on('inspectorStart', inspector.startInspecting); messageBus.on('inspectorEnd', inspector.stopInspecting); messageBus.on('createHighlightOverlay', (position: ElementPosition) => { inspector.highlightByPosition(position); }); messageBus.on('removeHighlightOverlay', () => inspector.unhighlight()); messageBus.on('createHydrationOverlay', highlightHydrationNodes); messageBus.on('removeHydrationOverlay', removeHydrationHighlights); return inspector; }; export interface SerializableDirectiveInstanceType extends DirectiveType { id: number; } export interface SerializableComponentInstanceType extends ComponentType { id: number; } export interface SerializableComponentTreeNode extends DevToolsNode< SerializableDirectiveInstanceType, SerializableComponentInstanceType > { children: SerializableComponentTreeNode[]; nativeElement?: never; // Since the nativeElement is not serializable, we will use this boolean as backup hasNativeElement: boolean; } function getRouterInstance() { const forest = getDirectiveForestManager().getIndexedDirectiveForest(); const rootNode = forest[0]; if (!rootNode || !rootNode.nativeElement) { return null; } const injector = getInjectorFromElementNode(rootNode.nativeElement); if (!injector) { return null; } const ng = ngDebugClient(); return (ng as any).ɵgetRouterInstance?.(injector); } // Here we drop properties to prepare the tree for serialization. // We don't need the component instance, so we just traverse the tree // and leave the component name. const prepareForestForSerialization = ( roots: ComponentTreeNode[], includeResolutionPath = false, ): SerializableComponentTreeNode[] => { const serializedNodes: SerializableComponentTreeNode[] = []; for (const node of roots) { const serializedNode: SerializableComponentTreeNode = { tagName: node.tagName, component: node.component ? { name: node.component.name, isElement: node.component.isElement, id: getDirectiveForestManager().getDirectiveId(node.component.instance)!, instanceId: ngDebugClient()?.ɵgetComponentInstanceDeepLinkId?.(node.component.instance), } : null, directives: node.directives?.map((d) => ({ name: d.name, id: getDirectiveForestManager().getDirectiveId(d.instance)!, })), children: prepareForestForSerialization(node.children, includeResolutionPath), hydration: node.hydration, controlFlowBlock: node.controlFlowBlock, static: node.static, changeDetection: node.component ? getDirectiveCdStrategy(node.component) : undefined, // native elements are not serializable hasNativeElement: !!node.nativeElement, }; serializedNodes.push(serializedNode); if (includeResolutionPath) { serializedNode.resolutionPath = getNodeDIResolutionPath(node); } } return serializedNodes; }; function getNodeDIResolutionPath(node: ComponentTreeNode): SerializedInjector[] | undefined { // Some nodes are not linked to HTMLElements, for example @defer blocks if (!node.nativeElement) { return undefined; } const nodeInjector = getInjectorFromElementNode(node.nativeElement); if (!nodeInjector) { return []; } // There are legit cases where an angular node will have non-ElementInjector injectors. // For example, components created with createComponent require the API consumer to // pass in an element injector, else it sets the element injector of the component // to the NullInjector if (!isElementInjector(nodeInjector)) { return []; } const element = getElementInjectorElement(nodeInjector); if (!nodeInjectorToResolutionPath.has(element)) { const resolutionPaths = getInjectorResolutionPath(nodeInjector); nodeInjectorToResolutionPath.set(element, serializeResolutionPath(resolutionPaths)); } return nodeInjectorToResolutionPath.get(element)!; } const getInjectorProvidersCallback = (messageBus: MessageBus<Events>) => (injector: SerializedInjector) => { const resolvedInjector = idToInjector.get(injector.id)?.deref(); if (!resolvedInjector) { return; } const providerRecords = getInjectorProviders(resolvedInjector); const allProviderRecords: SerializedProviderRecord[] = []; const tokenToRecords: Map<any, SerializedProviderRecord[]> = new Map(); for (const [index, providerRecord] of providerRecords.entries()) { const record = serializeProviderRecord( providerRecord, index, injector.type === 'environment', ); allProviderRecords.push(record); const records = tokenToRecords.get(providerRecord.token) ?? []; records.push(record); tokenToRecords.set(providerRecord.token, records); } const serializedProviderRecords: SerializedProviderRecord[] = []; for (const records of tokenToRecords.values()) { const multiRecords = records.filter((record) => record.multi); const nonMultiRecords = records.filter((record) => !record.multi); for (const record of nonMultiRecords) { serializedProviderRecords.push(record); } const [firstMultiRecord] = multiRecords; if (firstMultiRecord !== undefined) { // All multi providers will have the same token, so we can just use the first one. serializedProviderRecords.push({ token: firstMultiRecord.token, type: 'multi', multi: true, // todo(aleksanderbodurri): implememnt way to differentiate multi providers that // provided as viewProviders isViewProvider: firstMultiRecord.isViewProvider, index: records.map((record) => record.index as number), }); } } messageBus.emit('latestInjectorProviders', [injector, serializedProviderRecords]); }; const logProvider = ( serializedInjector: SerializedInjector, serializedProvider: SerializedProviderRecord, ): void => { const injector = idToInjector.get(serializedInjector.id)?.deref(); if (!injector) { return; } const providerRecords = getInjectorProviders(injector); console.group( `%c${serializedInjector.name}`, `color: ${ serializedInjector.type === 'element' ? '#a7d5a9' : '#f05057' }; font-size: 1.25rem; font-weight: bold;`, ); // tslint:disable-next-line:no-console console.log('injector: ', injector); if (typeof serializedProvider.index === 'number') { const provider = providerRecords[serializedProvider.index]; // tslint:disable-next-line:no-console console.log('provider: ', provider); // tslint:disable-next-line:no-console console.log(`value: `, injector.get(provider.token, null, {optional: true})); } else if (Array.isArray(serializedProvider.index)) { const providers = serializedProvider.index.map((index) => providerRecords[index]); // tslint:disable-next-line:no-console console.log('providers: ', providers); // tslint:disable-next-line:no-console console.log(`value: `, injector.get(providers[0].token, null, {optional: true})); } console.groupEnd(); }; const getTransferStateCallback = (messageBus: MessageBus<Events>) => () => { const ng = ngDebugClient(); const forest = getDirectiveForestManager().getIndexedDirectiveForest(); if (forest.length === 0) { messageBus.emit('transferStateData', [null]); return; } const merged: Record<string, TransferStateValue> = {}; let collected = false; for (const rootNode of forest) { if (!rootNode?.nativeElement) continue; const injector = getInjectorFromElementNode(rootNode.nativeElement); if (!injector) continue; const rootData = ng.ɵgetTransferState?.(injector) as Record<string, TransferStateValue> | null | undefined; if (rootData && typeof rootData === 'object') { Object.assign(merged, rootData); collected = true; } } messageBus.emit('transferStateData', [collected ? merged : null]); }; const getSignalGraphCallback = (messageBus: MessageBus<Events>) => (element: ElementPosition) => { // We assume that a new request for a signal graph // should invalidate the current ref cache. componentSignalGraphRef.clear(); const ng = ngDebugClient(); // get injector from position const node = queryDirectiveForest( element, getDirectiveForestManager().getIndexedDirectiveForest(), ); if (!node) { messageBus.emit('latestSignalGraph', [null]); return; } const injector = node.injector ?? getInjectorFromElementNode(node.nativeElement!); if (!injector) { messageBus.emit('latestSignalGraph', [null]); return; } const graph = ng.ɵgetSignalGraph?.(injector); if (graph) { const nodes = graph.nodes.map<DebugSignalGraphNode>((node) => { return { id: node.id, kind: node.kind, label: node.label, epoch: node.epoch, preview: serializeValue(node.value), debuggable: !!node.debuggableFn, }; }); messageBus.emit('latestSignalGraph', [{nodes, edges: graph.edges}]); } }; // Route data needs to be serializable to be sent over the message bus. export function sanitizeRouteData(route: Route): Route { if (route.data) { route.data = sanitizeObject(route.data); } if (route.children) { route.children = route.children.map(sanitizeRouteData); } return route; } /** * Keeps a reference to the last requested signal graph. * This should save us from needlessly calling `ng.ɵgetSignalGraph` * when we are still managing the same/last graph (e.g. inspecting * signal value nested properties). The ref is tied to the host element. * * Note: If the element is destroyed, the graph is garbage collected. */ const componentSignalGraphRef = new SignalGraphRef<Node>();