Skip to content

Commit d50877a

Browse files
fix(client): error type conversion on editing (#188)
1 parent 63da9fe commit d50877a

File tree

7 files changed

+102
-45
lines changed

7 files changed

+102
-45
lines changed

packages/client/src/components/inspector/InspectorDataField/EditInput.vue

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<script setup lang="ts">
22
import { VueButton, VueIcon, VueInput, VTooltip as vTooltip } from '@vue/devtools-ui'
3+
import { debounce } from 'perfect-debounce'
4+
import { toSubmit } from '@vue/devtools-kit'
35
46
const props = withDefaults(defineProps<{
57
modelValue: string
@@ -33,30 +35,19 @@ const value = useVModel(props, 'modelValue', emit)
3335
3436
function tryToParseJSONString(v: unknown) {
3537
try {
36-
JSON.parse(v as string)
38+
toSubmit(v as string)
3739
return true
3840
}
3941
catch {
4042
return false
4143
}
4244
}
4345
44-
const isWarning = computed(() =>
45-
// warning if is empty or is NaN if is a numeric value
46-
// or is not a valid Object if is an object
47-
value.value.trim().length === 0
48-
|| (
49-
props.type === 'number'
50-
? Number.isNaN(Number(value.value))
51-
: false
52-
)
53-
// @TODO: maybe a better way to check? use JSON.parse is not a performance-friendly way
54-
|| (
55-
props.type === 'object'
56-
? !tryToParseJSONString(value.value)
57-
: false
58-
),
59-
)
46+
const isWarning = ref(false)
47+
const checkWarning = () => debounce(() => {
48+
isWarning.value = !tryToParseJSONString(value.value)
49+
}, 300)
50+
watch(value, checkWarning())
6051
</script>
6152

6253
<template>

packages/client/src/components/inspector/InspectorStateField.vue

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import type { InspectorCustomState, InspectorState, InspectorStateEditorPayload } from '@vue/devtools-kit'
33
import { isArray, isObject, sortByKey } from '@vue/devtools-shared'
4-
import { formatInspectorStateValue, getInspectorStateValueType, getRawValue } from '@vue/devtools-kit'
4+
import { formatInspectorStateValue, getInspectorStateValueType, getRawValue, toEdit, toSubmit } from '@vue/devtools-kit'
55
import { useDevToolsBridgeRpc } from '@vue/devtools-core'
66
import { VueButton, VueIcon, VTooltip as vTooltip } from '@vue/devtools-ui'
77
import Actions from './InspectorDataField/Actions.vue'
@@ -113,9 +113,7 @@ const { editingType, editing, editingText, toggleEditing, nodeId } = useStateEdi
113113
watch(() => editing.value, (v) => {
114114
if (v) {
115115
const { value } = rawValue.value
116-
editingText.value = typeof value === 'object'
117-
? JSON.stringify(value)
118-
: value.toString()
116+
editingText.value = toEdit(value)
119117
}
120118
else {
121119
editingText.value = ''
@@ -132,7 +130,7 @@ function submit(dataType: string) {
132130
state: {
133131
newKey: null!,
134132
type: dataType,
135-
value: editingText.value,
133+
value: toSubmit(editingText.value),
136134
},
137135
} satisfies InspectorStateEditorPayload)
138136
toggleEditing()

packages/devtools-kit/src/core/component/state/__tests__/format.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,52 @@ describe('format: displayText and rawValue can be calculated by formatInspectorS
6969
})
7070
})
7171
})
72+
73+
describe('format: toEdit', () => {
74+
// eslint-disable-next-line test/consistent-test-it
75+
test.each([
76+
{ value: 123, target: '123' },
77+
{ value: 'string-value', target: '"string-value"' },
78+
{ value: true, target: 'true' },
79+
{ value: null, target: 'null' },
80+
// Tokenlized values
81+
{ value: INFINITY, target: 'Infinity' },
82+
{ value: NAN, target: 'NaN' },
83+
{ value: NEGATIVE_INFINITY, target: '-Infinity' },
84+
{ value: UNDEFINED, target: 'undefined' },
85+
// Object that has tokenlized values
86+
{ value: { foo: INFINITY }, target: '{"foo":Infinity}' },
87+
{ value: { foo: NAN }, target: '{"foo":NaN}' },
88+
{ value: { foo: NEGATIVE_INFINITY }, target: '{"foo":-Infinity}' },
89+
{ value: { foo: UNDEFINED }, target: '{"foo":undefined}' },
90+
])('value: $value will be deserialized to target', (value) => {
91+
const deserialized = format.toEdit(value.value)
92+
expect(deserialized).toBe(value.target)
93+
})
94+
})
95+
96+
describe('format: toSubmit', () => {
97+
// eslint-disable-next-line test/consistent-test-it
98+
test.each([
99+
{ value: '123', target: 123 },
100+
{ value: '"string-value"', target: 'string-value' },
101+
{ value: 'true', target: true },
102+
{ value: 'null', target: null },
103+
// Tokenlized values
104+
{ value: 'Infinity', target: Number.POSITIVE_INFINITY },
105+
{ value: 'NaN', target: Number.NaN },
106+
{ value: '-Infinity', target: Number.NEGATIVE_INFINITY },
107+
{ value: 'undefined', target: undefined },
108+
// // Object that has tokenlized values
109+
{ value: '{"foo":Infinity}', target: { foo: Number.POSITIVE_INFINITY } },
110+
{ value: '{"foo":NaN}', target: { foo: Number.NaN } },
111+
{ value: '{"foo":-Infinity}', target: { foo: Number.NEGATIVE_INFINITY } },
112+
// when serializing { key: undefined }, the key will be removed.
113+
{ value: '{"foo":undefined}', target: {} },
114+
// Regex test: The token in key field kept untouched.
115+
{ value: '{"undefined": NaN }', target: { undefined: Number.NaN } },
116+
])('value: $value will be serialized to target', (value) => {
117+
const serialized = format.toSubmit(value.value)
118+
expect(serialized).toStrictEqual(value.target)
119+
})
120+
})

packages/devtools-kit/src/core/component/state/editor.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,8 @@ class RefStateEditor {
9696
else {
9797
// if is reactive, then it must be object
9898
// to prevent loss reactivity, we should assign key by key
99-
const obj = JSON.parse(value)
10099
const previousKeys = Object.keys(ref)
101-
const currentKeys = Object.keys(obj)
100+
const currentKeys = Object.keys(value)
102101
// we should check the key diffs, if previous key is the longer
103102
// then remove the needless keys
104103
// @TODO: performance optimization
@@ -107,7 +106,7 @@ class RefStateEditor {
107106
diffKeys.forEach(key => Reflect.deleteProperty(ref, key))
108107
}
109108
currentKeys.forEach((key) => {
110-
Reflect.set(ref, key, Reflect.get(obj, key))
109+
Reflect.set(ref, key, Reflect.get(value, key))
111110
})
112111
}
113112
}

packages/devtools-kit/src/core/component/state/format.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { InspectorCustomState, InspectorState } from '../types'
22
import { INFINITY, NAN, NEGATIVE_INFINITY, UNDEFINED, rawTypeRE, specialTypeRE } from './constants'
33
import { isPlainObject } from './is'
4-
import { escape, internalStateTokenToString } from './util'
4+
import { escape, internalStateTokenToString, replaceStringToToken, replaceTokenToString } from './util'
5+
import { reviver } from './reviver'
56

67
export function getInspectorStateValueType(value, raw = true) {
78
const type = typeof value
@@ -91,11 +92,12 @@ export function getRawValue(value: InspectorState['value']) {
9192
let inherit = {}
9293
if (isCustom) {
9394
const data = value as InspectorCustomState
94-
const nestedCustom = typeof data._custom?.value === 'object' && '_custom' in data._custom.value
95-
? getRawValue(data._custom?.value)
95+
const customValue = data._custom?.value
96+
const nestedCustom = typeof customValue === 'object' && customValue !== null && '_custom' in customValue
97+
? getRawValue(customValue)
9698
: { inherit: undefined, value: undefined }
9799
inherit = nestedCustom.inherit || data._custom?.fields || {}
98-
value = nestedCustom.value || data._custom?.value as string
100+
value = nestedCustom.value || customValue as string
99101
}
100102
// @ts-expect-error @TODO: type
101103
if (value && value._isArray)
@@ -104,3 +106,11 @@ export function getRawValue(value: InspectorState['value']) {
104106

105107
return { value, inherit }
106108
}
109+
110+
export function toEdit(value: unknown) {
111+
return replaceTokenToString(JSON.stringify(value))
112+
}
113+
114+
export function toSubmit(value: string) {
115+
return JSON.parse(replaceStringToToken(value), reviver)
116+
}

packages/devtools-kit/src/core/component/state/util.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,38 @@
11
import { ESC, INFINITY, NAN, NEGATIVE_INFINITY, UNDEFINED, fnTypeRE } from './constants'
22
import { isComputed, isPlainObject, isPrimitive, isReactive, isReadOnly, isRef } from './is'
33

4+
export const tokenMap = {
5+
[UNDEFINED]: 'undefined',
6+
[NAN]: 'NaN',
7+
[INFINITY]: 'Infinity',
8+
[NEGATIVE_INFINITY]: '-Infinity',
9+
} as const
10+
11+
export const reversedTokenMap = Object.entries(tokenMap).reduce((acc, [key, value]) => {
12+
acc[value] = key
13+
return acc
14+
}, {})
15+
416
export function internalStateTokenToString(value: unknown) {
517
if (value === null)
618
return 'null'
719

8-
else if (value === UNDEFINED)
9-
return 'undefined'
10-
11-
else if (value === NAN)
12-
return 'NaN'
20+
return (typeof value === 'string' && tokenMap[value]) || false
21+
}
1322

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

17-
else if (value === NEGATIVE_INFINITY)
18-
return '-Infinity'
28+
export function replaceStringToToken(value: string) {
29+
const literalValue = reversedTokenMap[value.trim()]
30+
if (literalValue)
31+
return `"${literalValue}"`
1932

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

2338
/**

packages/devtools-kit/src/shared/util.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { formatInspectorStateValue, getInspectorStateValueType, getRawValue } from '../core/component/state/format'
21
import { stringifyReplacer } from '../core/component/state/replacer'
32
import { reviver } from '../core/component/state/reviver'
43
import { parseCircularAutoChunks, stringifyCircularAutoChunks } from './transfer'
@@ -16,8 +15,4 @@ export function parse(data: string, revive = false) {
1615
: parseCircularAutoChunks(data)
1716
}
1817

19-
export {
20-
formatInspectorStateValue,
21-
getInspectorStateValueType,
22-
getRawValue,
23-
}
18+
export * from '../core/component/state/format'

0 commit comments

Comments
 (0)