refactor(core): type-safe global ng (#53439)

This PR provides strict type definition for the window.ng object used
for both console debugging and devtools. `GlobalDevModeUtils` now
gathers all type information about all methods exposed on window.ng.

PR Close #53439
This commit is contained in:
Tomasz Ducin 2023-12-08 00:22:28 +01:00 committed by Andrew Scott
parent e7330560cd
commit 2d7d4e2cf0
17 changed files with 209 additions and 173 deletions

View file

@ -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",

View file

@ -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<Events>)
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<Events>) =>
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);

View file

@ -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<Injector|HTMLElement, string>();
export const nodeInjectorToResolutionPath = new WeakMap<HTMLElement, SerializedInjector[]>();
export const idToInjector = new Map<string, Injector>();
@ -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<SerializedInjector,
}
if (metadata.type === 'element') {
const source = metadata.source! as HTMLElement;
const source = metadata.source as HTMLElement;
const name = stripUnderscore(elementToDirectiveNames(source)[0]);
return {type: 'element', name, providers};
@ -377,7 +345,7 @@ export function serializeInjector(injector: Injector): Omit<SerializedInjector,
}
}
return {type: 'environment', name: stripUnderscore(metadata.source as string), providers};
return {type: 'environment', name: stripUnderscore(metadata.source ?? ''), providers};
}
console.error('Angular DevTools: Could not serialize injector.', injector);
@ -444,15 +412,10 @@ export function getElementInjectorElement(elementInjector: Injector): HTMLElemen
return getInjectorMetadata(elementInjector)!.source as HTMLElement;
}
export function isInjectionToken(token: Type<unknown>|InjectionToken<unknown>): boolean {
function isInjectionToken(token: Type<unknown>|InjectionToken<unknown>): 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<any>()): 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;
}
};

View file

@ -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",
],
)

View file

@ -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);
}
}

View file

@ -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",

View file

@ -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();

View file

@ -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<Hooks> = {}) {
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)));
}

View file

@ -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",
],
)

View file

@ -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;
}

View file

@ -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<unknown>;
isViewProvider: boolean;
provider: SingleProvider;
importPath?: (Injector|Type<unknown>)[];
}
export interface SerializedProviderRecord {
token: string;
type: 'type'|'existing'|'class'|'value'|'factory'|'multi';

View file

@ -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';

View file

@ -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,

View file

@ -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 {
/**

View file

@ -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<K extends GlobalUtilsFunctions>(
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;
}
}

View file

@ -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)!;

View file

@ -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);
}