2021-12-10 02:37:01 +00:00
|
|
|
/**
|
|
|
|
|
* @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
|
|
|
|
|
*/
|
|
|
|
|
|
2023-10-05 08:00:31 +00:00
|
|
|
import {ComponentExplorerViewQuery, DirectiveMetadata, DirectivesProperties, ElementPosition, InjectedService, PropertyQueryTypes, ProviderRecord, SerializedInjectedService, SerializedInjector, SerializedProviderRecord, UpdatedStateData,} from 'protocol';
|
2021-12-09 05:44:17 +00:00
|
|
|
|
|
|
|
|
import {buildDirectiveTree, getLViewFromDirectiveOrElementInstance} from './directive-forest/index';
|
2023-12-04 19:44:53 +00:00
|
|
|
import {deeplySerializeSelectedProperties, serializeDirectiveState,} from './state-serializer/state-serializer';
|
2021-10-11 18:11:29 +00:00
|
|
|
|
|
|
|
|
// Need to be kept in sync with Angular framework
|
|
|
|
|
// We can't directly import it from framework now
|
|
|
|
|
// because this also pulls up the security policies
|
|
|
|
|
// for Trusted Types, which we reinstantiate.
|
|
|
|
|
enum ChangeDetectionStrategy {
|
|
|
|
|
OnPush = 0,
|
|
|
|
|
Default = 1,
|
|
|
|
|
}
|
2020-01-27 18:40:18 +00:00
|
|
|
|
2021-12-09 05:44:17 +00:00
|
|
|
import {ComponentTreeNode, DirectiveInstanceType, ComponentInstanceType} from './interfaces';
|
2023-12-04 19:44:53 +00:00
|
|
|
import type {ClassProvider, ExistingProvider, FactoryProvider, InjectionToken, Injector, Type, ValueProvider,} from '@angular/core';
|
2020-01-27 18:40:18 +00:00
|
|
|
|
2021-09-08 22:36:40 +00:00
|
|
|
const ngDebug = () => (window as any).ng;
|
2023-10-05 08:00:31 +00:00
|
|
|
export const injectorToId = new WeakMap<Injector|HTMLElement, string>();
|
2023-11-02 05:50:48 +00:00
|
|
|
export const nodeInjectorToResolutionPath = new WeakMap<HTMLElement, SerializedInjector[]>();
|
2023-10-05 08:00:31 +00:00
|
|
|
export const idToInjector = new Map<string, Injector>();
|
|
|
|
|
export const injectorsSeen = new Set<string>();
|
|
|
|
|
let injectorId = 0;
|
|
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
export function getInjectorMetadata(
|
|
|
|
|
injector: Injector,
|
|
|
|
|
): {type: string; source: HTMLElement | string | null}|null {
|
2023-10-05 08:00:31 +00:00
|
|
|
return ngDebug().ɵgetInjectorMetadata(injector);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getInjectorResolutionPath(injector: Injector): Injector[] {
|
2023-11-02 05:50:48 +00:00
|
|
|
if (!ngDebugApiIsSupported('ɵgetInjectorResolutionPath')) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-05 08:00:31 +00:00
|
|
|
return ngDebug().ɵgetInjectorResolutionPath(injector);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getInjectorFromElementNode(element: Node): Injector|null {
|
|
|
|
|
return ngDebug().getInjector(element);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getDirectivesFromElement(element: HTMLElement):
|
|
|
|
|
{component: unknown|null; directives: unknown[];} {
|
|
|
|
|
let component = null;
|
|
|
|
|
if (element instanceof Element) {
|
|
|
|
|
component = ngDebug().getComponent(element);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
component,
|
|
|
|
|
directives: ngDebug().getDirectives(element),
|
|
|
|
|
};
|
|
|
|
|
}
|
2020-01-27 18:40:18 +00:00
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
export const getLatestComponentState = (
|
|
|
|
|
query: ComponentExplorerViewQuery,
|
|
|
|
|
directiveForest?: ComponentTreeNode[],
|
|
|
|
|
): {directiveProperties: DirectivesProperties}|undefined => {
|
|
|
|
|
// if a directive forest is passed in we don't have to build the forest again.
|
|
|
|
|
directiveForest = directiveForest ?? buildDirectiveForest();
|
2023-11-10 17:23:19 +00:00
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
const node = queryDirectiveForest(query.selectedElement, directiveForest);
|
|
|
|
|
if (!node) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-11-10 17:23:19 +00:00
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
const directiveProperties: DirectivesProperties = {};
|
2023-11-10 17:23:19 +00:00
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
const injector = ngDebug().getInjector(node.nativeElement);
|
2023-11-10 17:23:19 +00:00
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
let resolutionPathWithProviders: {injector: Injector; providers: ProviderRecord[]}[] = [];
|
|
|
|
|
if (hasDiDebugAPIs()) {
|
|
|
|
|
resolutionPathWithProviders =
|
|
|
|
|
getInjectorResolutionPath(injector).map((injector) => ({
|
|
|
|
|
injector,
|
|
|
|
|
providers: getInjectorProviders(injector),
|
|
|
|
|
}));
|
|
|
|
|
}
|
2023-11-10 17:23:19 +00:00
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
const populateResultSet = (dir: DirectiveInstanceType|ComponentInstanceType) => {
|
|
|
|
|
const {instance, name} = dir;
|
|
|
|
|
const metadata = getDirectiveMetadata(instance);
|
|
|
|
|
metadata.dependencies = getDependenciesForDirective(
|
|
|
|
|
injector,
|
|
|
|
|
resolutionPathWithProviders,
|
|
|
|
|
instance.constructor,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (query.propertyQuery.type === PropertyQueryTypes.All) {
|
|
|
|
|
directiveProperties[dir.name] = {
|
|
|
|
|
props: serializeDirectiveState(instance),
|
|
|
|
|
metadata,
|
2023-11-10 17:23:19 +00:00
|
|
|
};
|
2023-12-04 19:44:53 +00:00
|
|
|
}
|
2023-11-10 17:23:19 +00:00
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
if (query.propertyQuery.type === PropertyQueryTypes.Specified) {
|
|
|
|
|
directiveProperties[name] = {
|
|
|
|
|
props: deeplySerializeSelectedProperties(
|
|
|
|
|
instance,
|
|
|
|
|
query.propertyQuery.properties[name] || [],
|
|
|
|
|
),
|
|
|
|
|
metadata,
|
2023-11-10 17:23:19 +00:00
|
|
|
};
|
2023-12-04 19:44:53 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
node.directives.forEach((dir) => populateResultSet(dir));
|
|
|
|
|
if (node.component) {
|
|
|
|
|
populateResultSet(node.component);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
directiveProperties,
|
|
|
|
|
};
|
|
|
|
|
};
|
2020-01-27 18:40:18 +00:00
|
|
|
|
2023-10-05 08:00:31 +00:00
|
|
|
export function serializeElementInjectorWithId(injector: Injector): SerializedInjector|null {
|
|
|
|
|
let id: string;
|
|
|
|
|
const element = getElementInjectorElement(injector);
|
|
|
|
|
|
|
|
|
|
if (!injectorToId.has(element)) {
|
|
|
|
|
id = getInjectorId();
|
|
|
|
|
injectorToId.set(element, id);
|
|
|
|
|
idToInjector.set(id, injector);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
id = injectorToId.get(element)!;
|
|
|
|
|
idToInjector.set(id, injector);
|
|
|
|
|
injectorsSeen.add(id);
|
|
|
|
|
|
|
|
|
|
const serializedInjector = serializeInjector(injector);
|
|
|
|
|
if (serializedInjector === null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {id, ...serializedInjector};
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-02 07:03:41 +00:00
|
|
|
export function serializeInjectorWithId(injector: Injector): SerializedInjector|null {
|
|
|
|
|
if (isElementInjector(injector)) {
|
|
|
|
|
return serializeElementInjectorWithId(injector);
|
|
|
|
|
} else {
|
|
|
|
|
return serializeEnvironmentInjectorWithId(injector);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-05 08:00:31 +00:00
|
|
|
export function serializeEnvironmentInjectorWithId(injector: Injector): SerializedInjector|null {
|
|
|
|
|
let id: string;
|
|
|
|
|
|
|
|
|
|
if (!injectorToId.has(injector)) {
|
|
|
|
|
id = getInjectorId();
|
|
|
|
|
injectorToId.set(injector, id);
|
|
|
|
|
idToInjector.set(id, injector);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
id = injectorToId.get(injector)!;
|
|
|
|
|
idToInjector.set(id, injector);
|
|
|
|
|
injectorsSeen.add(id);
|
|
|
|
|
|
|
|
|
|
const serializedInjector = serializeInjector(injector);
|
|
|
|
|
if (serializedInjector === null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {id, ...serializedInjector};
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-08 18:35:50 +00:00
|
|
|
const enum DirectiveMetadataKey {
|
|
|
|
|
INPUTS = 'inputs',
|
|
|
|
|
OUTPUTS = 'outputs',
|
|
|
|
|
ENCAPSULATION = 'encapsulation',
|
|
|
|
|
ON_PUSH = 'onPush',
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-16 01:03:46 +00:00
|
|
|
// 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.
|
2020-04-08 18:35:50 +00:00
|
|
|
export const getDirectiveMetadata = (dir: any): DirectiveMetadata => {
|
2021-04-16 22:39:16 +00:00
|
|
|
const getMetadata = (window as any).ng.getDirectiveMetadata;
|
|
|
|
|
if (getMetadata) {
|
|
|
|
|
const metadata = getMetadata(dir);
|
2021-04-16 01:03:46 +00:00
|
|
|
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`.
|
2020-04-08 18:35:50 +00:00
|
|
|
const safelyGrabMetadata = (key: DirectiveMetadataKey) => {
|
2020-03-20 13:53:46 +00:00
|
|
|
try {
|
2020-04-08 18:35:50 +00:00
|
|
|
return dir.constructor.ɵcmp ? dir.constructor.ɵcmp[key] : dir.constructor.ɵdir[key];
|
2020-03-20 13:53:46 +00:00
|
|
|
} catch {
|
2020-04-08 18:35:50 +00:00
|
|
|
console.warn(`Could not find metadata for key: ${key} in directive:`, dir);
|
|
|
|
|
return undefined;
|
2020-03-20 13:53:46 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
2020-04-08 18:35:50 +00:00
|
|
|
inputs: safelyGrabMetadata(DirectiveMetadataKey.INPUTS),
|
|
|
|
|
outputs: safelyGrabMetadata(DirectiveMetadataKey.OUTPUTS),
|
|
|
|
|
encapsulation: safelyGrabMetadata(DirectiveMetadataKey.ENCAPSULATION),
|
|
|
|
|
onPush: safelyGrabMetadata(DirectiveMetadataKey.ON_PUSH),
|
2020-03-20 13:53:46 +00:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2023-10-05 08:00:31 +00:00
|
|
|
export function getInjectorProviders(injector: Injector): ProviderRecord[] {
|
|
|
|
|
if (isNullInjector(injector)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ngDebug().ɵgetInjectorProviders(injector);
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
const getDependenciesForDirective = (
|
|
|
|
|
injector: Injector,
|
|
|
|
|
resolutionPath: {injector: Injector; providers: ProviderRecord[]}[],
|
|
|
|
|
directive: any,
|
|
|
|
|
): SerializedInjectedService[] => {
|
|
|
|
|
if (!ngDebugApiIsSupported('ɵgetDependenciesFromInjectable')) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
2023-11-02 05:50:48 +00:00
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
let dependencies: InjectedService[] = ngDebug()
|
|
|
|
|
.ɵgetDependenciesFromInjectable(
|
|
|
|
|
injector,
|
|
|
|
|
directive,
|
|
|
|
|
)
|
|
|
|
|
.dependencies;
|
|
|
|
|
const serializedInjectedServices: SerializedInjectedService[] = [];
|
|
|
|
|
|
|
|
|
|
let position = 0;
|
|
|
|
|
for (const dependency of dependencies) {
|
|
|
|
|
const providedIn = dependency.providedIn;
|
|
|
|
|
const foundInjectorIndex = resolutionPath.findIndex((node) => node.injector === providedIn);
|
|
|
|
|
|
|
|
|
|
if (foundInjectorIndex === -1) {
|
|
|
|
|
position++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2023-10-05 08:00:31 +00:00
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
const providers = resolutionPath[foundInjectorIndex].providers;
|
|
|
|
|
const foundProvider = providers.find((provider) => provider.token === dependency.token);
|
|
|
|
|
|
|
|
|
|
// the dependency resolution path is
|
|
|
|
|
// the path from the root injector to the injector that provided the dependency (1)
|
|
|
|
|
// +
|
|
|
|
|
// the import path from the providing injector to the feature module that provided the
|
|
|
|
|
// dependency (2)
|
|
|
|
|
const dependencyResolutionPath = [
|
|
|
|
|
// (1)
|
|
|
|
|
...resolutionPath.slice(0, foundInjectorIndex + 1)
|
|
|
|
|
.map((node) => serializeInjectorWithId(node.injector)),
|
|
|
|
|
|
|
|
|
|
// (2)
|
|
|
|
|
// We slice the import path to remove the first element because this is the same
|
|
|
|
|
// injector as the last injector in the resolution path.
|
|
|
|
|
...(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,
|
|
|
|
|
position: [position++],
|
|
|
|
|
resolutionPath: dependencyResolutionPath,
|
|
|
|
|
});
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serializedInjectedServices.push({
|
|
|
|
|
token: valueToLabel(dependency.token),
|
|
|
|
|
value: valueToLabel(dependency.value),
|
|
|
|
|
flags: dependency.flags,
|
|
|
|
|
position: [position++],
|
|
|
|
|
resolutionPath: dependencyResolutionPath,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return serializedInjectedServices;
|
|
|
|
|
};
|
2023-10-05 08:00:31 +00:00
|
|
|
|
|
|
|
|
export const valueToLabel = (value: any): string => {
|
|
|
|
|
if (isInjectionToken(value)) {
|
|
|
|
|
return `InjectionToken(${value['_desc']})`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof value === 'object') {
|
2023-11-02 05:50:48 +00:00
|
|
|
return stripUnderscore(value.constructor.name);
|
2023-10-05 08:00:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof value === 'function') {
|
2023-11-02 05:50:48 +00:00
|
|
|
return stripUnderscore(value.name);
|
2023-10-05 08:00:31 +00:00
|
|
|
}
|
|
|
|
|
|
2023-11-02 05:50:48 +00:00
|
|
|
return stripUnderscore(value);
|
2023-10-05 08:00:31 +00:00
|
|
|
};
|
|
|
|
|
|
2023-11-02 05:50:48 +00:00
|
|
|
function stripUnderscore(str: string): string {
|
|
|
|
|
if (str.startsWith('_')) {
|
|
|
|
|
return str.slice(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return str;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-05 08:00:31 +00:00
|
|
|
export function serializeInjector(injector: Injector): Omit<SerializedInjector, 'id'>|null {
|
|
|
|
|
const metadata = getInjectorMetadata(injector);
|
|
|
|
|
|
|
|
|
|
if (metadata === null) {
|
|
|
|
|
console.error('Angular DevTools: Could not serialize injector.', injector);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-02 05:50:48 +00:00
|
|
|
const providers = getInjectorProviders(injector).length;
|
|
|
|
|
|
|
|
|
|
if (metadata.type === 'null') {
|
|
|
|
|
return {type: 'null', name: 'Null Injector', providers: 0};
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-05 08:00:31 +00:00
|
|
|
if (metadata.type === 'element') {
|
|
|
|
|
const source = metadata.source! as HTMLElement;
|
2023-11-02 05:50:48 +00:00
|
|
|
const name = stripUnderscore(elementToDirectiveNames(source)[0]);
|
2023-10-05 08:00:31 +00:00
|
|
|
|
2023-11-02 05:50:48 +00:00
|
|
|
return {type: 'element', name, providers};
|
2023-10-05 08:00:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (metadata.type === 'environment') {
|
2023-11-02 05:50:48 +00:00
|
|
|
if ((injector as any).scopes instanceof Set) {
|
|
|
|
|
if ((injector as any).scopes.has('platform')) {
|
|
|
|
|
return {type: 'environment', name: 'Platform', providers};
|
|
|
|
|
}
|
2023-10-05 08:00:31 +00:00
|
|
|
|
2023-11-02 05:50:48 +00:00
|
|
|
if ((injector as any).scopes.has('root')) {
|
|
|
|
|
return {type: 'environment', name: 'Root', providers};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {type: 'environment', name: stripUnderscore(metadata.source as string), providers};
|
2023-10-05 08:00:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.error('Angular DevTools: Could not serialize injector.', injector);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function serializeProviderRecord(
|
2023-12-04 19:44:53 +00:00
|
|
|
providerRecord: ProviderRecord,
|
|
|
|
|
index: number,
|
|
|
|
|
hasImportPath = false,
|
|
|
|
|
): SerializedProviderRecord {
|
2023-10-05 08:00:31 +00:00
|
|
|
let type: 'type'|'class'|'value'|'factory'|'existing' = 'type';
|
|
|
|
|
let multi = false;
|
|
|
|
|
|
|
|
|
|
if (typeof providerRecord.provider === 'object') {
|
|
|
|
|
if ((providerRecord.provider as ClassProvider).useClass !== undefined) {
|
|
|
|
|
type = 'class';
|
|
|
|
|
} else if ((providerRecord.provider as ValueProvider).useValue !== undefined) {
|
|
|
|
|
type = 'value';
|
|
|
|
|
} else if ((providerRecord.provider as FactoryProvider).useFactory !== undefined) {
|
|
|
|
|
type = 'factory';
|
|
|
|
|
} else if ((providerRecord.provider as ExistingProvider).useExisting !== undefined) {
|
|
|
|
|
type = 'existing';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (providerRecord.provider.multi !== undefined) {
|
|
|
|
|
multi = providerRecord.provider.multi;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
const serializedProvider: {
|
|
|
|
|
token: string; type: typeof type; multi: boolean; isViewProvider: boolean; index: number;
|
|
|
|
|
importPath?: string[];
|
|
|
|
|
} = {
|
2023-10-05 08:00:31 +00:00
|
|
|
token: valueToLabel(providerRecord.token),
|
|
|
|
|
type,
|
|
|
|
|
multi,
|
|
|
|
|
isViewProvider: providerRecord.isViewProvider,
|
2023-12-04 19:44:53 +00:00
|
|
|
index,
|
2023-10-05 08:00:31 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (hasImportPath) {
|
2023-12-04 19:44:53 +00:00
|
|
|
serializedProvider['importPath'] = (providerRecord.importPath ?? [])
|
|
|
|
|
.map(
|
|
|
|
|
(injector) => valueToLabel(injector),
|
|
|
|
|
);
|
2023-10-05 08:00:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return serializedProvider;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function elementToDirectiveNames(element: HTMLElement): string[] {
|
|
|
|
|
const {component, directives} = getDirectivesFromElement(element);
|
2023-12-04 19:44:53 +00:00
|
|
|
return [component, ...directives]
|
|
|
|
|
.map((dir) => dir?.constructor?.name ?? '')
|
|
|
|
|
.filter((dir) => !!dir);
|
2023-10-05 08:00:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getElementInjectorElement(elementInjector: Injector): HTMLElement {
|
|
|
|
|
if (!isElementInjector(elementInjector)) {
|
|
|
|
|
throw new Error('Injector is not an element injector');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return getInjectorMetadata(elementInjector)!.source as HTMLElement;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export 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';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isNullInjector(injector: Injector) {
|
|
|
|
|
const metadata = getInjectorMetadata(injector);
|
|
|
|
|
return metadata !== null && metadata.type === 'null';
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-12 15:47:40 +00:00
|
|
|
const getRootLViewsHelper = (element: Element, rootLViews = new Set<any>()): Set<any> => {
|
2020-03-11 03:08:56 +00:00
|
|
|
if (!(element instanceof HTMLElement)) {
|
2020-03-12 15:47:40 +00:00
|
|
|
return rootLViews;
|
2020-03-11 03:08:56 +00:00
|
|
|
}
|
|
|
|
|
const lView = getLViewFromDirectiveOrElementInstance(element);
|
|
|
|
|
if (lView) {
|
2020-03-12 01:13:06 +00:00
|
|
|
rootLViews.add(lView);
|
2020-03-12 15:47:40 +00:00
|
|
|
return rootLViews;
|
2020-03-11 03:08:56 +00:00
|
|
|
}
|
|
|
|
|
// tslint:disable-next-line: prefer-for-of
|
|
|
|
|
for (let i = 0; i < element.children.length; i++) {
|
|
|
|
|
getRootLViewsHelper(element.children[i], rootLViews);
|
|
|
|
|
}
|
|
|
|
|
return rootLViews;
|
|
|
|
|
};
|
|
|
|
|
|
2021-03-03 23:39:16 +00:00
|
|
|
const getRoots = () => {
|
2023-12-04 19:44:53 +00:00
|
|
|
const roots = Array.from(
|
|
|
|
|
document.documentElement.querySelectorAll('[ng-version]'),
|
|
|
|
|
) as HTMLElement[];
|
2021-12-16 07:00:43 +00:00
|
|
|
|
2021-03-03 23:39:16 +00:00
|
|
|
const isTopLevel = (element: HTMLElement) => {
|
2021-12-09 05:44:17 +00:00
|
|
|
let parent: HTMLElement|null = element;
|
2021-12-16 07:00:43 +00:00
|
|
|
|
2021-04-16 22:39:16 +00:00
|
|
|
while (parent?.parentElement) {
|
|
|
|
|
parent = parent.parentElement;
|
2021-03-03 23:39:16 +00:00
|
|
|
if (parent.hasAttribute('ng-version')) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-12-16 07:00:43 +00:00
|
|
|
|
2021-03-03 23:39:16 +00:00
|
|
|
return true;
|
|
|
|
|
};
|
2021-12-16 07:00:43 +00:00
|
|
|
|
2021-03-03 23:39:16 +00:00
|
|
|
return roots.filter(isTopLevel);
|
2020-03-12 01:13:06 +00:00
|
|
|
};
|
|
|
|
|
|
2020-03-30 18:43:00 +00:00
|
|
|
export const buildDirectiveForest = (): ComponentTreeNode[] => {
|
2021-03-03 23:39:16 +00:00
|
|
|
const roots = getRoots();
|
|
|
|
|
return Array.prototype.concat.apply([], Array.from(roots).map(buildDirectiveTree));
|
2020-01-27 18:40:18 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Based on an ElementID we return a specific component node.
|
|
|
|
|
// If we can't find any, we return null.
|
2023-12-04 19:44:53 +00:00
|
|
|
export const queryDirectiveForest = (
|
|
|
|
|
position: ElementPosition,
|
|
|
|
|
forest: ComponentTreeNode[],
|
|
|
|
|
): ComponentTreeNode|null => {
|
|
|
|
|
if (!position.length) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
let node: null|ComponentTreeNode = null;
|
|
|
|
|
for (const i of position) {
|
|
|
|
|
node = forest[i];
|
|
|
|
|
if (!node) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
forest = node.children;
|
|
|
|
|
}
|
|
|
|
|
return node;
|
|
|
|
|
};
|
2020-02-07 19:43:49 +00:00
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
export const findNodeInForest = (
|
|
|
|
|
position: ElementPosition,
|
|
|
|
|
forest: ComponentTreeNode[],
|
|
|
|
|
): HTMLElement|null => {
|
|
|
|
|
const foundComponent: ComponentTreeNode|null = queryDirectiveForest(position, forest);
|
|
|
|
|
return foundComponent ? (foundComponent.nativeElement as HTMLElement) : null;
|
|
|
|
|
};
|
2020-02-07 19:43:49 +00:00
|
|
|
|
2023-12-04 19:44:53 +00:00
|
|
|
export const findNodeFromSerializedPosition = (
|
|
|
|
|
serializedPosition: string,
|
|
|
|
|
): ComponentTreeNode|null => {
|
|
|
|
|
const position: number[] = serializedPosition.split(',').map((index) => parseInt(index, 10));
|
|
|
|
|
return queryDirectiveForest(position, buildDirectiveForest());
|
|
|
|
|
};
|
2020-02-19 20:07:01 +00:00
|
|
|
|
2020-03-20 18:54:37 +00:00
|
|
|
export const updateState = (updatedStateData: UpdatedStateData): void => {
|
2020-03-11 15:40:42 +00:00
|
|
|
const ngd = ngDebug();
|
2020-03-30 18:43:00 +00:00
|
|
|
const node = queryDirectiveForest(updatedStateData.directiveId.element, buildDirectiveForest());
|
2020-03-20 18:54:37 +00:00
|
|
|
if (!node) {
|
2021-12-09 05:44:17 +00:00
|
|
|
console.warn(
|
2023-12-04 19:44:53 +00:00
|
|
|
'Could not update the state of component',
|
|
|
|
|
updatedStateData,
|
|
|
|
|
'because the component was not found',
|
|
|
|
|
);
|
2020-03-20 18:54:37 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (updatedStateData.directiveId.directive !== undefined) {
|
2020-02-19 20:07:01 +00:00
|
|
|
const directive = node.directives[updatedStateData.directiveId.directive].instance;
|
|
|
|
|
mutateComponentOrDirective(updatedStateData, directive);
|
2020-03-11 15:40:42 +00:00
|
|
|
ngd.applyChanges(ngd.getOwningComponent(directive));
|
2020-03-20 18:54:37 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (node.component) {
|
|
|
|
|
const comp = node.component.instance;
|
|
|
|
|
mutateComponentOrDirective(updatedStateData, comp);
|
|
|
|
|
ngd.applyChanges(comp);
|
|
|
|
|
return;
|
2020-02-19 20:07:01 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2020-03-20 18:54:37 +00:00
|
|
|
const mutateComponentOrDirective = (updatedStateData: UpdatedStateData, compOrDirective: any) => {
|
2020-02-19 20:07:01 +00:00
|
|
|
const valueKey = updatedStateData.keyPath.pop();
|
2020-03-20 18:54:37 +00:00
|
|
|
if (valueKey === undefined) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-02-19 20:07:01 +00:00
|
|
|
|
|
|
|
|
let parentObjectOfValueToUpdate = compOrDirective;
|
2020-03-27 22:59:03 +00:00
|
|
|
updatedStateData.keyPath.forEach((key) => {
|
2020-02-19 20:07:01 +00:00
|
|
|
parentObjectOfValueToUpdate = parentObjectOfValueToUpdate[key];
|
|
|
|
|
});
|
2020-02-24 17:38:22 +00:00
|
|
|
|
2021-05-03 17:38:30 +00:00
|
|
|
// When we try to set a property which only has a getter
|
|
|
|
|
// the line below could throw an error.
|
|
|
|
|
try {
|
|
|
|
|
parentObjectOfValueToUpdate[valueKey] = updatedStateData.newValue;
|
2021-12-09 05:44:17 +00:00
|
|
|
} catch {
|
|
|
|
|
}
|
2020-02-19 20:07:01 +00:00
|
|
|
};
|
2023-11-02 07:03:41 +00:00
|
|
|
|
|
|
|
|
export function serializeResolutionPath(resolutionPath: Injector[]): SerializedInjector[] {
|
|
|
|
|
const serializedResolutionPath: SerializedInjector[] = [];
|
|
|
|
|
|
|
|
|
|
for (const injector of resolutionPath) {
|
|
|
|
|
let serializedInjectorWithId: SerializedInjector|null = null;
|
|
|
|
|
|
|
|
|
|
if (isElementInjector(injector)) {
|
|
|
|
|
serializedInjectorWithId = serializeElementInjectorWithId(injector);
|
|
|
|
|
} else {
|
|
|
|
|
serializedInjectorWithId = serializeEnvironmentInjectorWithId(injector);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (serializedInjectorWithId === null) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serializedResolutionPath.push(serializedInjectorWithId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return serializedResolutionPath;
|
|
|
|
|
}
|