import type { AppRecord, BackendContext } from '@vue-devtools/app-backend-api' import { BridgeEvents, HookEvents, SharedData, isBrowser, stringify } from '@vue-devtools/shared-utils' import type { App, ID, TimelineEventOptions, WithId } from '@vue/devtools-api' import { isPerformanceSupported, now } from '@vue/devtools-api' import { hook } from './global-hook' import { getAppRecord, getAppRecordId } from './app' import { builtinLayers } from './timeline-builtins' export function setupTimeline(ctx: BackendContext) { setupBuiltinLayers(ctx) } export function addBuiltinLayers(appRecord: AppRecord, ctx: BackendContext) { for (const layerDef of builtinLayers) { ctx.timelineLayers.push({ ...layerDef, appRecord, plugin: null, events: [], }) } } function setupBuiltinLayers(ctx: BackendContext) { if (isBrowser) { (['mousedown', 'mouseup', 'click', 'dblclick'] as const).forEach((eventType) => { window.addEventListener(eventType, async (event: MouseEvent) => { await addTimelineEvent({ layerId: 'mouse', event: { time: now(), data: { type: eventType, x: event.clientX, y: event.clientY, }, title: eventType, }, }, null, ctx) }, { capture: true, passive: true, }) }) ;(['keyup', 'keydown', 'keypress'] as const).forEach((eventType) => { window.addEventListener(eventType, async (event: KeyboardEvent) => { await addTimelineEvent({ layerId: 'keyboard', event: { time: now(), data: { type: eventType, key: event.key, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, altKey: event.altKey, metaKey: event.metaKey, }, title: event.key, }, }, null, ctx) }, { capture: true, passive: true, }) }) } hook.on(HookEvents.COMPONENT_EMIT, async (app, instance, event, params) => { try { if (!SharedData.componentEventsEnabled) { return } const appRecord = await getAppRecord(app, ctx) if (!appRecord) { return } const componentId = `${appRecord.id}:${instance.uid}` const componentDisplay = (await appRecord.backend.api.getComponentName(instance)) || 'Unknown Component' await addTimelineEvent({ layerId: 'component-event', event: { time: now(), data: { component: { _custom: { type: 'component-definition', display: componentDisplay, }, }, event, params, }, title: event, subtitle: `by ${componentDisplay}`, meta: { componentId, bounds: await appRecord.backend.api.getComponentBounds(instance), }, }, }, app, ctx) } catch (e) { if (SharedData.debugInfo) { console.error(e) } } }) } export async function sendTimelineLayers(ctx: BackendContext) { const layers = [] for (const layer of ctx.timelineLayers) { try { layers.push({ id: layer.id, label: layer.label, color: layer.color, appId: layer.appRecord?.id, pluginId: layer.plugin?.descriptor.id, groupsOnly: layer.groupsOnly, skipScreenshots: layer.skipScreenshots, ignoreNoDurationGroups: layer.ignoreNoDurationGroups, }) } catch (e) { if (SharedData.debugInfo) { console.error(e) } } } ctx.bridge.send(BridgeEvents.TO_FRONT_TIMELINE_LAYER_LIST, { layers, }) } export async function addTimelineEvent(options: TimelineEventOptions, app: App, ctx: BackendContext) { if (!SharedData.timelineRecording) { return } const appId = app ? getAppRecordId(app) : null const isAllApps = options.all || !app || appId == null const id = ctx.nextTimelineEventId++ const eventData: TimelineEventOptions & WithId = { id, ...options, all: isAllApps, } ctx.timelineEventMap.set(eventData.id, eventData) ctx.bridge.send(BridgeEvents.TO_FRONT_TIMELINE_EVENT, { appId: eventData.all ? 'all' : appId, layerId: eventData.layerId, event: mapTimelineEvent(eventData), }) const layer = ctx.timelineLayers.find(l => (isAllApps || l.appRecord?.options.app === app) && l.id === options.layerId) if (layer) { layer.events.push(eventData) } else if (SharedData.debugInfo) { console.warn(`Timeline layer ${options.layerId} not found`) } } const initialTime = Date.now() export const dateThreshold = initialTime - 1_000_000 export const perfTimeDiff = initialTime - now() function mapTimelineEvent(eventData: TimelineEventOptions & WithId) { let time = eventData.event.time if (isPerformanceSupported() && time < dateThreshold) { time += perfTimeDiff } return { id: eventData.id, time: Math.round(time * 1000), logType: eventData.event.logType, groupId: eventData.event.groupId, title: eventData.event.title, subtitle: eventData.event.subtitle, } } export async function clearTimeline(ctx: BackendContext) { ctx.timelineEventMap.clear() for (const layer of ctx.timelineLayers) { layer.events = [] } for (const backend of ctx.backends) { await backend.api.clearTimeline() } } export async function sendTimelineEventData(id: ID, ctx: BackendContext) { if (!SharedData.timelineRecording) { return } let data = null const eventData = ctx.timelineEventMap.get(id) if (eventData) { data = await ctx.currentAppRecord.backend.api.inspectTimelineEvent(eventData, ctx.currentAppRecord.options.app) data = stringify(data) } else if (SharedData.debugInfo) { console.warn(`Event ${id} not found`, ctx.timelineEventMap.keys()) } ctx.bridge.send(BridgeEvents.TO_FRONT_TIMELINE_EVENT_DATA, { eventId: id, data, }) } export function removeLayersForApp(app: App, ctx: BackendContext) { const layers = ctx.timelineLayers.filter(l => l.appRecord?.options.app === app) for (const layer of layers) { const index = ctx.timelineLayers.indexOf(layer) if (index !== -1) { ctx.timelineLayers.splice(index, 1) } for (const e of layer.events) { ctx.timelineEventMap.delete(e.id) } } } export function sendTimelineLayerEvents(appId: string, layerId: string, ctx: BackendContext) { if (!SharedData.timelineRecording) { return } const app = ctx.appRecords.find(ar => ar.id === appId)?.options.app if (!app) { return } const layer = ctx.timelineLayers.find(l => l.appRecord?.options.app === app && l.id === layerId) if (!layer) { return } ctx.bridge.send(BridgeEvents.TO_FRONT_TIMELINE_LAYER_LOAD_EVENTS, { appId, layerId, events: layer.events.map(e => mapTimelineEvent(e)), }) }