Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'