Skip to content

Commit

Permalink
fix(client): error type conversion on editing (#188)
Browse files Browse the repository at this point in the history
  • Loading branch information
Azurewarth0920 authored Jan 23, 2024
1 parent 63da9fe commit d50877a
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<script setup lang="ts">
import { VueButton, VueIcon, VueInput, VTooltip as vTooltip } from '@vue/devtools-ui'
import { debounce } from 'perfect-debounce'
import { toSubmit } from '@vue/devtools-kit'
const props = withDefaults(defineProps<{
modelValue: string
Expand Down Expand Up @@ -33,30 +35,19 @@ const value = useVModel(props, 'modelValue', emit)
function tryToParseJSONString(v: unknown) {
try {
JSON.parse(v as string)
toSubmit(v as string)
return true
}
catch {
return false
}
}
const isWarning = computed(() =>
// warning if is empty or is NaN if is a numeric value
// or is not a valid Object if is an object
value.value.trim().length === 0
|| (
props.type === 'number'
? Number.isNaN(Number(value.value))
: false
)
// @TODO: maybe a better way to check? use JSON.parse is not a performance-friendly way
|| (
props.type === 'object'
? !tryToParseJSONString(value.value)
: false
),
)
const isWarning = ref(false)
const checkWarning = () => debounce(() => {
isWarning.value = !tryToParseJSONString(value.value)
}, 300)
watch(value, checkWarning())
</script>

<template>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { InspectorCustomState, InspectorState, InspectorStateEditorPayload } from '@vue/devtools-kit'
import { isArray, isObject, sortByKey } from '@vue/devtools-shared'
import { formatInspectorStateValue, getInspectorStateValueType, getRawValue } from '@vue/devtools-kit'
import { formatInspectorStateValue, getInspectorStateValueType, getRawValue, toEdit, toSubmit } from '@vue/devtools-kit'
import { useDevToolsBridgeRpc } from '@vue/devtools-core'
import { VueButton, VueIcon, VTooltip as vTooltip } from '@vue/devtools-ui'
import Actions from './InspectorDataField/Actions.vue'
Expand Down Expand Up @@ -113,9 +113,7 @@ const { editingType, editing, editingText, toggleEditing, nodeId } = useStateEdi
watch(() => editing.value, (v) => {
if (v) {
const { value } = rawValue.value
editingText.value = typeof value === 'object'
? JSON.stringify(value)
: value.toString()
editingText.value = toEdit(value)
}
else {
editingText.value = ''
Expand All @@ -132,7 +130,7 @@ function submit(dataType: string) {
state: {
newKey: null!,
type: dataType,
value: editingText.value,
value: toSubmit(editingText.value),
},
} satisfies InspectorStateEditorPayload)
toggleEditing()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,52 @@ describe('format: displayText and rawValue can be calculated by formatInspectorS
})
})
})

describe('format: toEdit', () => {
// eslint-disable-next-line test/consistent-test-it
test.each([
{ value: 123, target: '123' },
{ value: 'string-value', target: '"string-value"' },
{ value: true, target: 'true' },
{ value: null, target: 'null' },
// Tokenlized values
{ value: INFINITY, target: 'Infinity' },
{ value: NAN, target: 'NaN' },
{ value: NEGATIVE_INFINITY, target: '-Infinity' },
{ value: UNDEFINED, target: 'undefined' },
// Object that has tokenlized values
{ value: { foo: INFINITY }, target: '{"foo":Infinity}' },
{ value: { foo: NAN }, target: '{"foo":NaN}' },
{ value: { foo: NEGATIVE_INFINITY }, target: '{"foo":-Infinity}' },
{ value: { foo: UNDEFINED }, target: '{"foo":undefined}' },
])('value: $value will be deserialized to target', (value) => {
const deserialized = format.toEdit(value.value)
expect(deserialized).toBe(value.target)
})
})

describe('format: toSubmit', () => {
// eslint-disable-next-line test/consistent-test-it
test.each([
{ value: '123', target: 123 },
{ value: '"string-value"', target: 'string-value' },
{ value: 'true', target: true },
{ value: 'null', target: null },
// Tokenlized values
{ value: 'Infinity', target: Number.POSITIVE_INFINITY },
{ value: 'NaN', target: Number.NaN },
{ value: '-Infinity', target: Number.NEGATIVE_INFINITY },
{ value: 'undefined', target: undefined },
// // Object that has tokenlized values
{ value: '{"foo":Infinity}', target: { foo: Number.POSITIVE_INFINITY } },
{ value: '{"foo":NaN}', target: { foo: Number.NaN } },
{ value: '{"foo":-Infinity}', target: { foo: Number.NEGATIVE_INFINITY } },
// when serializing { key: undefined }, the key will be removed.
{ value: '{"foo":undefined}', target: {} },
// Regex test: The token in key field kept untouched.
{ value: '{"undefined": NaN }', target: { undefined: Number.NaN } },
])('value: $value will be serialized to target', (value) => {
const serialized = format.toSubmit(value.value)
expect(serialized).toStrictEqual(value.target)
})
})
5 changes: 2 additions & 3 deletions packages/devtools-kit/src/core/component/state/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,8 @@ class RefStateEditor {
else {
// if is reactive, then it must be object
// to prevent loss reactivity, we should assign key by key
const obj = JSON.parse(value)
const previousKeys = Object.keys(ref)
const currentKeys = Object.keys(obj)
const currentKeys = Object.keys(value)
// we should check the key diffs, if previous key is the longer
// then remove the needless keys
// @TODO: performance optimization
Expand All @@ -107,7 +106,7 @@ class RefStateEditor {
diffKeys.forEach(key => Reflect.deleteProperty(ref, key))
}
currentKeys.forEach((key) => {
Reflect.set(ref, key, Reflect.get(obj, key))
Reflect.set(ref, key, Reflect.get(value, key))
})
}
}
Expand Down
18 changes: 14 additions & 4 deletions packages/devtools-kit/src/core/component/state/format.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { InspectorCustomState, InspectorState } from '../types'
import { INFINITY, NAN, NEGATIVE_INFINITY, UNDEFINED, rawTypeRE, specialTypeRE } from './constants'
import { isPlainObject } from './is'
import { escape, internalStateTokenToString } from './util'
import { escape, internalStateTokenToString, replaceStringToToken, replaceTokenToString } from './util'
import { reviver } from './reviver'

export function getInspectorStateValueType(value, raw = true) {
const type = typeof value
Expand Down Expand Up @@ -91,11 +92,12 @@ export function getRawValue(value: InspectorState['value']) {
let inherit = {}
if (isCustom) {
const data = value as InspectorCustomState
const nestedCustom = typeof data._custom?.value === 'object' && '_custom' in data._custom.value
? getRawValue(data._custom?.value)
const customValue = data._custom?.value
const nestedCustom = typeof customValue === 'object' && customValue !== null && '_custom' in customValue
? getRawValue(customValue)
: { inherit: undefined, value: undefined }
inherit = nestedCustom.inherit || data._custom?.fields || {}
value = nestedCustom.value || data._custom?.value as string
value = nestedCustom.value || customValue as string
}
// @ts-expect-error @TODO: type
if (value && value._isArray)
Expand All @@ -104,3 +106,11 @@ export function getRawValue(value: InspectorState['value']) {

return { value, inherit }
}

export function toEdit(value: unknown) {
return replaceTokenToString(JSON.stringify(value))
}

export function toSubmit(value: string) {
return JSON.parse(replaceStringToToken(value), reviver)
}
35 changes: 25 additions & 10 deletions packages/devtools-kit/src/core/component/state/util.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import { ESC, INFINITY, NAN, NEGATIVE_INFINITY, UNDEFINED, fnTypeRE } from './constants'
import { isComputed, isPlainObject, isPrimitive, isReactive, isReadOnly, isRef } from './is'

export const tokenMap = {
[UNDEFINED]: 'undefined',
[NAN]: 'NaN',
[INFINITY]: 'Infinity',
[NEGATIVE_INFINITY]: '-Infinity',
} as const

export const reversedTokenMap = Object.entries(tokenMap).reduce((acc, [key, value]) => {
acc[value] = key
return acc
}, {})

export function internalStateTokenToString(value: unknown) {
if (value === null)
return 'null'

else if (value === UNDEFINED)
return 'undefined'

else if (value === NAN)
return 'NaN'
return (typeof value === 'string' && tokenMap[value]) || false
}

else if (value === INFINITY)
return 'Infinity'
export function replaceTokenToString(value: string) {
const replaceRegex = new RegExp(`"(${Object.keys(tokenMap).join('|')})"`, 'g')
return value.replace(replaceRegex, (_, g1) => tokenMap[g1])
}

else if (value === NEGATIVE_INFINITY)
return '-Infinity'
export function replaceStringToToken(value: string) {
const literalValue = reversedTokenMap[value.trim()]
if (literalValue)
return `"${literalValue}"`

return false
// Match the token in value field and replace it with the literal value.
const replaceRegex = new RegExp(`:\\s*(${Object.keys(reversedTokenMap).join('|')})`, 'g')
return value.replace(replaceRegex, (_, g1) => `:"${reversedTokenMap[g1]}"`)
}

/**
Expand Down
7 changes: 1 addition & 6 deletions packages/devtools-kit/src/shared/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { formatInspectorStateValue, getInspectorStateValueType, getRawValue } from '../core/component/state/format'
import { stringifyReplacer } from '../core/component/state/replacer'
import { reviver } from '../core/component/state/reviver'
import { parseCircularAutoChunks, stringifyCircularAutoChunks } from './transfer'
Expand All @@ -16,8 +15,4 @@ export function parse(data: string, revive = false) {
: parseCircularAutoChunks(data)
}

export {
formatInspectorStateValue,
getInspectorStateValueType,
getRawValue,
}
export * from '../core/component/state/format'

0 comments on commit d50877a

Please sign in to comment.