feat: init

This commit is contained in:
2026-02-13 22:02:30 +01:00
commit 8f9ff830fb
16711 changed files with 3307340 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
import * as vue from 'vue';
import { ElementTraceInfo } from './record.mjs';
export { PositionInfo, findTraceAtPointer, findTraceFromElement, findTraceFromVNode, getInternalStore, hasData, recordPosition } from './record.mjs';
interface EventsMap {
[event: string]: any
}
interface DefaultEvents extends EventsMap {
[event: string]: (...args: any) => void
}
interface Unsubscribe {
(): void
}
interface Emitter<Events extends EventsMap = DefaultEvents> {
/**
* Calls each of the listeners registered for a given event.
*
* ```js
* ee.emit('tick', tickType, tickDuration)
* ```
*
* @param event The event name.
* @param args The arguments for listeners.
*/
emit<K extends keyof Events>(
this: this,
event: K,
...args: Parameters<Events[K]>
): void
/**
* Event names in keys and arrays with listeners in values.
*
* ```js
* emitter1.events = emitter2.events
* emitter2.events = { }
* ```
*/
events: Partial<{ [E in keyof Events]: Events[E][] }>
/**
* Add a listener for a given event.
*
* ```js
* const unbind = ee.on('tick', (tickType, tickDuration) => {
* count += 1
* })
*
* disable () {
* unbind()
* }
* ```
*
* @param event The event name.
* @param cb The listener function.
* @returns Unbind listener from event.
*/
on<K extends keyof Events>(this: this, event: K, cb: Events[K]): Unsubscribe
}
declare const lastMatchedElement: vue.ShallowRef<ElementTraceInfo | undefined, ElementTraceInfo | undefined>;
interface Events {
hover: (info: ElementTraceInfo | undefined, event: MouseEvent | PointerEvent) => void;
click: (info: ElementTraceInfo, event: MouseEvent | PointerEvent) => void;
enabled: () => void;
disabled: () => void;
}
declare const events: Emitter<Events>;
declare const isEnabled: vue.Ref<boolean, boolean>;
export { ElementTraceInfo, events, isEnabled, lastMatchedElement };
export type { Events };

View File

@@ -0,0 +1,70 @@
import { shallowRef, customRef, ref } from 'vue';
import { getInternalStore, findTraceAtPointer } from './record.mjs';
export { ElementTraceInfo, findTraceFromElement, findTraceFromVNode, hasData, recordPosition } from './record.mjs';
let createNanoEvents = () => ({
emit(event, ...args) {
for (
let callbacks = this.events[event] || [],
i = 0,
length = callbacks.length;
i < length;
i++
) {
callbacks[i](...args);
}
},
events: {},
on(event, cb) {
(this.events[event] ||= []).push(cb);
return () => {
this.events[event] = this.events[event]?.filter(i => cb !== i);
}
}
});
const lastMatchedElement = shallowRef();
const _store = getInternalStore();
const events = _store.events ||= createNanoEvents();
const isEnabled = customRef(() => {
const value = ref(false);
return {
get() {
return value.value;
},
set(newValue) {
if (newValue === value.value)
return;
value.value = newValue;
if (newValue)
events.emit("enabled");
else
events.emit("disabled");
}
};
});
if (typeof document !== "undefined") {
document.addEventListener("pointermove", (e) => {
if (!isEnabled.value)
return;
const result = findTraceAtPointer({ x: e.clientX, y: e.clientY });
if (result?.el === lastMatchedElement.value?.el)
return;
lastMatchedElement.value = result;
events.emit("hover", result, e);
});
document.addEventListener("click", (e) => {
if (!isEnabled.value)
return;
const result = findTraceAtPointer({ x: e.clientX, y: e.clientY });
if (result) {
events.emit("click", result, e);
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
return false;
}
}, true);
}
export { events, findTraceAtPointer, getInternalStore, isEnabled, lastMatchedElement };

View File

@@ -0,0 +1,50 @@
import * as vue from 'vue';
import { ElementTraceInfo } from './record.mjs';
export { PositionInfo, findTraceAtPointer, findTraceFromElement, findTraceFromVNode, getInternalStore, hasData, recordPosition } from './record.mjs';
export { Events, events, isEnabled, lastMatchedElement } from './listeners.mjs';
declare const state: {
isEnabled: boolean;
isVisible: boolean;
isAnimated: boolean;
isFocused: boolean;
main?: {
pos: [source: string, line: number, column: number];
vnode: vue.VNode | undefined;
el: Element | undefined;
readonly filepath: string;
readonly fullpath: string;
readonly rect: {
height: number;
width: number;
x: number;
y: number;
readonly bottom: number;
readonly left: number;
readonly right: number;
readonly top: number;
toJSON: () => any;
} | undefined;
getElementsSameFile: () => Element[] | undefined;
getParent: () => ElementTraceInfo | undefined;
getElementsSamePosition: () => Element[] | undefined;
} | undefined;
sub: {
rects?: {
id: string;
rect: {
height: number;
width: number;
x: number;
y: number;
readonly bottom: number;
readonly left: number;
readonly right: number;
readonly top: number;
toJSON: () => any;
};
}[] | undefined;
};
};
export { ElementTraceInfo, state };

View File

@@ -0,0 +1,193 @@
import { reactive, watchEffect } from 'vue';
import { isEnabled, events, lastMatchedElement } from './listeners.mjs';
export { ElementTraceInfo, findTraceAtPointer, findTraceFromElement, findTraceFromVNode, getInternalStore, hasData, recordPosition } from './record.mjs';
const state = reactive({
isEnabled,
isVisible: false,
isFocused: false,
isAnimated: true,
sub: {}
});
const ANIMATE_DURATION = "0.15s";
const PADDING_FOCUSED = 10;
function createOverlay() {
const overlay = document.createElement("div");
overlay.id = "vue-tracer-overlay";
Object.assign(overlay.style, {
position: "fixed",
top: "0",
left: "0",
bottom: "0",
right: "0",
zIndex: "999999",
pointerEvents: "none",
opacity: "0"
});
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
Object.assign(svg.style, {
position: "fixed",
top: "0",
left: "0",
bottom: "0",
pointerEvents: "none",
right: "0"
});
svg.setAttribute("width", "100%");
svg.setAttribute("height", "100%");
const mainText = document.createElement("div");
Object.assign(mainText.style, {
position: "fixed",
top: "0",
left: "0",
transition: `all ${ANIMATE_DURATION}`,
backgroundColor: "#197",
color: "#fff",
borderBottomLeftRadius: "4px",
borderBottomRightRadius: "4px",
padding: "1px 4px",
pointerEvents: "none",
fontSize: "10px",
fontFamily: "monospace",
display: "flex",
gap: "0.25rem",
boxShadow: "0 0 2px rgba(0,0,0,0.2)"
});
const mainTextTag = document.createElement("div");
const mainTextPath = document.createElement("div");
Object.assign(mainTextPath.style, {
opacity: "0.75",
fontSize: "9px"
});
mainText.appendChild(mainTextTag);
mainText.appendChild(mainTextPath);
const mainRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
mainRect.setAttribute("fill", "#1972");
mainRect.setAttribute("stroke", "#197");
mainRect.setAttribute("rx", "4");
mainRect.setAttribute("ry", "4");
overlay.appendChild(svg);
overlay.appendChild(mainText);
svg.appendChild(mainRect);
document.body.appendChild(overlay);
watchEffect(() => {
overlay.style.transition = state.isAnimated ? `all ${ANIMATE_DURATION}` : "none";
mainText.style.transition = state.isAnimated ? `all ${ANIMATE_DURATION}` : "none";
mainRect.style.transition = state.isAnimated ? `all ${ANIMATE_DURATION}` : "none";
overlay.style.opacity = state.isVisible ? "1" : "0";
});
watchEffect(() => {
if (state.main && state.main.rect) {
const rect = state.main.rect;
mainRect.style.opacity = "1";
if (state.isFocused) {
mainRect.setAttribute("rx", "8");
mainRect.setAttribute("ry", "8");
mainRect.setAttribute("x", String(rect.left - PADDING_FOCUSED));
mainRect.setAttribute("y", String(rect.top - PADDING_FOCUSED));
mainRect.setAttribute("width", String(rect.width + PADDING_FOCUSED * 2));
mainRect.setAttribute("height", String(rect.height + PADDING_FOCUSED * 2));
mainRect.style.filter = "blur(1px)";
} else {
mainRect.setAttribute("rx", "4");
mainRect.setAttribute("ry", "4");
mainRect.setAttribute("x", String(rect.left));
mainRect.setAttribute("y", String(rect.top));
mainRect.setAttribute("width", String(rect.width));
mainRect.setAttribute("height", String(rect.height));
mainRect.style.filter = "";
}
mainRect.style.fill = state.isFocused ? "transparent" : "#1972";
mainText.style.opacity = state.isFocused ? "0" : "1";
mainTextTag.textContent = "";
let tagName = "";
if (state.main?.vnode?.type) {
if (typeof state.main?.vnode?.type === "string")
tagName = state.main.vnode.type;
else
tagName = state.main.vnode.type.name;
}
mainTextTag.textContent = tagName ? `<${tagName}>` : "";
mainTextPath.textContent = `${state.main.pos[0]}:${state.main.pos[1]}:${state.main.pos[2]}`;
Object.assign(mainText.style, {
left: `${rect.left + 3}px`,
top: `${rect.top + rect.height - 1}px`
});
} else {
mainText.style.opacity = "0";
mainRect.style.opacity = "0";
}
});
const subMap = /* @__PURE__ */ new Map();
watchEffect(() => {
const toRemove = new Set(subMap.keys());
for (const { id, rect } of state.sub.rects || []) {
toRemove.delete(id);
let cover = subMap.get(id);
if (!cover) {
cover = document.createElementNS("http://www.w3.org/2000/svg", "rect");
cover.setAttribute("fill", "#8482");
cover.setAttribute("stroke", "#848");
cover.setAttribute("rx", "4");
cover.setAttribute("ry", "4");
svg.appendChild(cover);
subMap.set(id, cover);
}
cover.style.transition = state.isAnimated ? `all ${ANIMATE_DURATION}` : "none";
cover.setAttribute("x", String(rect.left));
cover.setAttribute("y", String(rect.top));
cover.setAttribute("width", String(rect.width));
cover.setAttribute("height", String(rect.height));
cover.style.opacity = "1";
}
for (const id of toRemove) {
const cover = subMap.get(id);
if (cover) {
cover.style.opacity = "0";
setTimeout(() => cover.remove(), 300);
subMap.delete(id);
}
}
});
}
const elementId = /* @__PURE__ */ new WeakMap();
function getElementId(el) {
let id = elementId.get(el);
if (!id) {
id = Math.random().toString(16).slice(2);
elementId.set(el, id);
}
return id;
}
function update(result) {
if (!result) {
state.isVisible = false;
state.sub.rects = void 0;
state.main = void 0;
return;
}
state.isVisible = true;
state.main = result;
const samePos = result.getElementsSamePosition();
state.sub.rects = samePos?.map((el) => ({
id: getElementId(el),
rect: el.getBoundingClientRect()
}));
}
function init() {
createOverlay();
events.on("hover", (result) => {
update(result);
});
document.addEventListener("scroll", () => {
if (state.isVisible)
update(lastMatchedElement.value);
});
window.addEventListener("resize", () => {
if (state.isVisible)
update(lastMatchedElement.value);
});
}
init();
export { events, isEnabled, lastMatchedElement, state };

View File

@@ -0,0 +1,44 @@
import { VNode } from 'vue';
type PositionInfo = [
source: string,
line: number,
column: number
];
interface Store {
hasData: boolean;
vnodeToPos: WeakMap<any, PositionInfo>;
fileToVNode: Map<string, WeakSet<any>>;
posToVNode: Map<string, Map<number, Map<number, WeakSet<any>>>>;
events?: any;
}
/**
* @internal
*/
declare function getInternalStore(): Store;
/**
* @internal
*/
declare function recordPosition(source: string, line: number, column: number, node: VNode): VNode;
declare class ElementTraceInfo {
pos: PositionInfo;
vnode: VNode | undefined;
el: Element | undefined;
constructor(pos: PositionInfo, el?: Element, vnode?: VNode);
get filepath(): string;
get fullpath(): string;
get rect(): DOMRect | undefined;
getElementsSameFile(): Element[] | undefined;
getParent(): ElementTraceInfo | undefined;
getElementsSamePosition(): Element[] | undefined;
}
declare function findTraceFromElement(el?: Element | null): ElementTraceInfo | undefined;
declare function findTraceFromVNode(vnode?: VNode, el?: Element): ElementTraceInfo | undefined;
declare function findTraceAtPointer(e: {
x: number;
y: number;
}): ElementTraceInfo | undefined;
declare function hasData(): boolean;
export { ElementTraceInfo, findTraceAtPointer, findTraceFromElement, findTraceFromVNode, getInternalStore, hasData, recordPosition };
export type { PositionInfo };

View File

@@ -0,0 +1,123 @@
const KEY_IGNORE = "data-v-inspector-ignore";
const KEY_GLOBAL = "__vue_tracer__";
let _store = globalThis[KEY_GLOBAL];
if (!_store) {
_store = {
hasData: false,
vnodeToPos: /* @__PURE__ */ new WeakMap(),
fileToVNode: /* @__PURE__ */ new Map(),
posToVNode: /* @__PURE__ */ new Map()
};
Object.defineProperty(globalThis, KEY_GLOBAL, {
value: _store,
configurable: true,
enumerable: false
});
}
function getInternalStore() {
return _store;
}
function recordPosition(source, line, column, node) {
if (!node || typeof node === "string" || typeof node === "number")
return node;
if (!_store.hasData)
_store.hasData = true;
const props = node.props ||= {};
_store.vnodeToPos.set(props, [source, line, column]);
if (!_store.fileToVNode.has(source))
_store.fileToVNode.set(source, /* @__PURE__ */ new WeakSet());
_store.fileToVNode.get(source).add(props);
if (!_store.posToVNode.has(source))
_store.posToVNode.set(source, /* @__PURE__ */ new Map());
const lineMap = _store.posToVNode.get(source);
if (!lineMap.has(line))
lineMap.set(line, /* @__PURE__ */ new Map());
const columnMap = lineMap.get(line);
if (!columnMap.has(column))
columnMap.set(column, /* @__PURE__ */ new WeakSet());
columnMap.get(column).add(props);
return node;
}
function getPositionFromVNode(node) {
const props = node?.props;
if (props)
return _store.vnodeToPos.get(props);
}
class ElementTraceInfo {
pos;
vnode;
el;
constructor(pos, el, vnode) {
this.vnode = vnode;
this.pos = pos;
this.el = el;
}
get filepath() {
return this.pos[0];
}
get fullpath() {
let path = this.pos[0];
if (this.pos[1]) {
path += `:${this.pos[1]}`;
if (this.pos[2])
path += `:${this.pos[2]}`;
}
return path;
}
get rect() {
return this.el?.getBoundingClientRect();
}
getElementsSameFile() {
const pos = this.pos;
const fileVNodeSet = _store.fileToVNode.get(pos[0]);
if (!fileVNodeSet)
return;
const sameFile = fileVNodeSet ? Array.from(document.querySelectorAll("*")).filter((e) => e !== this.el && e.__vnode?.props && fileVNodeSet.has(e.__vnode?.props)) : [];
return sameFile;
}
getParent() {
const parentVNode = this.vnode?.parent;
const parentEl = this.el?.parentElement;
return findTraceFromVNode(parentVNode) ?? findTraceFromElement(parentEl);
}
getElementsSamePosition() {
if (typeof this.vnode?.type !== "string")
return;
const pos = this.pos;
const posVNodeSet = _store.posToVNode.get(pos[0])?.get(pos[1])?.get(pos[2]);
if (!posVNodeSet)
return;
const samePos = posVNodeSet ? Array.from(document.querySelectorAll(this.vnode.type)).filter((e) => e !== this.el && e.__vnode?.props && posVNodeSet.has(e.__vnode?.props)) : [];
return samePos;
}
}
function findTraceFromElement(el) {
if (!el)
return;
const vnode = el.__vnode;
return findTraceFromVNode(vnode, el);
}
function findTraceFromVNode(vnode, el) {
if (!vnode)
return;
const pos = getPositionFromVNode(vnode);
if (!pos)
return;
return new ElementTraceInfo(pos, el ?? vnode?.el ?? void 0, vnode);
}
function findTraceAtPointer(e) {
let elements = document.elementsFromPoint(e.x, e.y);
const ignoreIndex = elements.findIndex((node) => node?.hasAttribute?.(KEY_IGNORE));
if (ignoreIndex !== -1)
elements = elements.slice(ignoreIndex);
for (const el of elements) {
const match = findTraceFromElement(el);
if (match)
return match;
}
}
function hasData() {
return _store.hasData;
}
export { ElementTraceInfo, findTraceAtPointer, findTraceFromElement, findTraceFromVNode, getInternalStore, hasData, recordPosition };

View File

@@ -0,0 +1,5 @@
import { DockClientScriptContext } from '@vitejs/devtools-kit/client';
declare function clientScriptSetup(ctx: DockClientScriptContext): void;
export { clientScriptSetup as default };

View File

@@ -0,0 +1,21 @@
import { events, isEnabled } from 'vite-plugin-vue-tracer/client/listeners';
import { state } from 'vite-plugin-vue-tracer/client/overlay';
function clientScriptSetup(ctx) {
ctx.current.events.on("entry:activated", () => {
events.on("click", (e) => {
ctx.rpc.call("vite:core:open-in-editor", `${e.pos[0]}:${e.pos[1]}:${e.pos[2]}`);
state.isVisible = false;
state.isEnabled = false;
ctx.docks.switchEntry(null);
});
isEnabled.value = true;
state.isVisible = true;
});
ctx.current.events.on("entry:deactivated", () => {
isEnabled.value = false;
state.isVisible = false;
});
}
export { clientScriptSetup as default };

26
node_modules/vite-plugin-vue-tracer/dist/index.d.mts generated vendored Normal file
View File

@@ -0,0 +1,26 @@
import { Plugin } from 'vite';
interface VueTracerOptions {
/**
* Enable this plugin or not, or only enable in certain environment.
*
* @default 'dev'
*/
enabled?: boolean | 'dev' | 'prod';
/**
* Resolve the record entry path to relative path.
* Normally, it should be true.
*
* @default true
*/
resolveRecordEntryPath?: boolean;
/**
* Enable Vite DevTools integration.
*
* @default false
*/
viteDevtools?: boolean;
}
declare function VueTracer(options?: VueTracerOptions): Plugin | undefined;
export { VueTracer, VueTracer as default };

115
node_modules/vite-plugin-vue-tracer/dist/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,115 @@
import process from 'node:process';
import { walk } from 'estree-walker';
import { resolveModulePath } from 'exsolve';
import MagicString from 'magic-string';
import { relative, dirname, isAbsolute } from 'pathe';
import { SourceMapConsumer } from 'source-map-js';
const functions = [
"h",
"_createElementVNode",
"_createElementBlock",
"_createBlock",
"_createVNode",
"_createStaticVNode"
];
const testRe = new RegExp(`\\b(?:${functions.join("|")})\\(`);
function VueTracer(options) {
let {
enabled = "dev",
resolveRecordEntryPath = true,
viteDevtools = false
} = options || {};
if (enabled === false)
return;
const pathRecordDist = resolveModulePath("vite-plugin-vue-tracer/client/record", { from: import.meta.url });
const getRecordPath = (id) => {
if (!resolveRecordEntryPath)
return "vite-plugin-vue-tracer/client/record";
let related = relative(dirname(id), pathRecordDist);
if (!related.startsWith("./") && !isAbsolute(related))
related = `./${related}`;
return related;
};
return {
name: "vite-plugin-vue-tracer",
enforce: "post",
configResolved(config) {
if (enabled === "dev")
enabled = config.command === "serve";
else if (enabled === "prod")
enabled = config.command === "build";
},
transform(code, id) {
if (!enabled)
return;
if (this.environment.name !== "client")
return;
if (!code.includes("_sfc_render("))
return;
if (!code.match(testRe))
return;
if (code.includes("_tracer("))
return;
function offsetToPos(index) {
const lines = code.slice(0, index).split("\n");
return {
line: lines.length,
column: lines[lines.length - 1].length
};
}
const map = this.getCombinedSourcemap();
const consumer = new SourceMapConsumer(map);
const s = new MagicString(code);
const ast = this.parse(code);
let hit = false;
walk(ast, {
enter(node) {
if (node.type !== "CallExpression" || node.callee.type !== "Identifier")
return;
if (!functions.includes(node.callee.name))
return;
const { start, end } = node;
const pos = offsetToPos(start);
const original = consumer.originalPositionFor(pos);
if (original.source === null)
return;
hit = true;
s.appendLeft(start, `_tracer(${original.line},${original.column},`);
s.appendRight(end, `)`);
}
});
if (!hit)
return;
const related = relative(process.cwd(), id);
s.prepend(`import { recordPosition as _tracerRecordPosition } from ${JSON.stringify(getRecordPath(id))}
`);
s.append(`
function _tracer(line, column, vnode) { return _tracerRecordPosition(${JSON.stringify(related)}, line, column, vnode) }
`);
return {
code: s.toString(),
map: s.generateMap({ hires: true })
};
},
// Vite DevTools integration
...viteDevtools ? {
devtools: {
setup(ctx) {
ctx.docks.register({
id: "vue-tracer",
title: "Vue Tracer",
icon: "ph:crosshair-simple-duotone",
type: "action",
action: {
importFrom: "vite-plugin-vue-tracer/client/vite-devtools",
importName: "default"
}
});
}
}
} : {}
};
}
export { VueTracer, VueTracer as default };