diff --git a/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel index 4a1a1426436..e012e9d0a13 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( "//devtools/projects/ng-devtools-backend/src/lib/component-inspector", "//devtools/projects/ng-devtools-backend/src/lib/directive-forest", "//devtools/projects/ng-devtools-backend/src/lib/hooks", + "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", "//devtools/projects/ng-devtools-backend/src/lib/state-serializer", "//devtools/projects/protocol", ], @@ -110,6 +111,7 @@ ts_library( ":utils", "//devtools/projects/ng-devtools-backend/src/lib/component-inspector", "//devtools/projects/ng-devtools-backend/src/lib/hooks", + "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", "//devtools/projects/ng-devtools-backend/src/lib/state-serializer", "//devtools/projects/protocol", "//devtools/projects/shared-utils", @@ -123,6 +125,7 @@ ts_library( deps = [ ":interfaces", "//devtools/projects/ng-devtools-backend/src/lib/directive-forest", + "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", "//devtools/projects/ng-devtools-backend/src/lib/state-serializer", "//devtools/projects/protocol", "//packages/core", 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 d55e26e1ae6..e56260880f8 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 @@ -11,11 +11,12 @@ import {debounceTime} from 'rxjs/operators'; import {appIsAngularInDevMode, appIsAngularIvy, appIsSupportedAngularVersion, getAngularVersion,} from 'shared-utils'; import {ComponentInspector} from './component-inspector/component-inspector'; -import {getElementInjectorElement, getInjectorFromElementNode, getInjectorProviders, getInjectorResolutionPath, getLatestComponentState, hasDiDebugAPIs, idToInjector, injectorsSeen, isElementInjector, nodeInjectorToResolutionPath, queryDirectiveForest, serializeProviderRecord, serializeResolutionPath, updateState} from './component-tree'; +import {getElementInjectorElement, getInjectorFromElementNode, getInjectorProviders, getInjectorResolutionPath, getLatestComponentState, idToInjector, injectorsSeen, isElementInjector, nodeInjectorToResolutionPath, queryDirectiveForest, serializeProviderRecord, serializeResolutionPath, updateState} from './component-tree'; import {unHighlight} from './highlighter'; import {disableTimingAPI, enableTimingAPI, initializeOrGetDirectiveForestHooks} from './hooks'; import {start as startProfiling, stop as stopProfiling} from './hooks/capture'; 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 {runOutsideAngular} from './utils'; @@ -75,7 +76,8 @@ const getLatestComponentExplorerViewCallback = (messageBus: MessageBus) initializeOrGetDirectiveForestHooks().indexForest(); const forest = prepareForestForSerialization( - initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(), hasDiDebugAPIs()); + initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(), + ngDebugDependencyInjectionApiIsSupported()); // cleanup injector id mappings for (const injectorId of idToInjector.keys()) { @@ -219,21 +221,21 @@ const prepareForestForSerialization = (roots: ComponentTreeNode[], includeResolu SerializableComponentTreeNode[] => { const serializedNodes: SerializableComponentTreeNode[] = []; for (const node of roots) { - const serializedNode = { + const serializedNode: SerializableComponentTreeNode = { element: node.element, component: node.component ? { name: node.component.name, isElement: node.component.isElement, - id: initializeOrGetDirectiveForestHooks().getDirectiveId(node.component.instance), + id: initializeOrGetDirectiveForestHooks().getDirectiveId(node.component.instance)!, } : null, directives: node.directives.map( (d) => ({ name: d.name, - id: initializeOrGetDirectiveForestHooks().getDirectiveId(d.instance), + id: initializeOrGetDirectiveForestHooks().getDirectiveId(d.instance)!, })), children: prepareForestForSerialization(node.children, includeResolutionPath), - } as SerializableComponentTreeNode; + }; serializedNodes.push(serializedNode); if (includeResolutionPath) { @@ -286,6 +288,7 @@ const getInjectorProvidersCallback = (messageBus: MessageBus) => for (const [index, providerRecord] of providerRecords.entries()) { const record = serializeProviderRecord(providerRecord, index, injector.type === 'environment'); + allProviderRecords.push(record); const records = tokenToRecords.get(providerRecord.token) ?? []; @@ -348,9 +351,8 @@ const logProvider = console.log('provider: ', provider); // tslint:disable-next-line:no-console console.log(`value: `, injector.get(provider.token, null, {optional: true})); - } else { - const providers = - (serializedProvider.index as number[]).map(index => providerRecords[index]); + } else if (Array.isArray(serializedProvider.index)) { + const providers = serializedProvider.index.map(index => providerRecords[index]); // tslint:disable-next-line:no-console console.log('providers: ', providers); 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 4f6146c5c73..6ac38092127 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts @@ -5,10 +5,10 @@ * 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 {ComponentExplorerViewQuery, DirectiveMetadata, DirectivesProperties, ElementPosition, InjectedService, PropertyQueryTypes, ProviderRecord, SerializedInjectedService, SerializedInjector, SerializedProviderRecord, UpdatedStateData,} from 'protocol'; +import {ComponentExplorerViewQuery, DirectiveMetadata, DirectivesProperties, ElementPosition, PropertyQueryTypes, SerializedInjectedService, SerializedInjector, SerializedProviderRecord, UpdatedStateData,} from 'protocol'; import {buildDirectiveTree, getLViewFromDirectiveOrElementInstance} from './directive-forest/index'; +import {ngDebugApiIsSupported, ngDebugClient, ngDebugDependencyInjectionApiIsSupported} from './ng-debug-api/ng-debug-api'; import {deeplySerializeSelectedProperties, serializeDirectiveState,} from './state-serializer/state-serializer'; // Need to be kept in sync with Angular framework @@ -21,9 +21,9 @@ enum ChangeDetectionStrategy { } import {ComponentTreeNode, DirectiveInstanceType, ComponentInstanceType} from './interfaces'; -import type {ClassProvider, ExistingProvider, FactoryProvider, InjectionToken, Injector, Type, ValueProvider,} from '@angular/core'; -const ngDebug = () => (window as any).ng; +import type {ClassProvider, ExistingProvider, FactoryProvider, InjectOptions, InjectionToken, Injector, Type, ValueProvider, ɵComponentDebugMetadata as ComponentDebugMetadata, ɵProviderRecord as ProviderRecord} from '@angular/core'; + export const injectorToId = new WeakMap(); export const nodeInjectorToResolutionPath = new WeakMap(); export const idToInjector = new Map(); @@ -34,32 +34,8 @@ export function getInjectorId() { return `${injectorId++}`; } -export function hasDiDebugAPIs(): boolean { - if (!ngDebugApiIsSupported('ɵgetInjectorResolutionPath')) { - return false; - } - if (!ngDebugApiIsSupported('ɵgetDependenciesFromInjectable')) { - return false; - } - if (!ngDebugApiIsSupported('ɵgetInjectorProviders')) { - return false; - } - if (!ngDebugApiIsSupported('ɵgetInjectorMetadata')) { - return false; - } - - return true; -} - -export function ngDebugApiIsSupported(api: string): boolean { - const ng = ngDebug(); - return typeof ng[api] === 'function'; -} - -export function getInjectorMetadata( - injector: Injector, - ): {type: string; source: HTMLElement | string | null}|null { - return ngDebug().ɵgetInjectorMetadata(injector); +function getInjectorMetadata(injector: Injector) { + return ngDebugClient().ɵgetInjectorMetadata(injector); } export function getInjectorResolutionPath(injector: Injector): Injector[] { @@ -67,23 +43,23 @@ export function getInjectorResolutionPath(injector: Injector): Injector[] { return []; } - return ngDebug().ɵgetInjectorResolutionPath(injector); + return ngDebugClient().ɵgetInjectorResolutionPath(injector); } export function getInjectorFromElementNode(element: Node): Injector|null { - return ngDebug().getInjector(element); + return ngDebugClient().getInjector(element); } -export function getDirectivesFromElement(element: HTMLElement): +function getDirectivesFromElement(element: HTMLElement): {component: unknown|null; directives: unknown[];} { let component = null; if (element instanceof Element) { - component = ngDebug().getComponent(element); + component = ngDebugClient().getComponent(element); } return { component, - directives: ngDebug().getDirectives(element), + directives: ngDebugClient().getDirectives(element), }; } @@ -101,17 +77,15 @@ export const getLatestComponentState = ( const directiveProperties: DirectivesProperties = {}; - const injector = ngDebug().getInjector(node.nativeElement); - - let resolutionPathWithProviders: {injector: Injector; providers: ProviderRecord[]}[] = []; - if (hasDiDebugAPIs()) { - resolutionPathWithProviders = - getInjectorResolutionPath(injector).map((injector) => ({ - injector, - providers: getInjectorProviders(injector), - })); - } + const injector = ngDebugClient().getInjector(node.nativeElement!); + const injectors = getInjectorResolutionPath(injector); + const resolutionPathWithProviders = !ngDebugDependencyInjectionApiIsSupported() ? + [] : + injectors.map((injector) => ({ + injector, + providers: getInjectorProviders(injector), + })); const populateResultSet = (dir: DirectiveInstanceType|ComponentInstanceType) => { const {instance, name} = dir; const metadata = getDirectiveMetadata(instance); @@ -149,7 +123,7 @@ export const getLatestComponentState = ( }; }; -export function serializeElementInjectorWithId(injector: Injector): SerializedInjector|null { +function serializeElementInjectorWithId(injector: Injector): SerializedInjector|null { let id: string; const element = getElementInjectorElement(injector); @@ -171,7 +145,7 @@ export function serializeElementInjectorWithId(injector: Injector): SerializedIn return {id, ...serializedInjector}; } -export function serializeInjectorWithId(injector: Injector): SerializedInjector|null { +function serializeInjectorWithId(injector: Injector): SerializedInjector|null { if (isElementInjector(injector)) { return serializeElementInjectorWithId(injector); } else { @@ -179,7 +153,7 @@ export function serializeInjectorWithId(injector: Injector): SerializedInjector| } } -export function serializeEnvironmentInjectorWithId(injector: Injector): SerializedInjector|null { +function serializeEnvironmentInjectorWithId(injector: Injector): SerializedInjector|null { let id: string; if (!injectorToId.has(injector)) { @@ -210,18 +184,16 @@ const enum DirectiveMetadataKey { // Gets directive metadata. For newer versions of Angular (v12+) it uses // the global `getDirectiveMetadata`. For prior versions of the framework // the method directly interacts with the directive/component definition. -export const getDirectiveMetadata = (dir: any): DirectiveMetadata => { - const getMetadata = (window as any).ng.getDirectiveMetadata; - if (getMetadata) { - const metadata = getMetadata(dir); - if (metadata) { - return { - inputs: metadata.inputs, - outputs: metadata.outputs, - encapsulation: metadata.encapsulation, - onPush: metadata.changeDetection === ChangeDetectionStrategy.OnPush, - }; - } +const getDirectiveMetadata = (dir: any): DirectiveMetadata => { + const getMetadata = ngDebugClient().getDirectiveMetadata; + const metadata = getMetadata?.(dir) as ComponentDebugMetadata; + if (metadata) { + return { + inputs: metadata.inputs, + outputs: metadata.outputs, + encapsulation: metadata.encapsulation, + onPush: metadata.changeDetection === ChangeDetectionStrategy.OnPush, + }; } // Used in older Angular versions, prior to the introduction of `getDirectiveMetadata`. @@ -242,12 +214,12 @@ export const getDirectiveMetadata = (dir: any): DirectiveMetadata => { }; }; -export function getInjectorProviders(injector: Injector): ProviderRecord[] { +export function getInjectorProviders(injector: Injector) { if (isNullInjector(injector)) { return []; } - return ngDebug().ɵgetInjectorProviders(injector); + return ngDebugClient().ɵgetInjectorProviders(injector); } const getDependenciesForDirective = ( @@ -259,12 +231,8 @@ const getDependenciesForDirective = ( return []; } - let dependencies: InjectedService[] = ngDebug() - .ɵgetDependenciesFromInjectable( - injector, - directive, - ) - .dependencies; + let dependencies = + ngDebugClient().ɵgetDependenciesFromInjectable(injector, directive)?.dependencies ?? []; const serializedInjectedServices: SerializedInjectedService[] = []; let position = 0; @@ -285,10 +253,10 @@ const getDependenciesForDirective = ( // + // the import path from the providing injector to the feature module that provided the // dependency (2) - const dependencyResolutionPath = [ + const dependencyResolutionPath: SerializedInjector[] = [ // (1) ...resolutionPath.slice(0, foundInjectorIndex + 1) - .map((node) => serializeInjectorWithId(node.injector)), + .map((node) => serializeInjectorWithId(node.injector)!), // (2) // We slice the import path to remove the first element because this is the same @@ -296,13 +264,13 @@ const getDependenciesForDirective = ( ...(foundProvider?.importPath ?? []).slice(1).map((node) => { return {type: 'imported-module', name: valueToLabel(node), id: getInjectorId()}; }), - ] as SerializedInjector[]; + ]; if (dependency.token && isInjectionToken(dependency.token)) { serializedInjectedServices.push({ token: dependency.token!.toString(), value: valueToLabel(dependency.value), - flags: dependency.flags, + flags: dependency.flags as InjectOptions, position: [position++], resolutionPath: dependencyResolutionPath, }); @@ -312,7 +280,7 @@ const getDependenciesForDirective = ( serializedInjectedServices.push({ token: valueToLabel(dependency.token), value: valueToLabel(dependency.value), - flags: dependency.flags, + flags: dependency.flags as InjectOptions, position: [position++], resolutionPath: dependencyResolutionPath, }); @@ -321,7 +289,7 @@ const getDependenciesForDirective = ( return serializedInjectedServices; }; -export const valueToLabel = (value: any): string => { +const valueToLabel = (value: any): string => { if (isInjectionToken(value)) { return `InjectionToken(${value['_desc']})`; } @@ -360,7 +328,7 @@ export function serializeInjector(injector: Injector): Omit|InjectionToken): boolean { +function isInjectionToken(token: Type|InjectionToken): boolean { return token.constructor.name === 'InjectionToken'; } -export function isEnvironmentInjector(injector: Injector) { - const metadata = getInjectorMetadata(injector); - return metadata !== null && metadata.type === 'environment'; -} - export function isElementInjector(injector: Injector) { const metadata = getInjectorMetadata(injector); return metadata !== null && metadata.type === 'element'; @@ -480,12 +443,10 @@ const getRootLViewsHelper = (element: Element, rootLViews = new Set()): Set }; const getRoots = () => { - const roots = Array.from( - document.documentElement.querySelectorAll('[ng-version]'), - ) as HTMLElement[]; + const roots = Array.from(document.documentElement.querySelectorAll('[ng-version]')); - const isTopLevel = (element: HTMLElement) => { - let parent: HTMLElement|null = element; + const isTopLevel = (element: Element) => { + let parent: Element|null = element; while (parent?.parentElement) { parent = parent.parentElement; @@ -541,7 +502,7 @@ export const findNodeFromSerializedPosition = ( }; export const updateState = (updatedStateData: UpdatedStateData): void => { - const ngd = ngDebug(); + const ng = ngDebugClient(); const node = queryDirectiveForest(updatedStateData.directiveId.element, buildDirectiveForest()); if (!node) { console.warn( @@ -554,13 +515,13 @@ export const updateState = (updatedStateData: UpdatedStateData): void => { if (updatedStateData.directiveId.directive !== undefined) { const directive = node.directives[updatedStateData.directiveId.directive].instance; mutateComponentOrDirective(updatedStateData, directive); - ngd.applyChanges(ngd.getOwningComponent(directive)); + ng.applyChanges(ng.getOwningComponent(directive)!); return; } if (node.component) { const comp = node.component.instance; mutateComponentOrDirective(updatedStateData, comp); - ngd.applyChanges(comp); + ng.applyChanges(comp); return; } }; 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 db24e0cafdd..dbb1903aa8e 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 @@ -14,6 +14,7 @@ ts_library( "//devtools/projects/ng-devtools-backend/src/lib:interfaces", "//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", "@npm//semver-dsl", ], ) 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 a2d3801e9a9..80f651feed1 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 @@ -7,10 +7,12 @@ */ import {ComponentTreeNode} from '../interfaces'; +import {ngDebugClient} from '../ng-debug-api/ng-debug-api'; import {isCustomElement} from '../utils'; const extractViewTree = - (domNode: Node|Element, result: ComponentTreeNode[], getComponent: (element: Element) => {}, + (domNode: Node|Element, result: ComponentTreeNode[], + getComponent: (element: Element) => {} | null, getDirectives: (node: Node) => {}[]): ComponentTreeNode[] => { const directives = getDirectives(domNode); if (!directives.length && !(domNode instanceof Element)) { @@ -56,8 +58,8 @@ const extractViewTree = export class RTreeStrategy { supports(_: any): boolean { - return ['getDirectiveMetadata', 'getComponent', 'getDirectives'].every( - (method) => typeof (window as any).ng[method] === 'function'); + return (['getDirectiveMetadata', 'getComponent', 'getDirectives'] as const) + .every((method) => typeof ngDebugClient()[method] === 'function'); } build(element: Element): ComponentTreeNode[] { @@ -66,8 +68,8 @@ export class RTreeStrategy { while (element.parentElement) { element = element.parentElement; } - const getComponent = (window as any).ng.getComponent as (element: Element) => {}; - const getDirectives = (window as any).ng.getDirectives as (node: Node) => {}[]; + const getComponent = ngDebugClient().getComponent; + const getDirectives = ngDebugClient().getDirectives; return extractViewTree(element, [], getComponent, getDirectives); } } diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/BUILD.bazel index 4e7d9243ed7..1c24f7fd8ec 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/BUILD.bazel @@ -12,6 +12,7 @@ ts_library( "//devtools/projects/ng-devtools-backend/src/lib:utils", "//devtools/projects/ng-devtools-backend/src/lib/directive-forest", "//devtools/projects/ng-devtools-backend/src/lib/hooks:identity_tracker", + "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", "//devtools/projects/protocol", "//packages/core", "@npm//rxjs", diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/index.ts b/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/index.ts index aec12240702..f9d36e6df7d 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/index.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/index.ts @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import {ngDebugClient} from '../../ng-debug-api/ng-debug-api'; + import {NgProfiler} from './native'; import {PatchingProfiler} from './polyfill'; import {Profiler} from './shared'; @@ -17,8 +19,7 @@ export {Hooks, Profiler} from './shared'; * Gives priority to NgProfiler, falls back on PatchingProfiler if framework APIs are not present. */ export const selectProfilerStrategy = (): Profiler => { - const ng = (window as any).ng; - if (typeof ng?.ɵsetProfiler === 'function') { + if (typeof ngDebugClient().ɵsetProfiler === 'function') { return new NgProfiler(); } return new PatchingProfiler(); diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/native.ts b/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/native.ts index 7d83e9a0b3d..3d06cdd137f 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/native.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/native.ts @@ -9,12 +9,14 @@ import {ɵProfilerEvent} from '@angular/core'; import {getDirectiveHostElement} from '../../directive-forest'; +import {ngDebugClient} from '../../ng-debug-api/ng-debug-api'; import {runOutsideAngular} from '../../utils'; import {IdentityTracker, NodeArray} from '../identity-tracker'; import {getLifeCycleName, Hooks, Profiler} from './shared'; -type ProfilerCallback = (event: ɵProfilerEvent, instanceOrLView: {}, hookOrListener: any) => void; +type ProfilerCallback = (event: ɵProfilerEvent, instanceOrLView: {}|null, hookOrListener: any) => + void; /** Implementation of Profiler that utilizes framework APIs fire profiler hooks. */ export class NgProfiler extends Profiler { @@ -24,20 +26,20 @@ export class NgProfiler extends Profiler { constructor(config: Partial = {}) { super(config); - this._setProfilerCallback((event: ɵProfilerEvent, instanceOrLView: {}, hookOrListener: any) => { - if (this[event] === undefined) { - return; - } + this._setProfilerCallback( + (event: ɵProfilerEvent, instanceOrLView: {}|null, hookOrListener: any) => { + if (this[event] === undefined) { + return; + } - this[event](instanceOrLView, hookOrListener); - }); + this[event](instanceOrLView, hookOrListener); + }); this._initialize(); } private _initialize(): void { - const ng = (window as any).ng; - ng.ɵsetProfiler( - (event: ɵProfilerEvent, instanceOrLView: {}, hookOrListener: any) => + ngDebugClient().ɵsetProfiler( + (event: ɵProfilerEvent, instanceOrLView: {}|null, hookOrListener: any) => this._callbacks.forEach((cb) => cb(event, instanceOrLView, hookOrListener))); } diff --git a/devtools/projects/ng-devtools-backend/src/lib/ng-debug-api/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/ng-debug-api/BUILD.bazel new file mode 100644 index 00000000000..2098079957b --- /dev/null +++ b/devtools/projects/ng-devtools-backend/src/lib/ng-debug-api/BUILD.bazel @@ -0,0 +1,15 @@ +# load("//devtools/tools:typescript.bzl", "ts_library") +load("//devtools/tools:ng_module.bzl", "ng_module") + +package(default_visibility = ["//visibility:public"]) + +ng_module( + name = "ng-debug-api", + srcs = glob( + include = ["*.ts"], + exclude = ["*.spec.ts"], + ), + deps = [ + "//packages/core", + ], +) diff --git a/devtools/projects/ng-devtools-backend/src/lib/ng-debug-api/ng-debug-api.ts b/devtools/projects/ng-devtools-backend/src/lib/ng-debug-api/ng-debug-api.ts new file mode 100644 index 00000000000..de65fdce6ef --- /dev/null +++ b/devtools/projects/ng-devtools-backend/src/lib/ng-debug-api/ng-debug-api.ts @@ -0,0 +1,48 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import type {ɵGlobalDevModeUtils as GlobalDevModeUtils} from '@angular/core'; + +/** + * Returns a handle to window.ng APIs (global angular debugging). + * + * @returns window.ng + */ +export const ngDebugClient = () => (window as any as GlobalDevModeUtils).ng; + +/** + * Checks whether a given debug API is supported within window.ng + * + * @returns boolean + */ +export function ngDebugApiIsSupported(api: keyof GlobalDevModeUtils['ng']): boolean { + const ng = ngDebugClient(); + return typeof ng[api] === 'function'; +} + +/** + * Checks whether Dependency Injection debug API is supported within window.ng + * + * @returns boolean + */ +export function ngDebugDependencyInjectionApiIsSupported(): boolean { + if (!ngDebugApiIsSupported('ɵgetInjectorResolutionPath')) { + return false; + } + if (!ngDebugApiIsSupported('ɵgetDependenciesFromInjectable')) { + return false; + } + if (!ngDebugApiIsSupported('ɵgetInjectorProviders')) { + return false; + } + if (!ngDebugApiIsSupported('ɵgetInjectorMetadata')) { + return false; + } + + return true; +} diff --git a/devtools/projects/protocol/src/lib/messages.ts b/devtools/projects/protocol/src/lib/messages.ts index 823af2de961..73f2e120754 100644 --- a/devtools/projects/protocol/src/lib/messages.ts +++ b/devtools/projects/protocol/src/lib/messages.ts @@ -7,7 +7,6 @@ */ import {InjectionToken, InjectOptions, Injector, Type, ViewEncapsulation} from '@angular/core'; -import {SingleProvider} from '@angular/core/src/di/provider_collection'; export interface DirectiveType { name: string; @@ -37,17 +36,6 @@ export interface SerializedInjector { providers?: number; } -/** - * Duplicate of the ProviderRecord interface from Angular framework to prevent - * needing to publically expose the interface from the framework. - */ -export interface ProviderRecord { - token: Type; - isViewProvider: boolean; - provider: SingleProvider; - importPath?: (Injector|Type)[]; -} - export interface SerializedProviderRecord { token: string; type: 'type'|'existing'|'class'|'value'|'factory'|'multi'; diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 97eab3fd7f2..21c8d7112f8 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -36,7 +36,7 @@ export {PendingTasks as ɵPendingTasks} from './pending_tasks'; export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS} from './platform/platform'; export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities'; export {AnimationRendererType as ɵAnimationRendererType} from './render/api'; -export {InjectorProfilerContext as ɵInjectorProfilerContext, setInjectorProfilerContext as ɵsetInjectorProfilerContext} from './render3/debug/injector_profiler'; +export {InjectorProfilerContext as ɵInjectorProfilerContext, ProviderRecord as ɵProviderRecord, setInjectorProfilerContext as ɵsetInjectorProfilerContext} from './render3/debug/injector_profiler'; export {allowSanitizationBypassAndThrow as ɵallowSanitizationBypassAndThrow, BypassType as ɵBypassType, getSanitizationBypassType as ɵgetSanitizationBypassType, SafeHtml as ɵSafeHtml, SafeResourceUrl as ɵSafeResourceUrl, SafeScript as ɵSafeScript, SafeStyle as ɵSafeStyle, SafeUrl as ɵSafeUrl, SafeValue as ɵSafeValue, unwrapSafeValue as ɵunwrapSafeValue} from './sanitization/bypass'; export {_sanitizeHtml as ɵ_sanitizeHtml} from './sanitization/html_sanitizer'; export {_sanitizeUrl as ɵ_sanitizeUrl} from './sanitization/url_sanitizer'; diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 4f9c1a2435d..87b5497e8e9 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -48,6 +48,7 @@ export { export { AttributeMarker as ɵAttributeMarker, ComponentDef as ɵComponentDef, + ComponentDebugMetadata as ɵComponentDebugMetadata, ComponentFactory as ɵRender3ComponentFactory, ComponentRef as ɵRender3ComponentRef, ComponentType as ɵComponentType, @@ -284,10 +285,7 @@ export { isNgModule as ɵisNgModule } from './render3/jit/util'; export { Profiler as ɵProfiler, ProfilerEvent as ɵProfilerEvent } from './render3/profiler'; -export { - publishDefaultGlobalUtils as ɵpublishDefaultGlobalUtils -, - publishGlobalUtil as ɵpublishGlobalUtil} from './render3/util/global_utils'; +export { GlobalDevModeUtils as ɵGlobalDevModeUtils } from './render3/util/global_utils'; export {ViewRef as ɵViewRef} from './render3/view_ref'; export { bypassSanitizationTrustHtml as ɵbypassSanitizationTrustHtml, diff --git a/packages/core/src/render3/debug/injector_profiler.ts b/packages/core/src/render3/debug/injector_profiler.ts index d5507c1da36..d47db92a734 100644 --- a/packages/core/src/render3/debug/injector_profiler.ts +++ b/packages/core/src/render3/debug/injector_profiler.ts @@ -84,6 +84,8 @@ export type InjectorProfilerEvent = /** * An object that contains information about a provider that has been configured + * + * TODO: rename to indicate that it is a debug structure eg. ProviderDebugInfo. */ export interface ProviderRecord { /** diff --git a/packages/core/src/render3/util/global_utils.ts b/packages/core/src/render3/util/global_utils.ts index cf503747143..e4382d5c683 100644 --- a/packages/core/src/render3/util/global_utils.ts +++ b/packages/core/src/render3/util/global_utils.ts @@ -33,6 +33,31 @@ import {getDependenciesFromInjectable, getInjectorMetadata, getInjectorProviders * */ export const GLOBAL_PUBLISH_EXPANDO_KEY = 'ng'; +const globalUtilsFunctions = { + /** + * Warning: functions that start with `ɵ` are considered *INTERNAL* and should not be relied upon + * in application's code. The contract of those functions might be changed in any release and/or a + * function can be removed completely. + */ + 'ɵgetDependenciesFromInjectable': getDependenciesFromInjectable, + 'ɵgetInjectorProviders': getInjectorProviders, + 'ɵgetInjectorResolutionPath': getInjectorResolutionPath, + 'ɵgetInjectorMetadata': getInjectorMetadata, + 'ɵsetProfiler': setProfiler, + + 'getDirectiveMetadata': getDirectiveMetadata, + 'getComponent': getComponent, + 'getContext': getContext, + 'getListeners': getListeners, + 'getOwningComponent': getOwningComponent, + 'getHostElement': getHostElement, + 'getInjector': getInjector, + 'getRootComponents': getRootComponents, + 'getDirectives': getDirectives, + 'applyChanges': applyChanges, +}; +type GlobalUtilsFunctions = keyof typeof globalUtilsFunctions; + let _published = false; /** * Publishes a collection of default debug tools onto`window.ng`. @@ -45,51 +70,34 @@ export function publishDefaultGlobalUtils() { _published = true; setupFrameworkInjectorProfiler(); - publishGlobalUtil('ɵgetDependenciesFromInjectable', getDependenciesFromInjectable); - publishGlobalUtil('ɵgetInjectorProviders', getInjectorProviders); - publishGlobalUtil('ɵgetInjectorResolutionPath', getInjectorResolutionPath); - publishGlobalUtil('ɵgetInjectorMetadata', getInjectorMetadata); - /** - * Warning: this function is *INTERNAL* and should not be relied upon in application's code. - * The contract of the function might be changed in any release and/or the function can be - * removed completely. - */ - publishGlobalUtil('ɵsetProfiler', setProfiler); - publishGlobalUtil('getDirectiveMetadata', getDirectiveMetadata); - publishGlobalUtil('getComponent', getComponent); - publishGlobalUtil('getContext', getContext); - publishGlobalUtil('getListeners', getListeners); - publishGlobalUtil('getOwningComponent', getOwningComponent); - publishGlobalUtil('getHostElement', getHostElement); - publishGlobalUtil('getInjector', getInjector); - publishGlobalUtil('getRootComponents', getRootComponents); - publishGlobalUtil('getDirectives', getDirectives); - publishGlobalUtil('applyChanges', applyChanges); + for (const [methodName, method] of Object.entries(globalUtilsFunctions)) { + publishGlobalUtil(methodName as GlobalUtilsFunctions, method); + } } } -export declare type GlobalDevModeContainer = { - [GLOBAL_PUBLISH_EXPANDO_KEY]: {[fnName: string]: Function}; +/** + * Default debug tools available under `window.ng`. + */ +export type GlobalDevModeUtils = { + [GLOBAL_PUBLISH_EXPANDO_KEY]: typeof globalUtilsFunctions; }; /** * Publishes the given function to `window.ng` so that it can be * used from the browser console when an application is not in production. */ -export function publishGlobalUtil(name: string, fn: Function): void { +export function publishGlobalUtil( + name: K, fn: typeof globalUtilsFunctions[K]): void { if (typeof COMPILED === 'undefined' || !COMPILED) { // Note: we can't export `ng` when using closure enhanced optimization as: // - closure declares globals itself for minified names, which sometimes clobber our `ng` global // - we can't declare a closure extern as the namespace `ng` is already used within Google // for typings for AngularJS (via `goog.provide('ng....')`). - const w = global as any as GlobalDevModeContainer; + const w = global as GlobalDevModeUtils; ngDevMode && assertDefined(fn, 'function not defined'); - if (w) { - let container = w[GLOBAL_PUBLISH_EXPANDO_KEY]; - if (!container) { - container = w[GLOBAL_PUBLISH_EXPANDO_KEY] = {}; - } - container[name] = fn; - } + + w[GLOBAL_PUBLISH_EXPANDO_KEY] ??= {} as any; + w[GLOBAL_PUBLISH_EXPANDO_KEY][name] = fn; } } diff --git a/packages/core/src/render3/util/injector_discovery_utils.ts b/packages/core/src/render3/util/injector_discovery_utils.ts index cb5181f4260..71b0c024f8a 100644 --- a/packages/core/src/render3/util/injector_discovery_utils.ts +++ b/packages/core/src/render3/util/injector_discovery_utils.ts @@ -478,8 +478,8 @@ export function getInjectorProviders(injector: Injector): ProviderRecord[] { * @returns an object containing the type and source of the given injector. If the injector metadata * cannot be determined, returns null. */ -export function getInjectorMetadata(injector: Injector): - {type: string; source: RElement | string | null}|null { +export function getInjectorMetadata(injector: Injector): {type: 'element', source: RElement}| + {type: 'environment', source: string | null}|{type: 'null', source: null}|null { if (injector instanceof NodeInjector) { const lView = getNodeInjectorLView(injector); const tNode = getNodeInjectorTNode(injector)!; diff --git a/packages/core/test/render3/global_utils_spec.ts b/packages/core/test/render3/global_utils_spec.ts index dfb1aca5a46..fc8e9adc480 100644 --- a/packages/core/test/render3/global_utils_spec.ts +++ b/packages/core/test/render3/global_utils_spec.ts @@ -7,19 +7,23 @@ */ import {setProfiler} from '@angular/core/src/render3/profiler'; + import {applyChanges} from '../../src/render3/util/change_detection_utils'; import {getComponent, getContext, getDirectiveMetadata, getDirectives, getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents} from '../../src/render3/util/discovery_utils'; -import {GLOBAL_PUBLISH_EXPANDO_KEY, GlobalDevModeContainer, publishDefaultGlobalUtils, publishGlobalUtil} from '../../src/render3/util/global_utils'; +import {GLOBAL_PUBLISH_EXPANDO_KEY, GlobalDevModeUtils, publishDefaultGlobalUtils, publishGlobalUtil} from '../../src/render3/util/global_utils'; import {global} from '../../src/util/global'; +type GlobalUtilFunctions = keyof GlobalDevModeUtils['ng']; + describe('global utils', () => { describe('publishGlobalUtil', () => { it('should publish a function to the window', () => { - const w = global as any as GlobalDevModeContainer; - expect(w[GLOBAL_PUBLISH_EXPANDO_KEY]['foo']).toBeFalsy(); + const w = global as any as GlobalDevModeUtils; + const foo = 'foo' as GlobalUtilFunctions; + expect(w[GLOBAL_PUBLISH_EXPANDO_KEY][foo]).toBeFalsy(); const fooFn = () => {}; - publishGlobalUtil('foo', fooFn); - expect(w[GLOBAL_PUBLISH_EXPANDO_KEY]['foo']).toBe(fooFn); + publishGlobalUtil(foo, fooFn); + expect(w[GLOBAL_PUBLISH_EXPANDO_KEY][foo]).toBe(fooFn); }); }); @@ -72,7 +76,7 @@ describe('global utils', () => { }); }); -function assertPublished(name: string, value: Function) { - const w = global as any as GlobalDevModeContainer; +function assertPublished(name: GlobalUtilFunctions, value: Function) { + const w = global as any as GlobalDevModeUtils; expect(w[GLOBAL_PUBLISH_EXPANDO_KEY][name]).toBe(value); }