import type { BackendContext } from '@vue-devtools/app-backend-api' import type { StateEditor } from '@vue-devtools/shared-utils' import { SharedData, camelize, kebabize } from '@vue-devtools/shared-utils' import type { ComponentInstance, CustomState, HookPayloads, Hooks, InspectedComponentData } from '@vue/devtools-api' import { returnError } from '../util' import { getInstanceName, getUniqueComponentId } from './util' const vueBuiltins = [ 'nextTick', 'defineComponent', 'defineAsyncComponent', 'defineCustomElement', 'ref', 'computed', 'reactive', 'readonly', 'watchEffect', 'watchPostEffect', 'watchSyncEffect', 'watch', 'isRef', 'unref', 'toRef', 'toRefs', 'isProxy', 'isReactive', 'isReadonly', 'shallowRef', 'triggerRef', 'customRef', 'shallowReactive', 'shallowReadonly', 'toRaw', 'markRaw', 'effectScope', 'getCurrentScope', 'onScopeDispose', 'onMounted', 'onUpdated', 'onUnmounted', 'onBeforeMount', 'onBeforeUpdate', 'onBeforeUnmount', 'onErrorCaptured', 'onRenderTracked', 'onRenderTriggered', 'onActivated', 'onDeactivated', 'onServerPrefetch', 'provide', 'inject', 'h', 'mergeProps', 'cloneVNode', 'isVNode', 'resolveComponent', 'resolveDirective', 'withDirectives', 'withModifiers', ] /** * Get the detailed information of an inspected instance. */ export function getInstanceDetails(instance: any, ctx: BackendContext): InspectedComponentData { return { id: getUniqueComponentId(instance, ctx), name: getInstanceName(instance), file: instance.type?.__file, state: getInstanceState(instance), } } function getInstanceState(instance) { const mergedType = resolveMergedOptions(instance) return processProps(instance).concat( processState(instance), processSetupState(instance), processComputed(instance, mergedType), processAttrs(instance), processProvide(instance), processInject(instance, mergedType), processRefs(instance), processEventListeners(instance), ) } /** * Process the props of an instance. * Make sure return a plain object because window.postMessage() * will throw an Error if the passed object contains Functions. * * @param {Vue} instance * @return {Array} */ function processProps(instance) { const propsData = [] const propDefinitions = instance.type.props for (let key in instance.props) { const propDefinition = propDefinitions ? propDefinitions[key] : null key = camelize(key) propsData.push({ type: 'props', key, value: returnError(() => instance.props[key]), meta: propDefinition ? { type: propDefinition.type ? getPropType(propDefinition.type) : 'any', required: !!propDefinition.required, ...propDefinition.default != null ? { default: propDefinition.default.toString(), } : {}, } : { type: 'invalid', }, editable: SharedData.editableProps, }) } return propsData } const fnTypeRE = /^(?:function|class) (\w+)/ /** * Convert prop type constructor to string. */ function getPropType(type) { if (Array.isArray(type)) { return type.map(t => getPropType(t)).join(' or ') } if (type == null) { return 'null' } const match = type.toString().match(fnTypeRE) return typeof type === 'function' ? (match && match[1]) || 'any' : 'any' } /** * Process state, filtering out props and "clean" the result * with a JSON dance. This removes functions which can cause * errors during structured clone used by window.postMessage. * * @param {Vue} instance * @return {Array} */ function processState(instance) { const type = instance.type const props = type.props const getters = type.vuex && type.vuex.getters const computedDefs = type.computed const data = { ...instance.data, ...instance.renderContext, } return Object.keys(data) .filter(key => ( !(props && key in props) && !(getters && key in getters) && !(computedDefs && key in computedDefs) )) .map(key => ({ key, type: 'data', value: returnError(() => data[key]), editable: true, })) } function processSetupState(instance) { const raw = instance.devtoolsRawSetupState const combinedSetupState = (Object.keys(instance.setupState).length ? instance.setupState : instance.exposed ) || {} return Object.keys(combinedSetupState) .filter(key => !vueBuiltins.includes(key) && key.split(/(?=[A-Z])/)[0] !== 'use') .map((key) => { const value = returnError(() => toRaw(combinedSetupState[key])) const rawData = raw[key] let result: any let isOther = typeof value === 'function' || typeof value?.render === 'function' // Components || typeof value?.__asyncLoader === 'function' // Components || (typeof value === 'object' && value && ('setup' in value || 'props' in value)) // Components || /^v[A-Z]/.test(key) // Directives if (rawData) { const info = getSetupStateInfo(rawData) const objectType = info.computed ? 'Computed' : info.ref ? 'Ref' : info.reactive ? 'Reactive' : null const isState = info.ref || info.computed || info.reactive const raw = rawData.effect?.raw?.toString() || rawData.effect?.fn?.toString() if (objectType) { isOther = false } result = { ...objectType ? { objectType } : {}, ...raw ? { raw } : {}, editable: isState && !info.readonly, } } const type = isOther ? 'setup (other)' : 'setup' return { key, value, type, ...result, } }) } function isRef(raw: any): boolean { return !!raw.__v_isRef } function isComputed(raw: any): boolean { return isRef(raw) && !!raw.effect } function isReactive(raw: any): boolean { return !!raw.__v_isReactive } function isReadOnly(raw: any): boolean { return !!raw.__v_isReadonly } function toRaw(value: any) { if (value?.__v_raw) { return value.__v_raw } return value } function getSetupStateInfo(raw: any) { return { ref: isRef(raw), computed: isComputed(raw), reactive: isReactive(raw), readonly: isReadOnly(raw), } } export function getCustomObjectDetails(object: any, _proto: string): CustomState | undefined { const info = getSetupStateInfo(object) const isState = info.ref || info.computed || info.reactive if (isState) { const objectType = info.computed ? 'Computed' : info.ref ? 'Ref' : info.reactive ? 'Reactive' : null const value = toRaw(info.reactive ? object : object._value) const raw = object.effect?.raw?.toString() || object.effect?.fn?.toString() return { _custom: { type: objectType.toLowerCase(), objectType, value, ...raw ? { tooltip: `${raw}` } : {}, }, } } if (typeof object.__asyncLoader === 'function') { return { _custom: { type: 'component-definition', display: 'Async component definition', }, } } } /** * Process the computed properties of an instance. * * @param {Vue} instance * @return {Array} */ function processComputed(instance, mergedType) { const type = mergedType const computed = [] const defs = type.computed || {} // use for...in here because if 'computed' is not defined // on component, computed properties will be placed in prototype // and Object.keys does not include // properties from object's prototype for (const key in defs) { const def = defs[key] const type = typeof def === 'function' && def.vuex ? 'vuex bindings' : 'computed' computed.push({ type, key, value: returnError(() => instance.proxy[key]), editable: typeof def.set === 'function', }) } return computed } function processAttrs(instance) { return Object.keys(instance.attrs) .map(key => ({ type: 'attrs', key, value: returnError(() => instance.attrs[key]), })) } function processProvide(instance) { return Reflect.ownKeys(instance.provides) .map(key => ({ type: 'provided', key: key.toString(), value: returnError(() => instance.provides[key]), })) } function processInject(instance, mergedType) { if (!mergedType?.inject) { return [] } let keys = [] let defaultValue if (Array.isArray(mergedType.inject)) { keys = mergedType.inject.map(key => ({ key, originalKey: key, })) } else { keys = Reflect.ownKeys(mergedType.inject).map((key) => { const value = mergedType.inject[key] let originalKey if (typeof value === 'string' || typeof value === 'symbol') { originalKey = value } else { originalKey = value.from defaultValue = value.default } return { key, originalKey, } }) } return keys.map(({ key, originalKey }) => ({ type: 'injected', key: originalKey && key !== originalKey ? `${originalKey.toString()} ➞ ${key.toString()}` : key.toString(), value: returnError(() => Object.prototype.hasOwnProperty.call(instance.ctx, key) ? instance.ctx[key] : Object.prototype.hasOwnProperty.call(instance.provides, originalKey) ? instance.provides[originalKey] : defaultValue), })) } function processRefs(instance) { return Object.keys(instance.refs) .map(key => ({ type: 'refs', key, value: returnError(() => instance.refs[key]), })) } function processEventListeners(instance) { const emitsDefinition = instance.type.emits const declaredEmits = Array.isArray(emitsDefinition) ? emitsDefinition : Object.keys(emitsDefinition ?? {}) const declaredEmitsMap = declaredEmits.reduce((emitsMap, key) => { emitsMap[kebabize(key)] = key return emitsMap }, {}) const keys = Object.keys(instance.vnode.props ?? {}) const result = [] for (const key of keys) { const [prefix, ...eventNameParts] = key.split(/(?=[A-Z])/) if (prefix === 'on') { const eventName = eventNameParts.join('-').toLowerCase() const normalizedEventName = declaredEmitsMap[eventName] result.push({ type: 'event listeners', key: normalizedEventName || eventName, value: { _custom: { display: normalizedEventName ? '✅ Declared' : '⚠️ Not declared', tooltip: !normalizedEventName ? `The event ${eventName} is not declared in the emits option. It will leak into the component's attributes ($attrs).` : null, }, }, }) } } return result } export function editState({ componentInstance, path, state, type }: HookPayloads[Hooks.EDIT_COMPONENT_STATE], stateEditor: StateEditor, _ctx: BackendContext) { if (!['data', 'props', 'computed', 'setup'].includes(type)) { return } let target: any const targetPath: string[] = path.slice() if (Object.keys(componentInstance.props).includes(path[0])) { // Props target = componentInstance.props } else if (componentInstance.devtoolsRawSetupState && Object.keys(componentInstance.devtoolsRawSetupState).includes(path[0])) { // Setup target = componentInstance.devtoolsRawSetupState const currentValue = stateEditor.get(componentInstance.devtoolsRawSetupState, path) if (currentValue != null) { const info = getSetupStateInfo(currentValue) if (info.readonly) { return } } } else { target = componentInstance.proxy } if (target && targetPath) { stateEditor.set(target, targetPath, 'value' in state ? state.value : undefined, stateEditor.createDefaultSetCallback(state)) } } function reduceStateList(list) { if (!list.length) { return undefined } return list.reduce((map, item) => { const key = item.type || 'data' const obj = map[key] = map[key] || {} obj[item.key] = item.value return map }, {}) } export function getCustomInstanceDetails(instance) { if (instance._) { instance = instance._ } const state = getInstanceState(instance) return { _custom: { type: 'component', id: instance.__VUE_DEVTOOLS_UID__, display: getInstanceName(instance), tooltip: 'Component instance', value: reduceStateList(state), fields: { abstract: true, }, }, } } function resolveMergedOptions( instance: ComponentInstance, ) { const raw = instance.type const { mixins, extends: extendsOptions } = raw const globalMixins = instance.appContext.mixins if (!globalMixins.length && !mixins && !extendsOptions) { return raw } const options = {} globalMixins.forEach(m => mergeOptions(options, m, instance)) mergeOptions(options, raw, instance) return options } function mergeOptions( to: any, from: any, instance: ComponentInstance, ) { if (typeof from === 'function') { from = from.options } if (!from) { return to } const { mixins, extends: extendsOptions } = from extendsOptions && mergeOptions(to, extendsOptions, instance) mixins && mixins.forEach(m => mergeOptions(to, m, instance), ) for (const key of ['computed', 'inject']) { if (Object.prototype.hasOwnProperty.call(from, key)) { if (!to[key]) { to[key] = from[key] } else { to[key] = Object.assign(Object.create(null), to[key], from[key]) } } } return to }