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/5] 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 6ca0c929e..bfba85111 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 000000000..7f4ce2d53 --- /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 c56c63d31..5e2dc24a0 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 000000000..1e325355e --- /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 000000000..d574f3faf --- /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 000000000..08a1fc378 --- /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 b68ece449..e803eadfc 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 000000000..66fc85abd --- /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/5] 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 000000000..62e6f2621 --- /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/5] 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 7f4ce2d53..ff7d1940b 100644 --- a/packages/vite/src/app/pages/hmr.vue +++ b/packages/vite/src/app/pages/hmr.vue @@ -1,5 +1,5 @@ diff --git a/packages/ui/src/utils/plugin-color.ts b/packages/ui/src/utils/plugin-color.ts new file mode 100644 index 000000000..7bd505b0a --- /dev/null +++ b/packages/ui/src/utils/plugin-color.ts @@ -0,0 +1,46 @@ +import { getHashColorFromString, getHsla } from './color' + +/** + * Predefined color map for matching the branding + * + * Accept a 6-digit hex color string or a hue number + * Hue numbers are preferred because they will adapt better contrast in light/dark mode + * + * Hue numbers reference: + * - 0: red + * - 30: orange + * - 60: yellow + * - 120: green + * - 180: cyan + * - 240: blue + * - 270: purple + */ +export const predefinedColorMap = { + error: 0, + client: 60, + bailout: -1, + ssr: 270, + vite: 250, + vite1: 240, + vite2: 120, + rolldown: 18, + virtual: 160, + nuxt: 140, +} as Record + +export function getPluginColor(name: string, opacity = 1): string { + name = name.replace(/[^a-z]+/gi, '').toLowerCase() + if (name in predefinedColorMap) { + const color = predefinedColorMap[name]! + if (typeof color === 'number') { + return getHsla(color, opacity) + } + else { + if (opacity === 1) + return color + const opacityHex = Math.floor(opacity * 255).toString(16).padStart(2, '0') + return color + opacityHex + } + } + return getHashColorFromString(name, opacity) +} diff --git a/packages/vite/package.json b/packages/vite/package.json index 9c3451454..70f4c6fc6 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -35,8 +35,10 @@ "prepack": "pnpm build" }, "dependencies": { + "@dagrejs/dagre": "catalog:frontend", "@floating-ui/dom": "catalog:frontend", "@vitejs/devtools-kit": "workspace:*", + "@vue-flow/core": "catalog:frontend", "birpc": "catalog:deps", "devframe": "catalog:deps", "envinfo": "catalog:deps", diff --git a/packages/vite/src/app/components/HmrPropagationGraph.vue b/packages/vite/src/app/components/HmrPropagationGraph.vue new file mode 100644 index 000000000..791241a87 --- /dev/null +++ b/packages/vite/src/app/components/HmrPropagationGraph.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/packages/vite/src/app/pages/hmr.vue b/packages/vite/src/app/pages/hmr.vue index ff7d1940b..f1a9ee705 100644 --- a/packages/vite/src/app/pages/hmr.vue +++ b/packages/vite/src/app/pages/hmr.vue @@ -1,13 +1,16 @@ + + diff --git a/packages/vite/src/modules/rpc.ts b/packages/vite/src/modules/rpc.ts index 5e2dc24a0..f68c2d578 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 { createHmrTrackerPlugin } from '../node/hmr/plugin' import { createHmrTracker } from '../node/hmr/tracker' import { rpcFunctions } from '../node/rpc' @@ -23,19 +24,7 @@ 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(createHmrTrackerPlugin(hmrTracker)) addVitePlugin(DevToolsServer()) }, diff --git a/packages/vite/src/node/hmr/__tests__/plugin.test.ts b/packages/vite/src/node/hmr/__tests__/plugin.test.ts new file mode 100644 index 000000000..1ff206673 --- /dev/null +++ b/packages/vite/src/node/hmr/__tests__/plugin.test.ts @@ -0,0 +1,94 @@ +import type { EnvironmentModuleNode, Plugin, ResolvedConfig } from 'vite' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'pathe' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { createHmrTrackerPlugin } from '../plugin' +import { createHmrTracker } from '../tracker' + +function moduleNode(id: string, selfAccepting = false): EnvironmentModuleNode { + return { + id, + url: id, + type: 'js', + isSelfAccepting: selfAccepting, + importers: new Set(), + acceptedHmrDeps: new Set(), + acceptedHmrExports: null, + } as EnvironmentModuleNode +} + +function hook any>(value: T | { handler: T } | undefined): T { + return typeof value === 'function' ? value : value!.handler +} + +describe('hMR tracking plugin', () => { + let workspace: string + let tracker: ReturnType + let plugin: Plugin + + beforeEach(() => { + workspace = mkdtempSync(join(tmpdir(), 'devtools-hmr-')) + writeFileSync(join(workspace, 'pnpm-workspace.yaml'), 'packages:\n - apps/*\n') + mkdirSync(join(workspace, 'apps/client'), { recursive: true }) + tracker = createHmrTracker() + plugin = createHmrTrackerPlugin(tracker) + // Vite invokes configuration and HMR hooks with separate context objects. + Reflect.apply(hook(plugin.configResolved), {}, [{ root: join(workspace, 'apps/client') } as ResolvedConfig]) + }) + + afterEach(() => rmSync(workspace, { recursive: true, force: true })) + + function update(modules: EnvironmentModuleNode[]) { + Reflect.apply(hook(plugin.hotUpdate), {}, [{ + type: 'update', + file: modules[0]!.id, + modules, + timestamp: 1000, + }]) + return tracker.getUpdates()[0]! + } + + it('records paths relative to the workspace, across hook contexts', () => { + const source = moduleNode(join(workspace, 'apps/client/src/main.ts'), true) + const result = update([source]) + expect(result.files).toEqual(['apps/client/src/main.ts']) + expect(result.modules).toEqual(result.files) + expect(result.boundaries).toEqual(result.files) + }) + + it('marks a self-accepting importer as a boundary and stops there', () => { + const source = moduleNode(join(workspace, 'dep.ts')) + const boundary = moduleNode(join(workspace, 'component.vue'), true) + source.importers.add(boundary) + boundary.importers.add(moduleNode(join(workspace, 'app.ts'))) + const result = update([source]) + expect(result.boundaries).toEqual(['component.vue']) + expect(result.graph.nodes.map(({ id, type }) => ({ id, type }))).toEqual([ + { id: 'dep.ts', type: 'source' }, + { id: 'component.vue', type: 'boundary' }, + ]) + expect(result.graph.edges).toEqual([{ from: 'component.vue', to: 'dep.ts' }]) + }) + + it('preserves the boundary role when an importer is also an updated module', () => { + const source = moduleNode(join(workspace, 'dep.ts')) + const boundary = moduleNode(join(workspace, 'component.vue'), true) + source.importers.add(boundary) + const result = update([source, boundary]) + expect(result.graph.nodes.find(node => node.id === 'component.vue')?.type).toBe('boundary') + }) + + it('stops at a dependency-accepting boundary', () => { + const source = moduleNode(join(workspace, 'dep.ts')) + const boundary = moduleNode(join(workspace, 'accept.ts')) + boundary.acceptedHmrDeps.add(source) + source.importers.add(boundary) + const result = update([source]) + expect(result.boundaries).toEqual(['accept.ts']) + expect(result.graph.nodes.find(node => node.id === 'accept.ts')).toMatchObject({ + type: 'boundary', + acceptedDeps: ['dep.ts'], + }) + }) +}) diff --git a/packages/vite/src/node/hmr/__tests__/tracker.test.ts b/packages/vite/src/node/hmr/__tests__/tracker.test.ts index 62e6f2621..4fdb4ff8b 100644 --- a/packages/vite/src/node/hmr/__tests__/tracker.test.ts +++ b/packages/vite/src/node/hmr/__tests__/tracker.test.ts @@ -1,16 +1,29 @@ import { describe, expect, it } from 'vitest' import { createHmrTracker } from '../tracker' +function update(overrides = {}) { + return { + timestamp: 1000, + type: 'update' as const, + files: ['a.ts'], + modules: [], + boundaries: [], + graph: { nodes: [], edges: [] }, + ...overrides, + } +} + 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: [] }) + tracker.record(update({ files: ['a.ts'] })) + tracker.record(update({ timestamp: 2000, files: ['b.ts'], boundaries: ['b.ts'] })) const updates = tracker.getUpdates() expect(updates).toHaveLength(2) expect(updates[0]?.files[0]).toBe('b.ts') + expect(updates[0]?.boundaries).toEqual(['b.ts']) expect(updates[1]?.files[0]).toBe('a.ts') }) @@ -18,7 +31,7 @@ describe('createHmrTracker', () => { const tracker = createHmrTracker() for (let i = 0; i < 210; i++) { - tracker.record({ timestamp: i, type: 'update', files: [`file-${i}.ts`], modules: [] }) + tracker.record(update({ timestamp: i, files: [`file-${i}.ts`] })) } const updates = tracker.getUpdates() @@ -30,8 +43,8 @@ describe('createHmrTracker', () => { 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.record(update()) + tracker.record(update({ timestamp: 2000, files: ['b.ts'] })) tracker.clear() expect(tracker.getUpdates()).toHaveLength(0) diff --git a/packages/vite/src/node/hmr/plugin.ts b/packages/vite/src/node/hmr/plugin.ts new file mode 100644 index 000000000..2391899ca --- /dev/null +++ b/packages/vite/src/node/hmr/plugin.ts @@ -0,0 +1,93 @@ +import type { EnvironmentModuleNode, Plugin } from 'vite' +import type { HmrGraphEdge, HmrGraphNode } from '../../shared/types' +import type { HmrTracker } from './tracker' +import { isAbsolute, relative } from 'pathe' +import { searchForWorkspaceRoot } from 'vite' + +export function createHmrTrackerPlugin(tracker: HmrTracker): Plugin { + let root: string + return { + name: 'vite:devtools:hmr-tracker', + configResolved(config) { + root = searchForWorkspaceRoot(config.root) + }, + hotUpdate({ type: changeType, file, modules, timestamp }) { + if (modules.length === 0) + return + const rel = (p: string) => isAbsolute(p) ? relative(root, p) : p + const decode = (s: string) => { + try { + return decodeURIComponent(s) + } + catch { + return s + } + } + const modId = (m: EnvironmentModuleNode) => rel(decode(m.id ?? m.url)) + + // Walk the module graph to build propagation path + const boundaries = new Set() + const graphNodes = new Map() + const graphEdges: HmrGraphEdge[] = [] + const visited = new Set() + + function nodeData(mod: EnvironmentModuleNode, role: HmrGraphNode['type']): HmrGraphNode { + const id = modId(mod) + const node: HmrGraphNode = { id, type: role, moduleType: mod.type, selfAccepting: !!mod.isSelfAccepting, importersCount: mod.importers.size } + const deps = [...mod.acceptedHmrDeps].map(d => modId(d)) + if (deps.length) + node.acceptedDeps = deps + if (mod.acceptedHmrExports !== null && mod.acceptedHmrExports !== undefined) + node.acceptedExports = [...mod.acceptedHmrExports] + return node + } + + function walk(mod: EnvironmentModuleNode) { + if (visited.has(mod)) + return + visited.add(mod) + + const id = modId(mod) + + if (mod.isSelfAccepting) { + boundaries.add(id) + graphNodes.set(id, nodeData(mod, 'boundary')) + return + } + + for (const importer of mod.importers) { + const importerId = modId(importer) + graphEdges.push({ from: importerId, to: id }) + + if (importer.acceptedHmrDeps.has(mod)) { + boundaries.add(importerId) + graphNodes.set(importerId, nodeData(importer, 'boundary')) + } + else { + if (!graphNodes.has(importerId)) + graphNodes.set(importerId, nodeData(importer, 'intermediate')) + walk(importer) + } + } + } + + const relFile = rel(file) + for (const mod of modules) { + graphNodes.set(modId(mod), nodeData(mod, 'source')) + } + for (const mod of modules) { + walk(mod) + } + + tracker.record({ + timestamp, + type: 'update', + changeType, + files: [relFile], + modules: modules.map(m => modId(m)), + boundaries: [...boundaries], + graph: { nodes: [...graphNodes.values()], edges: graphEdges }, + }) + }, + } +} diff --git a/packages/vite/src/shared/types.ts b/packages/vite/src/shared/types.ts index 66fc85abd..be8e0af35 100644 --- a/packages/vite/src/shared/types.ts +++ b/packages/vite/src/shared/types.ts @@ -1,26 +1,44 @@ +/** A node in the HMR propagation graph. */ +export interface HmrGraphNode { + /** Module ID (relative to project root). */ + id: string + /** Role of the node in the propagation. */ + type: 'source' | 'intermediate' | 'boundary' + /** Vite module type. */ + moduleType?: 'js' | 'css' | 'asset' + /** Whether this module accepts its own updates. */ + selfAccepting?: boolean + /** Module IDs this boundary accepts as HMR deps. */ + acceptedDeps?: string[] + /** Exports accepted for partial HMR (null = full accept). */ + acceptedExports?: string[] | null + /** Number of modules that import this one. */ + importersCount?: number +} + +/** A directed edge in the HMR propagation graph. */ +export interface HmrGraphEdge { + /** Importing module (relative to project root). */ + from: string + /** Imported module (relative to project root). */ + to: string +} + export interface HmrUpdate { - /** - * Auto-incremented identifier, unique within the current session. - */ + /** Auto-incremented identifier, unique within the current session. */ id: string - /** - * Unix timestamp (ms) when the update was received. - */ + /** Unix timestamp (ms) when the update was received. */ timestamp: number - /** - * Whether the change was a hot module replacement or a full page reload. - */ + /** 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. - */ + /** How the file changed on disk. */ + changeType?: 'create' | 'update' | 'delete' + /** File paths (relative to project root) that triggered the update. */ files: string[] - /** - * Module IDs (or URLs) invalidated by the change. - */ + /** Module IDs (relative to project root) invalidated by the change. */ modules: string[] - /** - * Time in milliseconds the update took to apply, if measured. - */ - duration?: number + /** Module IDs that accepted the update (HMR boundaries). */ + boundaries: string[] + /** Propagation graph from changed file to HMR boundaries. */ + graph: { nodes: HmrGraphNode[], edges: HmrGraphEdge[] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5bb5451f7..fb57b5641 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,6 +184,9 @@ catalogs: specifier: ^2.0.17 version: 2.0.17 frontend: + '@dagrejs/dagre': + specifier: ^3.0.0 + version: 3.0.0 '@floating-ui/dom': specifier: ^1.7.6 version: 1.7.6 @@ -196,6 +199,9 @@ catalogs: '@visual-json/vue': specifier: ^0.4.0 version: 0.4.0 + '@vue-flow/core': + specifier: ^1.48.2 + version: 1.48.2 '@vueuse/components': specifier: ^14.3.0 version: 14.3.0 @@ -1040,12 +1046,18 @@ importers: packages/vite: dependencies: + '@dagrejs/dagre': + specifier: catalog:frontend + version: 3.0.0 '@floating-ui/dom': specifier: catalog:frontend version: 1.7.6 '@vitejs/devtools-kit': specifier: workspace:* version: link:../kit + '@vue-flow/core': + specifier: catalog:frontend + version: 1.48.2(vue@3.5.38(typescript@6.0.3)) birpc: specifier: catalog:deps version: 4.0.0 @@ -1372,6 +1384,12 @@ packages: '@colordx/core@5.4.3': resolution: {integrity: sha512-kIxYSfA5T8HXjav55UaaH/o/cKivF6jCCGIb8eqtcsfI46wsvlSiT8jMDyrl779qLec3c2c2oHBZo4oAhvbjrQ==} + '@dagrejs/dagre@3.0.0': + resolution: {integrity: sha512-ZzhnTy1rfuoew9Ez3EIw4L2znPGnYYhfn8vc9c4oB8iw6QAsszbiU0vRhlxWPFnmmNSFAkrYeF1PhM5m4lAN0Q==} + + '@dagrejs/graphlib@4.0.1': + resolution: {integrity: sha512-IvcV6FduIIAmLwnH+yun+QtV36SC7mERqa86aClNqmMN09WhmPPYU8ckHrZBozErf+UvHPWOTJYaGYiIcs0DgA==} + '@devframes/hub@0.5.4': resolution: {integrity: sha512-NZs5RFuNb6tjQd2JBsRYQT+OqE+z16Kg9/72HG4k+uJdzK90sAGtAjOwK8bsvav/9l85KGx4agfkgxnfbl313w==} peerDependencies: @@ -3606,6 +3624,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} @@ -4070,6 +4091,11 @@ packages: '@volar/typescript@2.4.28': resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + '@vue-flow/core@1.48.2': + resolution: {integrity: sha512-raxhgKWE+G/mcEvXJjGFUDYW9rAI3GOtiHR3ZkNpwBWuIaCC1EYiBmKGwJOoNzVFgwO7COgErnK7i08i287AFA==} + peerDependencies: + vue: ^3.3.0 + '@vue-macros/common@3.1.2': resolution: {integrity: sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==} engines: {node: '>=20.19.0'} @@ -4149,6 +4175,9 @@ packages: peerDependencies: vue: ^3.5.0 + '@vueuse/core@10.11.1': + resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} + '@vueuse/core@12.8.2': resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} @@ -4204,6 +4233,9 @@ packages: universal-cookie: optional: true + '@vueuse/metadata@10.11.1': + resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} + '@vueuse/metadata@12.8.2': resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} @@ -4225,6 +4257,9 @@ packages: vue: ^3.5.0 vue-router: ^4.0.0 || ^5.0.0 + '@vueuse/shared@10.11.1': + resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} + '@vueuse/shared@12.8.2': resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} @@ -8779,6 +8814,12 @@ snapshots: '@colordx/core@5.4.3': {} + '@dagrejs/dagre@3.0.0': + dependencies: + '@dagrejs/graphlib': 4.0.1 + + '@dagrejs/graphlib@4.0.1': {} + '@devframes/hub@0.5.4(devframe@0.5.4(typescript@6.0.3))': dependencies: birpc: 4.0.0 @@ -10953,6 +10994,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/web-bluetooth@0.0.20': {} + '@types/web-bluetooth@0.0.21': {} '@types/webextension-polyfill@0.8.3': {} @@ -11793,6 +11836,17 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 + '@vue-flow/core@1.48.2(vue@3.5.38(typescript@6.0.3))': + dependencies: + '@vueuse/core': 10.11.1(vue@3.5.38(typescript@6.0.3)) + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + vue: 3.5.38(typescript@6.0.3) + transitivePeerDependencies: + - '@vue/composition-api' + '@vue-macros/common@3.1.2(vue@3.5.38(typescript@6.0.3))': dependencies: '@vue/compiler-sfc': 3.5.38 @@ -11929,6 +11983,16 @@ snapshots: '@vueuse/shared': 14.3.0(vue@3.5.38(typescript@6.0.3)) vue: 3.5.38(typescript@6.0.3) + '@vueuse/core@10.11.1(vue@3.5.38(typescript@6.0.3))': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 10.11.1 + '@vueuse/shared': 10.11.1(vue@3.5.38(typescript@6.0.3)) + vue-demi: 0.14.10(vue@3.5.38(typescript@6.0.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + '@vueuse/core@12.8.2(typescript@6.0.3)': dependencies: '@types/web-bluetooth': 0.0.21 @@ -11963,6 +12027,8 @@ snapshots: fuse.js: 7.4.2 idb-keyval: 6.2.5 + '@vueuse/metadata@10.11.1': {} + '@vueuse/metadata@12.8.2': {} '@vueuse/metadata@14.2.1': {} @@ -11997,6 +12063,13 @@ snapshots: vue: 3.5.38(typescript@6.0.3) vue-router: 5.1.0(@vue/compiler-sfc@3.5.38)(vite@8.0.16(@types/node@25.0.3)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.38(typescript@6.0.3)) + '@vueuse/shared@10.11.1(vue@3.5.38(typescript@6.0.3))': + dependencies: + vue-demi: 0.14.10(vue@3.5.38(typescript@6.0.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + '@vueuse/shared@12.8.2(typescript@6.0.3)': dependencies: vue: 3.5.38(typescript@6.0.3) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ab2dbf070..41ff4392c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -111,10 +111,12 @@ catalogs: vitepress-plugin-group-icons: ^1.7.5 vitepress-plugin-mermaid: ^2.0.17 frontend: + '@dagrejs/dagre': ^3.0.0 '@floating-ui/dom': ^1.7.6 '@json-render/core': 0.13.0 '@json-render/vue': 0.13.0 '@visual-json/vue': ^0.4.0 + '@vue-flow/core': ^1.48.2 '@vueuse/components': ^14.3.0 '@vueuse/core': ^14.3.0 '@vueuse/router': ^14.3.0