Skip to content
This repository has been archived by the owner on Jan 6, 2024. It is now read-only.

Commit

Permalink
feat: add component inspector to support component tree navigable (#200)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: webfansplz <308241863@qq.com>
  • Loading branch information
alexzhang1030 and webfansplz committed Aug 13, 2023
1 parent cd9e534 commit 3e730ae
Show file tree
Hide file tree
Showing 19 changed files with 265 additions and 81 deletions.
21 changes: 20 additions & 1 deletion packages/client/components/ComponentTreeNode.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script setup lang="ts">
import scrollIntoView from 'scroll-into-view-if-needed'
/* eslint-disable @typescript-eslint/consistent-type-imports */
import type { ComponentTreeNode } from '~/types'
Expand All @@ -11,6 +13,23 @@ const props = withDefaults(defineProps<{
const { isSelected, select, isExpanded, toggleExpand } = useComponent(props.data)
const { highlight, unhighlight } = useHighlightComponent(props.data)
const toggleEl = ref<HTMLElement>()
function autoScroll() {
if (isSelected.value && toggleEl.value) {
const el = toggleEl.value
scrollIntoView(el, {
scrollMode: 'if-needed',
block: 'center',
behavior: 'smooth',
inline: 'nearest',
})
}
}
watch(isSelected, () => autoScroll())
watch(toggleEl, () => autoScroll())
</script>

<template>
Expand All @@ -24,7 +43,7 @@ const { highlight, unhighlight } = useHighlightComponent(props.data)
@mouseover="highlight"
@mouseleave="unhighlight"
>
<h3 vue-block-title @click="data.hasChildren ? toggleExpand(data.id) : () => {}">
<h3 ref="toggleEl" vue-block-title @click="data.hasChildren ? toggleExpand(data.id) : () => {}">
<VDExpandIcon v-if="data.hasChildren" :value="isExpanded" />
<i v-else inline-block h-6 w-6 />
<span
Expand Down
29 changes: 18 additions & 11 deletions packages/client/composables/component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ComponentInternalInstance } from 'vue'
import { InstanceMap, getInstanceDetails, getInstanceName, getInstanceOrVnodeRect, getRootElementsFromComponentInstance } from '~/logic/components'
import { getInstanceName } from '@vite-plugin-vue-devtools/core'
import { InstanceMap, getInstanceDetails, getInstanceOrVnodeRect, getRootElementsFromComponentInstance } from '~/logic/components'
import { useDevToolsClient } from '~/logic/client'
import type { ComponentTreeNode } from '~/types'

Expand All @@ -13,23 +14,29 @@ const expandedMap = ref<Record<ComponentTreeNode['id'], boolean>>({

export const selectedComponent = ref<ComponentInternalInstance>()
export const selectedComponentState = shallowRef<Record<string, any>[]>([])

export function selectComponentTreeNode(data: ComponentTreeNode) {
selected.value = data.id
selectedComponentName.value = data.name
// TODO (Refactor): get instance state way
selectedComponentState.value = InstanceMap.get(data.id)
selectedComponentNode.value = data
// selectedComponent.value = instance.instance
// selectedComponentState.value = getInstanceState(instance.instance!)
}

export function setExpanded(id: string, expanded: boolean) {
expandedMap.value[id] = expanded
}

export function useComponent(instance: ComponentTreeNode & { instance?: ComponentInternalInstance }) {
function select(data: ComponentTreeNode) {
selected.value = data.id
selectedComponentName.value = data.name
// TODO (Refactor): get instance state way
selectedComponentState.value = InstanceMap.get(data.id)
selectedComponentNode.value = data
// selectedComponent.value = instance.instance
// selectedComponentState.value = getInstanceState(instance.instance!)
}
function toggleExpand(id: string) {
expandedMap.value[id] = !expandedMap.value[id]
}
const isSelected = computed(() => selected.value === instance.id)
const isExpanded = computed(() => expandedMap.value[instance.id])

return { isSelected, select, isExpanded, toggleExpand }
return { isSelected, select: selectComponentTreeNode, isExpanded, toggleExpand }
}

export function useHighlightComponent(node: ComponentTreeNode): {
Expand Down
4 changes: 3 additions & 1 deletion packages/client/logic/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ const client = ref<VueDevtoolsHostClient>({
componentInspector: {
highlight: () => {},
unHighlight: () => {},
scrollToComponent: () => {},
scrollToComponent: () => { },
startInspect: () => { },
stopInspect: () => { },
},
rerenderHighlight: {
updateInfo: () => {},
Expand Down
3 changes: 2 additions & 1 deletion packages/client/logic/components/data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/indent */
import type { ComponentInternalInstance } from 'vue'
import { camelize, getInstanceName, getUniqueComponentId, returnError } from './util'
import { getInstanceName } from '@vite-plugin-vue-devtools/core'
import { camelize, getUniqueComponentId, returnError } from './util'

const vueBuiltins = [
'nextTick',
Expand Down
3 changes: 2 additions & 1 deletion packages/client/logic/components/filter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ComponentInternalInstance } from 'vue'
import { classify, getInstanceName, kebabize } from './util'
import { getInstanceName } from '@vite-plugin-vue-devtools/core'
import { classify, kebabize } from './util'

export class ComponentFilter {
private filter: string
Expand Down
1 change: 0 additions & 1 deletion packages/client/logic/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { ComponentWalker, InstanceMap } from './tree'
export { getInstanceState, processSetupState, getInstanceDetails, getSetupStateInfo } from './data'
export { getInstanceOrVnodeRect, getRootElementsFromComponentInstance } from './el'
export { getInstanceName } from './util'
9 changes: 7 additions & 2 deletions packages/client/logic/components/tree.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { ComponentInternalInstance, SuspenseBoundary } from 'vue'
import { getInstanceName, getRenderKey, getUniqueComponentId, isBeingDestroyed, isFragment } from './util'
import { getInstanceName } from '@vite-plugin-vue-devtools/core'
import { getRenderKey, getUniqueComponentId, isBeingDestroyed, isFragment } from './util'
import { ComponentFilter } from './filter'
import { getRootElementsFromComponentInstance } from './el'
import { getInstanceState } from './data'
import type { ComponentTreeNode } from '~/types'

export const InstanceMap = new Map()
export const UidToTreeNodeMap = new Map<number, ComponentTreeNode>()

export class ComponentWalker {
maxDepth: number
recursively: boolean
Expand All @@ -27,7 +31,7 @@ export class ComponentWalker {

getComponentParents(instance: ComponentInternalInstance) {
this.captureIds = new Map()
const parents = []
const parents: ComponentInternalInstance[] = []
this.captureId(instance)
let parent = instance
// eslint-disable-next-line no-cond-assign
Expand Down Expand Up @@ -205,6 +209,7 @@ export class ComponentWalker {
// }

InstanceMap.set(treeNode.id, getInstanceState(instance))
UidToTreeNodeMap.set(treeNode.uid, treeNode)
treeNode.instance = instance

return treeNode
Expand Down
47 changes: 0 additions & 47 deletions packages/client/logic/components/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,53 +9,6 @@ export function isFragment(instance: ComponentInternalInstance) {
return Fragment === instance.subTree?.type
}

/**
* Get the appropriate display name for an instance.
*
* @param {Vue} instance
* @return {String}
*/
export function getInstanceName(instance: any) {
const name = getComponentTypeName(instance.type || {})
if (name)
return name
if (instance.root === instance)
return 'Root'
for (const key in instance.parent?.type?.components) {
if (instance.parent.type.components[key] === instance.type)
return saveComponentName(instance, key)
}

for (const key in instance.appContext?.components) {
if (instance.appContext.components[key] === instance.type)
return saveComponentName(instance, key)
}

const fileName = getComponentFileName(instance.type || {})
if (fileName)
return fileName

return 'Anonymous Component'
}

function saveComponentName(instance: ComponentInternalInstance, key: string) {
return key
}

function getComponentTypeName(options: any) {
return options.name || options._componentTag || options.__vdevtools_guessedName || options.__name
}

export function getComponentFileName(options: any) {
const file = options.__file // injected by vite
// TODO: classify
if (file) {
const filename = options.__file?.match(/\/?([^/]+?)(\.[^/.]+)?$/)?.[1]
return filename ?? file
}
// return classify(basename(file, '.vue'))
}

/**
* Returns a devtools unique id for instance.
* @param {Vue} instance
Expand Down
2 changes: 1 addition & 1 deletion packages/client/logic/timeline.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { nanoid } from 'nanoid'
import { getComponentFileName } from './components/util'
import { getComponentFileName } from '@vite-plugin-vue-devtools/core'
import { useDevToolsClient } from './client'

interface TimelineLayer {
Expand Down
1 change: 1 addition & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"json-editor-vue": "^0.10.6",
"minimatch": "^9.0.3",
"nanoid": "^4.0.2",
"scroll-into-view-if-needed": "^3.0.10",
"splitpanes": "^3.1.5",
"vanilla-jsoneditor": "^0.17.8",
"vite-hot-client": "^0.2.1",
Expand Down
55 changes: 52 additions & 3 deletions packages/client/pages/components.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
<script setup lang="ts">
import { Pane, Splitpanes } from 'splitpanes'
import { scrollToComponent, selected, selectedComponentName, selectedComponentNode, selectedComponentNodeFilePath } from '../composables/component'
import type { ComponentInternalInstance } from 'vue'
import { scrollToComponent, selectComponentTreeNode, selected, selectedComponentName, selectedComponentNode, selectedComponentNodeFilePath } from '../composables/component'
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { UidToTreeNodeMap } from '../logic/components/tree'
import type { ComponentTreeNode } from '~/types'
import { ComponentWalker, getInstanceState } from '~/logic/components'
import { useDevToolsClient } from '~/logic/client'
import { instance, onVueInstanceUpdate } from '~/logic/app'
import { rootPath } from '~/logic/global'
import { getUniqueComponentId } from '~/logic/components/util'
const componentTree = ref<ComponentTreeNode[]>([])
const filterName = ref('')
Expand Down Expand Up @@ -82,14 +85,60 @@ function openInEditor() {
const client = useDevToolsClient()
client.value.openInEditor(selectedComponentNodeFilePath.value)
}
const client = useDevToolsClient()
const inspectorEnabled = ref(false)
function inspectComponentClick(instance: ComponentInternalInstance) {
inspectorEnabled.value = false
const treeNode = UidToTreeNodeMap.get(instance.uid)
if (treeNode) {
selectComponentTreeNode(treeNode)
const walker = new ComponentWalker(0, null, false)
const parents = walker.getComponentParents(instance)
parents.reverse().forEach((instance) => {
const id = getUniqueComponentId(instance)
// Ignore root
if (id.endsWith('root'))
return
setExpanded(id, true)
})
}
}
function toggleInspector(target?: boolean) {
inspectorEnabled.value = target ?? !inspectorEnabled.value
if (inspectorEnabled.value)
client.value.componentInspector.startInspect(inspectComponentClick)
else client.value.componentInspector.stopInspect()
}
const { control, c, escape } = useMagicKeys()
watchEffect(() => {
if ((control.value && c.value) || (escape.value))
toggleInspector(false)
})
</script>

<template>
<div h-screen n-panel-grids>
<Splitpanes>
<Pane border="r base">
<div v-if="componentWalker" w-full px10px py12px>
<VDTextInput v-model="filterName" placeholder="Find components..." />
<div v-if="componentWalker" sticky left-0 top-0 z-300 w-full flex gap2 px10px py12px bg-base>
<VDTextInput v-model="filterName" placeholder="Find components..." flex-1 />
<button p2 @click="() => toggleInspector()">
<svg
xmlns="http://www.w3.org/2000/svg"
style="height: 1.1em; width: 1.1em; opacity:0.5;"
:style="inspectorEnabled ? 'opacity:1;color:#00dc82' : ''"
viewBox="0 0 24 24"
>
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="12" cy="12" r=".5" fill="currentColor" /><path d="M5 12a7 7 0 1 0 14 0a7 7 0 1 0-14 0m7-9v2m-9 7h2m7 7v2m7-9h2" /></g>
</svg>
</button>
</div>
<div h-screen select-none overflow-scroll p-2 class="no-scrollbar">
<ComponentTreeNode v-for="(item) in componentTree" :key="item.id" :data="item" />
Expand Down
3 changes: 1 addition & 2 deletions packages/client/pages/rerender-trace.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { DevToolsHooks } from '@vite-plugin-vue-devtools/core'
import { DevToolsHooks, getInstanceName } from '@vite-plugin-vue-devtools/core'
import type { ComponentInternalInstance, DebuggerEvent, Ref } from 'vue'
import { useDevToolsClient } from '~/logic/client'
import { rootPath } from '~/logic/global'
import { getSetupStateInfo, toRaw } from '~/logic/components/data'
import { getInstanceName } from '~/logic/components'
type ComponentInstance = ComponentInternalInstance & {
devtoolsRawSetupState: Record<string, unknown>
Expand Down
3 changes: 3 additions & 0 deletions packages/client/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ComponentInternalInstance } from 'vue'
import type { Router } from 'vue-router'

type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
Expand Down Expand Up @@ -27,6 +28,8 @@ export interface VueDevtoolsHostClient {
componentInspector: {
highlight: (_name: string, _bounds: ComponentInspectorBounds) => void
unHighlight: () => void
startInspect(cb?: (instance: ComponentInternalInstance) => void): void
stopInspect(): void
scrollToComponent: (_bounds: ComponentInspectorBounds) => void
}
rerenderHighlight: {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
},
"devDependencies": {
"@babel/types": "^7.22.5",
"@vue/compiler-sfc": "^3.3.4"
"@vue/compiler-sfc": "^3.3.4",
"vue": "^3.3.4"
}
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './hook'
export * from './host'
export * from './rpc'
export * from './constant'
export * from './shared'
48 changes: 48 additions & 0 deletions packages/core/src/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { ComponentInternalInstance } from 'vue'

export function getComponentTypeName(options: any) {
return options.name || options._componentTag || options.__vdevtools_guessedName || options.__name
}

function saveComponentName(instance: ComponentInternalInstance, key: string) {
return key
}

export function getComponentFileName(options: any) {
const file = options.__file // injected by vite
// TODO: classify
if (file) {
const filename = options.__file?.match(/\/?([^/]+?)(\.[^/.]+)?$/)?.[1]
return filename ?? file
}
// return classify(basename(file, '.vue'))
}

/**
* Get the appropriate display name for an instance.
*
* @param {Vue} instance
* @return {String}
*/
export function getInstanceName(instance: any) {
const name = getComponentTypeName(instance.type || {})
if (name)
return name
if (instance.root === instance)
return 'Root'
for (const key in instance.parent?.type?.components) {
if (instance.parent.type.components[key] === instance.type)
return saveComponentName(instance, key)
}

for (const key in instance.appContext?.components) {
if (instance.appContext.components[key] === instance.type)
return saveComponentName(instance, key)
}

const fileName = getComponentFileName(instance.type || {})
if (fileName)
return fileName

return 'Anonymous Component'
}
Loading

0 comments on commit 3e730ae

Please sign in to comment.