diff --git a/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts b/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts index 1efaa9e847c..3534e871b9b 100644 --- a/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts +++ b/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts @@ -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 { diff --git a/projects/ng-devtools-backend/src/lib/component-inspector.spec.ts b/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.spec.ts similarity index 100% rename from projects/ng-devtools-backend/src/lib/component-inspector.spec.ts rename to projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.spec.ts diff --git a/projects/ng-devtools-backend/src/lib/component-inspector.ts b/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts similarity index 94% rename from projects/ng-devtools-backend/src/lib/component-inspector.ts rename to projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts index 618244db541..db48b8a5f24 100644 --- a/projects/ng-devtools-backend/src/lib/component-inspector.ts +++ b/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts @@ -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; diff --git a/projects/ng-devtools-backend/src/lib/component-tree.ts b/projects/ng-devtools-backend/src/lib/component-tree.ts index 56ae155b35a..3df05de13db 100644 --- a/projects/ng-devtools-backend/src/lib/component-tree.ts +++ b/projects/ng-devtools-backend/src/lib/component-tree.ts @@ -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; diff --git a/projects/ng-devtools-backend/src/lib/recording/record-factory.ts b/projects/ng-devtools-backend/src/lib/recording/record-factory.ts index 2a94fb18f71..1d58820800a 100644 --- a/projects/ng-devtools-backend/src/lib/recording/record-factory.ts +++ b/projects/ng-devtools-backend/src/lib/recording/record-factory.ts @@ -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; diff --git a/projects/ng-devtools-backend/src/lib/state-serializer.ts b/projects/ng-devtools-backend/src/lib/state-serializer.ts deleted file mode 100644 index 4aa64abcac3..00000000000 --- a/projects/ng-devtools-backend/src/lib/state-serializer.ts +++ /dev/null @@ -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; -}; diff --git a/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts b/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts new file mode 100644 index 00000000000..445b2b0cc2e --- /dev/null +++ b/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts @@ -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; +}; diff --git a/projects/ng-devtools-backend/src/lib/state-serializer.spec.ts b/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.spec.ts similarity index 100% rename from projects/ng-devtools-backend/src/lib/state-serializer.spec.ts rename to projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.spec.ts diff --git a/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.ts b/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.ts new file mode 100644 index 00000000000..1f17f517f6e --- /dev/null +++ b/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.ts @@ -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; +};