refactor(devtools): separate object creation concern from state serializer api (rangle/angular-devtools#69)

This commit is contained in:
AleksanderBodurri 2020-02-19 13:28:41 -05:00 committed by GitHub
parent f06aedc28c
commit ec1c95cbdb
9 changed files with 406 additions and 274 deletions

View file

@ -8,8 +8,8 @@ import {
prepareForestForSerialization,
} from './component-tree';
import { start as startProfiling, stop as stopProfiling } from './recording';
import { serializeComponentState } from './state-serializer';
import { ComponentInspector, ComponentInspectorOptions } from './component-inspector';
import { serializeComponentState } from './state-serializer/state-serializer';
import { ComponentInspector, ComponentInspectorOptions } from './component-inspector/component-inspector';
import { setConsoleReference } from './selected-component';
import { unHighlight } from './highlighter';
import {

View file

@ -1,13 +1,13 @@
import { unHighlight, highlight, findComponentAndHost } from './highlighter';
import { unHighlight, highlight, findComponentAndHost } from '../highlighter';
import { Type } from '@angular/core';
import {
getDirectiveForest,
ComponentTreeNode,
findNodeInForest,
getIndexForNativeElementInForest,
} from './component-tree';
} from '../component-tree';
import { ElementID } from 'protocol';
import { indexForest, IndexedNode } from './recording/observer';
import { indexForest, IndexedNode } from '../recording/observer';
export interface ComponentInspectorOptions {
onComponentEnter: (id: ElementID) => void;

View file

@ -1,4 +1,4 @@
import { deeplySerializeSelectedProperties } from './state-serializer';
import { deeplySerializeSelectedProperties } from './state-serializer/state-serializer';
declare const ng: any;
@ -36,6 +36,7 @@ export const getLatestComponentState = (query: ComponentExplorerViewQuery): Dire
if (!node) {
return undefined;
}
result = {};
node.directives.forEach(dir => {
if (!query.expandedProperties[dir.name]) {
@ -45,6 +46,7 @@ export const getLatestComponentState = (query: ComponentExplorerViewQuery): Dire
props: deeplySerializeSelectedProperties(dir.instance, query.expandedProperties[dir.name]),
};
});
if (node.component) {
if (!query.expandedProperties[node.component.name]) {
return;

View file

@ -1,7 +1,7 @@
import { RecorderComponent } from './observer';
import { AppRecord, ComponentEventType, LifeCycleEventType } from 'protocol';
import { getComponentName } from '../highlighter';
import { serializeComponentState } from '../state-serializer';
import { serializeComponentState } from '../state-serializer/state-serializer';
export interface RecordFactoryOptions {
eventType: ComponentEventType | LifeCycleEventType;

View file

@ -1,267 +0,0 @@
import { Descriptor, PropType, NestedProp } from 'protocol';
const ignoreList = new Set(['__ngContext__', '__ngSimpleChanges__']);
const commonTypes = {
boolean: PropType.Boolean,
bigint: PropType.BigInt,
function: PropType.Function,
number: PropType.Number,
string: PropType.String,
symbol: PropType.Symbol,
};
const MAX_LEVEL = 1;
const getDescriptorPreview = (type: PropType, prop: any): string => {
switch (type) {
case PropType.Array:
return `Array(${prop.length})`;
case PropType.BigInt:
case PropType.Boolean:
return truncate(prop.toString());
case PropType.String:
return `"${prop}"`;
case PropType.Function:
return `${prop.name}(...)`;
case PropType.HTMLElement:
return prop.constructor.name;
case PropType.Null:
return 'null';
case PropType.Number:
return parseInt(prop, 10).toString();
case PropType.Object:
return Object.keys(prop).length > 0 ? '{...}' : '{}';
case PropType.Symbol:
return 'Symbol()';
case PropType.Undefined:
return 'undefined';
case PropType.Unknown:
return 'unknown';
}
return '';
};
const getPropType = (prop: any): PropType => {
if (prop === undefined) {
return PropType.Undefined;
}
if (prop === null) {
return PropType.Null;
}
if (prop instanceof HTMLElement) {
return PropType.HTMLElement;
}
const type = typeof prop;
if (commonTypes[type] !== undefined) {
return commonTypes[type];
}
if (type === 'object') {
if (Array.isArray(prop)) {
return PropType.Array;
} else if (Object.prototype.toString.call(prop) === '[object Date]') {
return PropType.Date;
} else {
return PropType.Object;
}
}
console.log(type, prop);
return PropType.Unknown;
};
const truncate = (str: string, max = 20): string => {
if (str.length > max) {
return str.substr(0, max) + '...';
}
return str;
};
const serializeShallowProperty = (prop: any): Descriptor => {
const type = getPropType(prop);
switch (type) {
case PropType.BigInt:
return {
type,
value: truncate(prop.toString()),
editable: false,
expandable: false,
preview: getDescriptorPreview(type, prop),
};
case PropType.String:
return {
type,
value: truncate(prop.toString()),
editable: true,
expandable: false,
preview: getDescriptorPreview(type, prop),
};
case PropType.Boolean:
case PropType.Number:
case PropType.Date:
return {
type,
value: prop,
editable: true,
expandable: false,
preview: getDescriptorPreview(type, prop),
};
case PropType.Null:
case PropType.Undefined:
return {
type,
editable: true,
expandable: false,
preview: getDescriptorPreview(type, prop),
};
case PropType.Symbol:
case PropType.Function:
case PropType.HTMLElement:
case PropType.Unknown: {
return {
type,
editable: false,
expandable: false,
preview: getDescriptorPreview(type, prop),
};
}
}
};
export const nestedSerializer = (
serializableInstance: any,
nodes: NestedProp[],
currentLevel = 0,
level = MAX_LEVEL
): Descriptor => {
const type: PropType = getPropType(serializableInstance);
if (currentLevel < level) {
return levelSerializer(
serializableInstance,
undefined,
currentLevel,
level,
(nestedProp: any, propName: string | number | undefined, nestedLevel: number) => {
const idx = nodes.findIndex(v => v.name === propName);
if (idx < 0) {
// The property is not specified in the query.
return nestedSerializer(nestedProp, [], nestedLevel, level);
}
return nestedSerializer(nestedProp, nodes[idx].children, nestedLevel, level);
}
);
} else {
switch (type) {
case PropType.Array:
const arr: Descriptor = {
type,
editable: true,
expandable: serializableInstance.length > 0,
preview: getDescriptorPreview(type, serializableInstance),
};
if (nodes && nodes.length) {
arr.value = nodes.map(c => nestedSerializer(serializableInstance[c.name], c.children, currentLevel + 1));
}
return arr;
case PropType.Object:
const obj: Descriptor = {
type,
editable: true,
expandable: Object.keys(serializableInstance).length > 0,
preview: getDescriptorPreview(type, serializableInstance),
};
if (nodes && nodes.length) {
obj.value = nodes.reduce((accum, c) => {
if (serializableInstance.hasOwnProperty(c.name) && typeof c.name === 'string' && !ignoreList.has(c.name)) {
accum[c.name] = nestedSerializer(serializableInstance[c.name], c.children, currentLevel + 1);
}
return accum;
}, {});
}
return obj;
default:
return serializeShallowProperty(serializableInstance);
}
}
};
export const levelSerializer = (
serializableInstance: any,
_: string | number | undefined = undefined,
currentLevel = 0,
level = MAX_LEVEL,
continuation = levelSerializer
): Descriptor => {
const type = getPropType(serializableInstance);
switch (type) {
case PropType.Array:
if (currentLevel < level) {
return {
type,
value: serializableInstance.map((nested: any, idx: number) =>
continuation(nested, idx, currentLevel + 1, level)
),
editable: true,
expandable: serializableInstance.length > 0,
preview: getDescriptorPreview(type, serializableInstance),
};
}
return {
type,
editable: true,
expandable: serializableInstance.length > 0,
preview: getDescriptorPreview(type, serializableInstance),
};
case PropType.Object:
if (currentLevel < level) {
return {
type,
value: Object.keys(serializableInstance).reduce((prev, key) => {
if (typeof key === 'string' && !ignoreList.has(key)) {
prev[key] = continuation(serializableInstance[key], key, currentLevel + 1, level);
}
return prev;
}, {}),
editable: true,
expandable: Object.keys(serializableInstance).length > 0,
preview: getDescriptorPreview(type, serializableInstance),
};
}
return {
type,
editable: true,
expandable: Object.keys(serializableInstance).length > 0,
preview: getDescriptorPreview(type, serializableInstance),
};
default:
return serializeShallowProperty(serializableInstance);
}
};
export const serializeComponentState = (instance: object, levels = MAX_LEVEL): { [key: string]: Descriptor } => {
const result = {};
for (const prop in instance) {
if (instance.hasOwnProperty(prop) && !ignoreList.has(prop)) {
result[prop] = levelSerializer(instance[prop], null, 0, levels);
}
}
return result;
};
export const deeplySerializeSelectedProperties = (
instance: any,
props: NestedProp[]
): { [name: string]: Descriptor } => {
const result = {};
Object.keys(instance).forEach(propName => {
if (ignoreList.has(propName)) {
return;
}
const idx = props.findIndex(v => v.name === propName);
if (idx < 0) {
result[propName] = levelSerializer(instance[propName]);
} else {
result[propName] = nestedSerializer(instance[propName], props[idx].children);
}
});
return result;
};

View file

@ -0,0 +1,265 @@
import { Descriptor, NestedProp, PropType } from 'protocol';
interface PropData {
type: PropType;
prop: any;
}
interface LevelOptions {
currentLevel: number;
level?: number;
}
interface CommonTypeCases {
arrayCase?: () => any;
bigIntCase?: () => any;
booleanCase?: () => any;
stringCase?: () => any;
dateCase?: () => any;
functionCase?: () => any;
htmlElementCase?: () => any;
nullCase?: () => any;
numberCase?: () => any;
objectCase?: () => any;
symbolCase?: () => any;
undefinedCase?: () => any;
unknownCase?: () => any;
}
const ignoreList = new Set(['__ngContext__', '__ngSimpleChanges__']);
const shallowPropTypeToTreeMetaData = {
[PropType.String]: {
editable: true,
expandable: false,
},
[PropType.BigInt]: {
editable: false,
expandable: false,
},
[PropType.Boolean]: {
editable: true,
expandable: false,
},
[PropType.Number]: {
editable: true,
expandable: false,
},
[PropType.Date]: {
editable: true,
expandable: false,
},
[PropType.Null]: {
editable: true,
expandable: false,
},
[PropType.Undefined]: {
editable: true,
expandable: false,
},
[PropType.Symbol]: {
editable: false,
expandable: false,
},
[PropType.Function]: {
editable: false,
expandable: false,
},
[PropType.HTMLElement]: {
editable: false,
expandable: false,
},
[PropType.Unknown]: {
editable: false,
expandable: false,
},
};
export const createShallowSerializedDescriptor = (propData: PropData): Descriptor => {
const { type } = propData;
const shallowSerializedDescriptor: Descriptor = {
type,
expandable: shallowPropTypeToTreeMetaData[type].expandable,
editable: shallowPropTypeToTreeMetaData[type].editable,
preview: getDescriptorPreview(propData),
};
const value = getShallowDescriptorValue(propData);
if (value !== undefined) {
shallowSerializedDescriptor.value = value;
}
return shallowSerializedDescriptor;
};
export const createLevelSerializedDescriptor = (
propData: PropData,
levelOptions: LevelOptions,
continuation: any
): Descriptor => {
const { type, prop } = propData;
const levelSerializedDescriptor: Descriptor = {
type,
editable: true,
expandable: Object.keys(prop).length > 0,
preview: getDescriptorPreview(propData),
};
if (levelOptions.currentLevel < levelOptions.level) {
const value = getLevelDescriptorValue(propData, levelOptions, continuation);
if (value !== undefined) {
levelSerializedDescriptor.value = value;
}
}
return levelSerializedDescriptor;
};
export const createNestedSerializedDescriptor = (
propData: PropData,
levelOptions: LevelOptions,
nodes: NestedProp[],
nestedSerializer: any
): Descriptor => {
const { type, prop } = propData;
const nestedSerializedDescriptor: Descriptor = {
type,
editable: true,
expandable: Object.keys(prop).length > 0,
preview: getDescriptorPreview(propData),
};
if (nodes && nodes.length) {
const value = getNestedDescriptorValue(propData, levelOptions, nodes, nestedSerializer);
if (value !== undefined) {
nestedSerializedDescriptor.value = value;
}
}
return nestedSerializedDescriptor;
};
const getDataByType = (type: PropType, valueByType: CommonTypeCases, defaultValue?: any) => {
try {
switch (type) {
case PropType.Array:
return valueByType.arrayCase();
case PropType.BigInt:
return valueByType.bigIntCase();
case PropType.Boolean:
return valueByType.booleanCase();
case PropType.String:
return valueByType.stringCase();
case PropType.Function:
return valueByType.functionCase();
case PropType.Date:
return valueByType.dateCase();
case PropType.HTMLElement:
return valueByType.htmlElementCase();
case PropType.Null:
return valueByType.nullCase();
case PropType.Number:
return valueByType.numberCase();
case PropType.Object:
return valueByType.objectCase();
case PropType.Symbol:
return valueByType.symbolCase();
case PropType.Undefined:
return valueByType.undefinedCase();
case PropType.Unknown:
return valueByType.unknownCase();
}
} catch {
return defaultValue;
}
};
const getDescriptorPreview = (propData: PropData) => {
const { type, prop } = propData;
return getDataByType(
type,
{
arrayCase: () => `Array(${prop.length})`,
bigIntCase: () => truncate(prop.toString()),
booleanCase: () => truncate(prop.toString()),
stringCase: () => `"${prop}"`,
functionCase: () => `${prop.name}(...)`,
htmlElementCase: () => prop.constructor.name,
nullCase: () => 'null',
numberCase: () => parseInt(prop, 10).toString(),
objectCase: () => (Object.keys(prop).length > 0 ? '{...}' : '{}'),
symbolCase: () => 'Symbol()',
undefinedCase: () => 'undefined',
unknownCase: () => 'unknown',
},
''
);
};
const getShallowDescriptorValue = (propData: PropData) => {
const { type, prop } = propData;
return getDataByType(
type,
{
bigIntCase: () => prop,
booleanCase: () => prop,
stringCase: () => prop,
dateCase: () => prop,
numberCase: () => prop,
},
undefined
);
};
const getNestedDescriptorValue = (
propData: PropData,
levelOptions: LevelOptions,
nodes: NestedProp[],
nestedSerializer: any
) => {
const { type, prop } = propData;
const { currentLevel } = levelOptions;
return getDataByType(type, {
arrayCase: () =>
nodes.map(nestedProp => nestedSerializer(prop[nestedProp.name], nestedProp.children, currentLevel + 1)),
objectCase: () =>
nodes.reduce((accumulator, nestedProp) => {
if (
prop.hasOwnProperty(nestedProp.name) &&
typeof nestedProp.name === 'string' &&
!ignoreList.has(nestedProp.name)
) {
accumulator[nestedProp.name] = nestedSerializer(prop[nestedProp.name], nestedProp.children, currentLevel + 1);
}
return accumulator;
}, {}),
});
};
const getLevelDescriptorValue = (propData: PropData, levelOptions: LevelOptions, continuation: any) => {
const { type, prop } = propData;
const { currentLevel, level } = levelOptions;
return getDataByType(type, {
arrayCase: () => prop.map((nested: any, idx: number) => continuation(nested, idx, currentLevel + 1, level)),
objectCase: () =>
Object.keys(prop).reduce((accumulator, propName) => {
if (typeof propName === 'string' && !ignoreList.has(propName)) {
accumulator[propName] = continuation(prop[propName], propName, currentLevel + 1, level);
}
return accumulator;
}, {}),
});
};
const truncate = (str: string, max = 20): string => {
if (str.length > max) {
return str.substr(0, max) + '...';
}
return str;
};

View file

@ -0,0 +1,132 @@
import { Descriptor, NestedProp, PropType } from 'protocol';
import {
createLevelSerializedDescriptor,
createNestedSerializedDescriptor,
createShallowSerializedDescriptor,
} from './serialized-descriptor-factory';
const ignoreList = new Set(['__ngContext__', '__ngSimpleChanges__']);
const commonTypes = {
boolean: PropType.Boolean,
bigint: PropType.BigInt,
function: PropType.Function,
number: PropType.Number,
string: PropType.String,
symbol: PropType.Symbol,
};
const MAX_LEVEL = 1;
const getPropType = (prop: any): PropType => {
if (prop === undefined) {
return PropType.Undefined;
}
if (prop === null) {
return PropType.Null;
}
if (prop instanceof HTMLElement) {
return PropType.HTMLElement;
}
const type = typeof prop;
if (commonTypes[type] !== undefined) {
return commonTypes[type];
}
if (type === 'object') {
if (Array.isArray(prop)) {
return PropType.Array;
} else if (Object.prototype.toString.call(prop) === '[object Date]') {
return PropType.Date;
} else {
return PropType.Object;
}
}
return PropType.Unknown;
};
export const nestedSerializer = (
serializableInstance: any,
nodes: NestedProp[],
currentLevel = 0,
level = MAX_LEVEL
): Descriptor => {
const type: PropType = getPropType(serializableInstance);
const propData = { prop: serializableInstance, type };
const levelOptions = { level, currentLevel };
if (currentLevel < level) {
return levelSerializer(
serializableInstance,
undefined,
currentLevel,
level,
nestedSerializerContinuation(nodes, level)
);
}
if (type === PropType.Array || type === PropType.Object) {
return createNestedSerializedDescriptor(propData, levelOptions, nodes, nestedSerializer);
}
return createShallowSerializedDescriptor(propData);
};
const nestedSerializerContinuation = (nodes: NestedProp[], level: number) => (
nestedProp: any,
propName: string | number | undefined,
nestedLevel: number
) => {
const idx = nodes.findIndex(v => v.name === propName);
if (idx < 0) {
// The property is not specified in the query.
return nestedSerializer(nestedProp, [], nestedLevel, level);
}
return nestedSerializer(nestedProp, nodes[idx].children, nestedLevel, level);
};
export const levelSerializer = (
serializableInstance: any,
_: string | number | undefined = undefined,
currentLevel = 0,
level = MAX_LEVEL,
continuation = levelSerializer
): Descriptor => {
const type = getPropType(serializableInstance);
const propData = { prop: serializableInstance, type };
const levelOptions = { level, currentLevel };
if (type === PropType.Array || type === PropType.Object) {
return createLevelSerializedDescriptor(propData, levelOptions, continuation);
}
return createShallowSerializedDescriptor(propData);
};
export const serializeComponentState = (instance: object, levels = MAX_LEVEL): { [key: string]: Descriptor } => {
const result = {};
for (const prop in instance) {
if (instance.hasOwnProperty(prop) && !ignoreList.has(prop)) {
result[prop] = levelSerializer(instance[prop], null, 0, levels);
}
}
return result;
};
export const deeplySerializeSelectedProperties = (
instance: any,
props: NestedProp[]
): { [name: string]: Descriptor } => {
const result = {};
Object.keys(instance).forEach(propName => {
if (ignoreList.has(propName)) {
return;
}
const idx = props.findIndex(v => v.name === propName);
if (idx < 0) {
result[propName] = levelSerializer(instance[propName]);
} else {
result[propName] = nestedSerializer(instance[propName], props[idx].children);
}
});
return result;
};