From 3bbbedf76b739e14d5d3b4fa17f10f3d9b844e7d Mon Sep 17 00:00:00 2001 From: Arash Ari Sheyda Date: Thu, 4 Jun 2026 16:46:48 -0600 Subject: [PATCH 1/4] feat(vite): add HMR Inspector tab --- packages/vite/src/app/app.vue | 5 + packages/vite/src/app/pages/hmr.vue | 358 ++++++++++++++++++ packages/vite/src/modules/rpc.ts | 18 + packages/vite/src/node/hmr/tracker.ts | 37 ++ .../src/node/rpc/functions/vite-hmr-clear.ts | 17 + .../node/rpc/functions/vite-hmr-updates.ts | 17 + packages/vite/src/node/rpc/index.ts | 4 + packages/vite/src/shared/types.ts | 26 ++ 8 files changed, 482 insertions(+) create mode 100644 packages/vite/src/app/pages/hmr.vue create mode 100644 packages/vite/src/node/hmr/tracker.ts create mode 100644 packages/vite/src/node/rpc/functions/vite-hmr-clear.ts create mode 100644 packages/vite/src/node/rpc/functions/vite-hmr-updates.ts create mode 100644 packages/vite/src/shared/types.ts diff --git a/packages/vite/src/app/app.vue b/packages/vite/src/app/app.vue index 6ca0c929..bfba8511 100644 --- a/packages/vite/src/app/app.vue +++ b/packages/vite/src/app/app.vue @@ -20,6 +20,11 @@ useSideNav(() => { icon: 'i-ph-house-duotone', to: '/home', }, + { + title: 'HMR Inspector', + icon: 'i-ph-lightning-duotone', + to: '/hmr', + }, ] }) diff --git a/packages/vite/src/app/pages/hmr.vue b/packages/vite/src/app/pages/hmr.vue new file mode 100644 index 00000000..7f4ce2d5 --- /dev/null +++ b/packages/vite/src/app/pages/hmr.vue @@ -0,0 +1,358 @@ + + + diff --git a/packages/vite/src/modules/rpc.ts b/packages/vite/src/modules/rpc.ts index c56c63d3..5e2dc24a 100644 --- a/packages/vite/src/modules/rpc.ts +++ b/packages/vite/src/modules/rpc.ts @@ -1,5 +1,6 @@ import { addVitePlugin, defineNuxtModule } from '@nuxt/kit' import { DevToolsServer } from '../../../core/src/node/plugins/server' +import { createHmrTracker } from '../node/hmr/tracker' import { rpcFunctions } from '../node/rpc' export default defineNuxtModule({ @@ -8,10 +9,13 @@ export default defineNuxtModule({ configKey: 'devtoolsRpc', }, setup() { + const hmrTracker = createHmrTracker() + addVitePlugin({ name: 'vite:devtools:vite', devtools: { setup(ctx) { + ;(ctx as any).__hmrTracker = hmrTracker for (const fn of rpcFunctions) { ctx.rpc.register(fn as any) } @@ -19,6 +23,20 @@ export default defineNuxtModule({ }, }) + addVitePlugin({ + name: 'vite:devtools:hmr-tracker', + hotUpdate({ file, modules, timestamp }) { + if (modules.length > 0) { + hmrTracker.record({ + timestamp, + type: 'update', + files: [file], + modules: modules.map(m => m.id ?? m.url), + }) + } + }, + }) + addVitePlugin(DevToolsServer()) }, }) diff --git a/packages/vite/src/node/hmr/tracker.ts b/packages/vite/src/node/hmr/tracker.ts new file mode 100644 index 00000000..1e325355 --- /dev/null +++ b/packages/vite/src/node/hmr/tracker.ts @@ -0,0 +1,37 @@ +import type { HmrUpdate } from '~~/shared/types' + +/** Maximum number of HMR events retained in the circular buffer. */ +const MAX_HISTORY = 200 + +/** + * Creates an in-memory tracker that records HMR events from Vite's + * `hotUpdate` hook and exposes them to the client via RPC. + */ +export function createHmrTracker() { + const updates: HmrUpdate[] = [] + let counter = 0 + + /** Prepend a new update to the history, evicting the oldest entry if full. */ + function record(update: Omit) { + const entry: HmrUpdate = { ...update, id: String(++counter) } + updates.unshift(entry) + if (updates.length > MAX_HISTORY) { + updates.length = MAX_HISTORY + } + return entry + } + + /** Return all recorded updates, newest first. */ + function getUpdates() { + return updates + } + + /** Discard all recorded updates. */ + function clear() { + updates.length = 0 + } + + return { record, getUpdates, clear } +} + +export type HmrTracker = ReturnType diff --git a/packages/vite/src/node/rpc/functions/vite-hmr-clear.ts b/packages/vite/src/node/rpc/functions/vite-hmr-clear.ts new file mode 100644 index 00000000..d574f3fa --- /dev/null +++ b/packages/vite/src/node/rpc/functions/vite-hmr-clear.ts @@ -0,0 +1,17 @@ +import type { HmrTracker } from '~~/node/hmr/tracker' +import { defineRpcFunction } from '@vitejs/devtools-kit' + +/** Clears the recorded HMR update history. */ +export const viteHmrClear = defineRpcFunction({ + name: 'vite:hmr-clear', + type: 'action', + jsonSerializable: true, + setup: (context) => { + const tracker: HmrTracker | undefined = (context as any).__hmrTracker + return { + handler: async () => { + tracker?.clear() + }, + } + }, +}) diff --git a/packages/vite/src/node/rpc/functions/vite-hmr-updates.ts b/packages/vite/src/node/rpc/functions/vite-hmr-updates.ts new file mode 100644 index 00000000..08a1fc37 --- /dev/null +++ b/packages/vite/src/node/rpc/functions/vite-hmr-updates.ts @@ -0,0 +1,17 @@ +import type { HmrTracker } from '~~/node/hmr/tracker' +import { defineRpcFunction } from '@vitejs/devtools-kit' + +/** Returns the current list of recorded HMR updates. */ +export const viteHmrUpdates = defineRpcFunction({ + name: 'vite:hmr-updates', + type: 'query', + jsonSerializable: true, + setup: (context) => { + const tracker: HmrTracker | undefined = (context as any).__hmrTracker + return { + handler: async () => { + return tracker?.getUpdates() ?? [] + }, + } + }, +}) diff --git a/packages/vite/src/node/rpc/index.ts b/packages/vite/src/node/rpc/index.ts index b68ece44..e803eadf 100644 --- a/packages/vite/src/node/rpc/index.ts +++ b/packages/vite/src/node/rpc/index.ts @@ -1,11 +1,15 @@ import type { RpcDefinitionsToFunctions } from '@vitejs/devtools-kit' import { viteEnvInfo } from './functions/vite-env-info' +import { viteHmrClear } from './functions/vite-hmr-clear' +import { viteHmrUpdates } from './functions/vite-hmr-updates' import { viteMetaInfo } from './functions/vite-meta-info' import '@vitejs/devtools-kit' export const rpcFunctions = [ viteMetaInfo, viteEnvInfo, + viteHmrUpdates, + viteHmrClear, ] as const export type ServerFunctions = RpcDefinitionsToFunctions diff --git a/packages/vite/src/shared/types.ts b/packages/vite/src/shared/types.ts new file mode 100644 index 00000000..66fc85ab --- /dev/null +++ b/packages/vite/src/shared/types.ts @@ -0,0 +1,26 @@ +export interface HmrUpdate { + /** + * Auto-incremented identifier, unique within the current session. + */ + id: string + /** + * Unix timestamp (ms) when the update was received. + */ + timestamp: number + /** + * Whether the change was a hot module replacement or a full page reload. + */ + type: 'update' | 'full-reload' + /** + * Absolute paths of the files that triggered the update. + */ + files: string[] + /** + * Module IDs (or URLs) invalidated by the change. + */ + modules: string[] + /** + * Time in milliseconds the update took to apply, if measured. + */ + duration?: number +} From 4eab9a571c2ce776d6570388cfc6185f0e3d5f1f Mon Sep 17 00:00:00 2001 From: Arash Ari Sheyda Date: Thu, 4 Jun 2026 16:50:33 -0600 Subject: [PATCH 2/4] feat(hmr): add tests --- .../src/node/hmr/__tests__/tracker.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 packages/vite/src/node/hmr/__tests__/tracker.test.ts diff --git a/packages/vite/src/node/hmr/__tests__/tracker.test.ts b/packages/vite/src/node/hmr/__tests__/tracker.test.ts new file mode 100644 index 00000000..62e6f262 --- /dev/null +++ b/packages/vite/src/node/hmr/__tests__/tracker.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest' +import { createHmrTracker } from '../tracker' + +describe('createHmrTracker', () => { + it('should store updates newest first', () => { + const tracker = createHmrTracker() + + tracker.record({ timestamp: 1000, type: 'update', files: ['a.ts'], modules: [] }) + tracker.record({ timestamp: 2000, type: 'update', files: ['b.ts'], modules: [] }) + + const updates = tracker.getUpdates() + expect(updates).toHaveLength(2) + expect(updates[0]?.files[0]).toBe('b.ts') + expect(updates[1]?.files[0]).toBe('a.ts') + }) + + it('should evict oldest entries when exceeding max history', () => { + const tracker = createHmrTracker() + + for (let i = 0; i < 210; i++) { + tracker.record({ timestamp: i, type: 'update', files: [`file-${i}.ts`], modules: [] }) + } + + const updates = tracker.getUpdates() + expect(updates).toHaveLength(200) + expect(updates[0]?.files[0]).toBe('file-209.ts') + expect(updates[199]?.files[0]).toBe('file-10.ts') + }) + + it('should clear all updates', () => { + const tracker = createHmrTracker() + + tracker.record({ timestamp: 1000, type: 'update', files: ['a.ts'], modules: [] }) + tracker.record({ timestamp: 2000, type: 'update', files: ['b.ts'], modules: [] }) + + tracker.clear() + expect(tracker.getUpdates()).toHaveLength(0) + }) +}) From d0884111619bb3de253fe95b55e9dc94e86c3bc6 Mon Sep 17 00:00:00 2001 From: Arash Ari Sheyda Date: Thu, 4 Jun 2026 16:58:11 -0600 Subject: [PATCH 3/4] chore: lint --- packages/vite/src/app/pages/hmr.vue | 2 +- packages/vite/src/node/hmr/tracker.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/app/pages/hmr.vue b/packages/vite/src/app/pages/hmr.vue index 7f4ce2d5..ff7d1940 100644 --- a/packages/vite/src/app/pages/hmr.vue +++ b/packages/vite/src/app/pages/hmr.vue @@ -1,5 +1,5 @@