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..ff7d1940 --- /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/__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) + }) +}) diff --git a/packages/vite/src/node/hmr/tracker.ts b/packages/vite/src/node/hmr/tracker.ts new file mode 100644 index 00000000..acb12848 --- /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..c448431a --- /dev/null +++ b/packages/vite/src/node/rpc/functions/vite-hmr-clear.ts @@ -0,0 +1,17 @@ +import type { HmrTracker } from '../..//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..40f4d479 --- /dev/null +++ b/packages/vite/src/node/rpc/functions/vite-hmr-updates.ts @@ -0,0 +1,17 @@ +import type { HmrTracker } from '../../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 +}