diff --git a/src/client/components/Container.vue b/src/client/components/Container.vue index 9a04b5c..1e163c7 100644 --- a/src/client/components/Container.vue +++ b/src/client/components/Container.vue @@ -1,5 +1,9 @@ + + diff --git a/src/client/logic/rpc-static.ts b/src/client/logic/rpc-static.ts index 06db1e0..3f750d1 100644 --- a/src/client/logic/rpc-static.ts +++ b/src/client/logic/rpc-static.ts @@ -22,5 +22,8 @@ export function createStaticRpcClient(): RpcFunctions { resolveId: (query, id) => getModuleTransformInfo(query, id).then(r => r.resolvedId), onModuleUpdated: async () => undefined, getServerMetrics: async () => ({}), + getWaterfallInfo: async () => ({}), + getHmrEvents: async () => [], + list: () => null!, } } diff --git a/src/client/pages/index.vue b/src/client/pages/index.vue index 28134df..e7bd6ea 100644 --- a/src/client/pages/index.vue +++ b/src/client/pages/index.vue @@ -55,6 +55,9 @@ const isRoot = computed(() => route.path === '/') + +
+ diff --git a/src/client/pages/index/waterfall.vue b/src/client/pages/index/waterfall.vue new file mode 100644 index 0000000..1fcfba1 --- /dev/null +++ b/src/client/pages/index/waterfall.vue @@ -0,0 +1,322 @@ + + + diff --git a/src/client/stores/options.ts b/src/client/stores/options.ts index 7c2952a..e24d3f8 100644 --- a/src/client/stores/options.ts +++ b/src/client/stores/options.ts @@ -12,6 +12,8 @@ export interface ViewState { showBailout: boolean showOneColumn: boolean sort: 'default' | 'time-asc' | 'time-desc' + waterfallShowResolveId: boolean + waterfallStacking: boolean } export interface SearchState { @@ -37,6 +39,8 @@ export const useOptionsStore = defineStore('options', () => { showBailout: false, showOneColumn: false, sort: 'default', + waterfallShowResolveId: true, + waterfallStacking: true, }, { mergeDefaults: true }, ) diff --git a/src/node/context.ts b/src/node/context.ts index 8eb4ed2..824a41d 100644 --- a/src/node/context.ts +++ b/src/node/context.ts @@ -1,8 +1,9 @@ import type { Environment, ResolvedConfig } from 'vite' -import type { Metadata, ModuleInfo, PluginMetricInfo, QueryEnv, ResolveIdInfo, ServerMetrics, TransformInfo } from '../types' +import type { HmrEventInfo, Metadata, ModuleInfo, PluginMetricInfo, QueryEnv, ResolveIdInfo, ServerMetrics, TransformInfo, WaterfallInfo } from '../types' import type { ViteInspectOptions } from './options' import { Buffer } from 'node:buffer' import { resolve } from 'node:path' +import { objectMap } from '@antfu/utils' import { createFilter } from 'unplugin-utils' import { DUMMY_LOAD_PLUGIN_NAME } from './constants' import { removeVersionQuery, serializePlugin } from './utils' @@ -111,10 +112,12 @@ export class InspectContextViteEnv { transform: Record resolveId: Record transformCounter: Record + hmrEvents: HmrEventInfo[] } = { transform: {}, resolveId: {}, transformCounter: {}, + hmrEvents: [], } recordTransform(id: string, info: TransformInfo, preTransformCode: string) { @@ -147,6 +150,10 @@ export class InspectContextViteEnv { this.data.resolveId[id].push(info) } + recordHmrEvent(info: HmrEventInfo) { + this.data.hmrEvents.push(info) + } + invalidate(id: string) { id = this.normalizeId(id) delete this.data.transform[id] @@ -294,6 +301,53 @@ export class InspectContextViteEnv { } } + async getWaterfallInfo() { + const resolveIdByResult: Record = {} + for (const id in this.data.resolveId) { + const info = this.data.resolveId[id][0] + resolveIdByResult[info.result] = { + ...info, + result: id, + } + } + return objectMap(this.data.transform, (id, transforms) => { + const result: WaterfallInfo[string] = [] + let currentId = id + while (resolveIdByResult[currentId]) { + const info = resolveIdByResult[currentId] + result.push({ + name: info.name, + start: info.start, + end: info.end, + isResolveId: true, + }) + if (currentId === info.result) + break + currentId = info.result + } + for (const transform of transforms) { + result.push({ + name: transform.name, + start: transform.start, + end: transform.end, + isResolveId: false, + }) + } + result.sort((a, b) => a.start - b.start) + const filtered = result.filter(({ start, end }, i) => i === 0 || i === result.length - 1 || start < end) + return filtered.length + ? [ + id, + filtered, + ] + : undefined + }) + } + + async getHmrEvents() { + return this.data.hmrEvents.sort((a, b) => a.timestamp - b.timestamp) + } + clearId(_id: string) { const id = this.resolveId(_id) if (id) { diff --git a/src/node/index.ts b/src/node/index.ts index 1d529f7..7af6d46 100644 --- a/src/node/index.ts +++ b/src/node/index.ts @@ -233,13 +233,16 @@ export default function PluginInspect(options: ViteInspectOptions = {}): Plugin return null }, }, - hotUpdate({ modules, environment }) { + hotUpdate({ modules, type, file, timestamp }) { const ids = modules.map(module => module.id) - environment.hot.send({ + this.environment.hot.send({ type: 'custom', event: 'vite-plugin-inspect:update', data: { ids } as HMRData, }) + + const env = ctx.getEnvContext(this.environment) + env?.recordHmrEvent({ type, file, timestamp }) }, async buildEnd() { if (!build) diff --git a/src/node/rpc.ts b/src/node/rpc.ts index ccadc8f..621f834 100644 --- a/src/node/rpc.ts +++ b/src/node/rpc.ts @@ -25,6 +25,12 @@ export function createServerRpc( .data .serverMetrics || {} }, + async getWaterfallInfo(query) { + return ctx.queryEnv(query).getWaterfallInfo() + }, + async getHmrEvents(query) { + return ctx.queryEnv(query).getHmrEvents() + }, async onModuleUpdated() {}, async list() { diff --git a/src/node/utils.ts b/src/node/utils.ts index 872d233..278bc11 100644 --- a/src/node/utils.ts +++ b/src/node/utils.ts @@ -34,3 +34,25 @@ export function removeVersionQuery(url: string) { } return url } + +export function createFilter(pattern: string) { + pattern = pattern.trim() + if (!pattern) { + return () => true + } + const regex = new RegExp(pattern, 'i') + return (name: string) => regex.test(name) +} + +export function generatorHashColorByString(str: string) { + let hash = 0 + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash) + } + let color = '#' + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xFF + color += (`00${value.toString(16)}`).slice(-2) + } + return color +} diff --git a/src/types.ts b/src/types.ts index aa7e831..0963391 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,12 @@ export interface ResolveIdInfo { error?: ParsedError } +export interface HmrEventInfo { + type: 'create' | 'update' | 'delete' + file: string + timestamp: number +} + export interface ParsedError { message: string stack: StackFrame[] @@ -44,6 +50,13 @@ export interface ModuleTransformInfo { transforms: TransformInfo[] } +export type WaterfallInfo = Record + export interface PluginMetricInfo { name: string enforce?: string @@ -107,6 +120,8 @@ export interface RpcFunctions { getModuleTransformInfo: (query: QueryEnv, id: string, clear?: boolean) => Promise getPluginMetrics: (query: QueryEnv) => Promise getServerMetrics: (query: QueryEnv) => Promise + getWaterfallInfo: (query: QueryEnv) => Promise + getHmrEvents: (query: QueryEnv) => Promise resolveId: (query: QueryEnv, id: string) => Promise onModuleUpdated: () => Promise