UNPKG

vite-plugin-vue-inspector

Version:

Jump to local IDE source code when clicking Vue elements in the browser.

194 lines (154 loc) 5.79 kB
import type { VNode } from 'vue' const KEY_DATA = 'data-v-inspector' const KEY_IGNORE = 'data-v-inspector-ignore' const KEY_PROPS_DATA = '__v_inspector' const KEY_GLOBAL = '__vue_inspector_store__' export type PositionInfo = [source: string, line: number, column: number] interface Store { hasData: boolean vnodeToPos: WeakMap<object, PositionInfo> fileToVNode: Map<string, WeakSet<object>> posToVNode: Map<string, Map<number, Map<number, WeakSet<object>>>> events?: unknown } const globalStore = globalThis as typeof globalThis & { [KEY_GLOBAL]?: Store } const store: Store = globalStore[KEY_GLOBAL] ?? ({ hasData: false, vnodeToPos: new WeakMap(), fileToVNode: new Map(), posToVNode: new Map(), } satisfies Store) if (!globalStore[KEY_GLOBAL]) { Object.defineProperty(globalStore, KEY_GLOBAL, { value: store, configurable: true, enumerable: false, }) } export function getInternalStore(): Store { return store } function parsePositionInfo(value: unknown): PositionInfo | undefined { if (typeof value !== 'string') return const match = value.match(/^(.+):(\d+):(\d+)$/) if (!match) return return [match[1]!, Number(match[2]), Number(match[3])] } function moveInspectorDataToHiddenProps(props: Record<string, unknown>): void { const data = props[KEY_DATA] if (typeof data !== 'string') return delete props[KEY_DATA] if (KEY_PROPS_DATA in props) return Object.defineProperty(props, KEY_PROPS_DATA, { value: data, configurable: true, enumerable: false, }) } export function recordPosition(source: string, line: number, column: number, node: VNode): VNode { if (!node || typeof node === 'string' || typeof node === 'number') return node store.hasData = true const props = (node.props ||= {}) as Record<string, unknown> moveInspectorDataToHiddenProps(props) store.vnodeToPos.set(props, [source, line, column]) if (!store.fileToVNode.has(source)) store.fileToVNode.set(source, new WeakSet()) store.fileToVNode.get(source)!.add(props) if (!store.posToVNode.has(source)) store.posToVNode.set(source, new Map()) const lineMap = store.posToVNode.get(source)! if (!lineMap.has(line)) lineMap.set(line, new Map()) const columnMap = lineMap.get(line)! if (!columnMap.has(column)) columnMap.set(column, new WeakSet()) columnMap.get(column)!.add(props) return node } function getPositionFromVNode(node: VNode): PositionInfo | undefined { const props = node?.props as Record<string, unknown> | undefined if (!props) return return store.vnodeToPos.get(props) ?? parsePositionInfo(props[KEY_PROPS_DATA]) } function getComponentPositionFromElement(el: Element): [PositionInfo, VNode] | undefined { const vnode = (el as any).__vnode?.ctx?.vnode as VNode | undefined if (!vnode || vnode.el !== el) return const pos = getPositionFromVNode(vnode) if (!pos) return return [pos, vnode] } function isSamePosition(a: PositionInfo | undefined, b: PositionInfo): boolean { return Boolean(a && a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) } export class ElementTraceInfo { pos: PositionInfo vnode: VNode | undefined el: Element | undefined constructor(pos: PositionInfo, el?: Element, vnode?: VNode) { this.vnode = vnode this.pos = pos this.el = el } get filepath(): string { return this.pos[0] } get fullpath(): string { let file = this.pos[0] if (this.pos[1]) { file += `:${this.pos[1]}` if (this.pos[2]) file += `:${this.pos[2]}` } return file } get rect(): DOMRect | undefined { return this.el?.getBoundingClientRect() } getElementsSameFile(): Element[] | undefined { return Array.from(document.querySelectorAll('*')).filter( (el) => el !== this.el && findTraceFromElement(el)?.filepath === this.filepath, ) } getParent(): ElementTraceInfo | undefined { const parentVNode = (this.vnode as any)?.parent const parentEl = this.el?.parentElement return findTraceFromVNode(parentVNode) ?? findTraceFromElement(parentEl) } getElementsSamePosition(): Element[] | undefined { const selector = typeof this.vnode?.type === 'string' ? this.vnode.type : this.el?.tagName.toLowerCase() || '*' return Array.from(document.querySelectorAll(selector)).filter( (el) => el !== this.el && isSamePosition(findTraceFromElement(el)?.pos, this.pos), ) } } export function findTraceFromElement(el?: Element | null): ElementTraceInfo | undefined { if (!el) return const vnode: VNode | undefined = (el as any).__vnode const vnodeTrace = findTraceFromVNode(vnode, el) if (vnodeTrace) return vnodeTrace const componentPosition = getComponentPositionFromElement(el) if (componentPosition) return new ElementTraceInfo(componentPosition[0], el, componentPosition[1]) const attrPos = parsePositionInfo(el.getAttribute(KEY_DATA)) if (attrPos) return new ElementTraceInfo(attrPos, el, vnode) } export function findTraceFromVNode(vnode?: VNode, el?: Element): ElementTraceInfo | undefined { if (!vnode) return const pos = getPositionFromVNode(vnode) if (!pos) return return new ElementTraceInfo(pos, (el ?? vnode?.el ?? undefined) as any, vnode) } export function findTraceAtPointer(e: { x: number; y: number }): ElementTraceInfo | undefined { let elements = document.elementsFromPoint(e.x, e.y) const ignoreIndex = elements.findIndex((node) => node?.hasAttribute?.(KEY_IGNORE)) if (ignoreIndex !== -1) elements = elements.slice(ignoreIndex + 1) for (const el of elements) { const match = findTraceFromElement(el) if (match) return match } } export function hasData(): boolean { return ( store.hasData || (typeof document !== 'undefined' && document.querySelector(`[${KEY_DATA}]`) != null) ) }