From aee9b7a4084ae267eb4adc4622a19743e80c9a8b Mon Sep 17 00:00:00 2001 From: libondev Date: Tue, 10 Jun 2025 14:19:34 +0800 Subject: [PATCH 1/4] feat: add auto-hmr-plugin and autoHmr option --- src/auto-hmr/index.ts | 101 ++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 9 ++++ src/options.ts | 6 +++ 3 files changed, 116 insertions(+) create mode 100644 src/auto-hmr/index.ts diff --git a/src/auto-hmr/index.ts b/src/auto-hmr/index.ts new file mode 100644 index 000000000..4e8b08e75 --- /dev/null +++ b/src/auto-hmr/index.ts @@ -0,0 +1,101 @@ +import type { UnpluginOptions } from 'unplugin' +import type { VariableDeclarator, ImportDeclaration } from 'estree' + +function nameFromDeclaration(node?: VariableDeclarator) { + return node?.id.type === 'Identifier' ? node.id.name : '' +} + +function getRouterDeclaration(nodes?: VariableDeclarator[]) { + return nodes?.find( + (x) => + x.init?.type === 'CallExpression' && + x.init.callee.type === 'Identifier' && + x.init.callee.name === 'createRouter' + ) +} + +function getHandleHotUpdateDeclaration(node?: ImportDeclaration, modulePath?: string) { + return ( + node?.type === 'ImportDeclaration' && + node.source.value === modulePath && + node.specifiers.some( + (x) => + x.type === 'ImportSpecifier' && + x.imported.type === 'Identifier' && + x.imported.name === 'handleHotUpdate' + ) + ) +} + +interface AutoHmrPluginOptions { + modulePath: string +} + +export function createAutoHmrPlugin({ modulePath }: AutoHmrPluginOptions): UnpluginOptions { + const handleHotUpdateCallRegex = /handleHotUpdate\([\s\S]*?\)/ + + return { + name: 'unplugin-vue-router-auto-hmr', + enforce: 'post', + + transform(code, id) { + if (id.startsWith('\x00')) return + + // If you don't use automatically generated routes, + // maybe it will be meaningless to deal with hmr? + if (!code.includes('createRouter(') && !code.includes(modulePath)) { + return + } + + const ast = this.parse(code) + + let isImported: boolean = false + let routerName: string | undefined + let routerDeclaration: VariableDeclarator | undefined + + // @ts-expect-error + for (const node of ast.body) { + if ( + node.type === 'ExportNamedDeclaration' || + node.type === 'VariableDeclaration' + ) { + if (!routerName) { + routerDeclaration = getRouterDeclaration(node.type === 'VariableDeclaration' + ? node.declarations + : node.declaration?.type === 'VariableDeclaration' + ? node.declaration?.declarations + : undefined) + + routerName = nameFromDeclaration(routerDeclaration) + } + } else if (node.type === 'ImportDeclaration') { + isImported ||= getHandleHotUpdateDeclaration(node, modulePath) + } + } + + if (routerName) { + const isHandleHotUpdateCalled = handleHotUpdateCallRegex.test(code) + + const handleHotUpdateCode = [code] + + // add import if not imported + if (!isImported) { + handleHotUpdateCode.unshift( + `import { handleHotUpdate } from '${modulePath}'` + ) + } + + // add handleHotUpdate call if not called + if (!isHandleHotUpdateCalled) { + handleHotUpdateCode.push(`handleHotUpdate(${routerName})`) + } + + return { + code: handleHotUpdateCode.join('\n') + } + } + + return + } + } +} diff --git a/src/index.ts b/src/index.ts index d51df9472..bc5346561 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ import { join } from 'pathe' import { appendExtensionListToPattern } from './core/utils' import { MACRO_DEFINE_PAGE_QUERY } from './core/definePage' import { createAutoExportPlugin } from './data-loaders/auto-exports' +import { createAutoHmrPlugin } from './auto-hmr' export type * from './types' @@ -202,6 +203,14 @@ export default createUnplugin((opt = {}, _meta) => { ) } + if (options.autoHmr) { + plugins.push( + createAutoHmrPlugin({ + modulePath: MODULE_ROUTES_PATH, + }) + ) + } + return plugins }) diff --git a/src/options.ts b/src/options.ts index 7fe1f7e0f..69e79ee0d 100644 --- a/src/options.ts +++ b/src/options.ts @@ -213,6 +213,12 @@ export interface Options { */ watch?: boolean + /** + * Whether to enable auto HMR for Vue Router. + * @default `false` + */ + autoHmr?: boolean + /** * Experimental options. **Warning**: these can change or be removed at any time, even it patch releases. Keep an eye * on the Changelog. From 7abc277ad2d07310cf6e428d71d5d22eb37bf1d9 Mon Sep 17 00:00:00 2001 From: Libon Date: Tue, 10 Jun 2025 14:30:51 +0800 Subject: [PATCH 2/4] chore: improve the handling of type errors. Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/auto-hmr/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/auto-hmr/index.ts b/src/auto-hmr/index.ts index 4e8b08e75..16f55a37f 100644 --- a/src/auto-hmr/index.ts +++ b/src/auto-hmr/index.ts @@ -53,7 +53,9 @@ export function createAutoHmrPlugin({ modulePath }: AutoHmrPluginOptions): Unplu let routerName: string | undefined let routerDeclaration: VariableDeclarator | undefined - // @ts-expect-error + for (const node of ast.body as any[]) { + // …rest of loop body… + } for (const node of ast.body) { if ( node.type === 'ExportNamedDeclaration' || From ea89aa8a286179489644f45ed1fe22df28afac19 Mon Sep 17 00:00:00 2001 From: libondev Date: Tue, 10 Jun 2025 14:31:50 +0800 Subject: [PATCH 3/4] Revert "chore: improve the handling of type errors." This reverts commit 7abc277ad2d07310cf6e428d71d5d22eb37bf1d9. --- src/auto-hmr/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/auto-hmr/index.ts b/src/auto-hmr/index.ts index 16f55a37f..4e8b08e75 100644 --- a/src/auto-hmr/index.ts +++ b/src/auto-hmr/index.ts @@ -53,9 +53,7 @@ export function createAutoHmrPlugin({ modulePath }: AutoHmrPluginOptions): Unplu let routerName: string | undefined let routerDeclaration: VariableDeclarator | undefined - for (const node of ast.body as any[]) { - // …rest of loop body… - } + // @ts-expect-error for (const node of ast.body) { if ( node.type === 'ExportNamedDeclaration' || From 9fdf3397e97947890f1288c15856223b1be4e25f Mon Sep 17 00:00:00 2001 From: libondev Date: Tue, 10 Jun 2025 14:41:03 +0800 Subject: [PATCH 4/4] refactor(auto-hmr): Judging whether to call handleHotUpdate based on ast --- src/auto-hmr/index.ts | 52 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/auto-hmr/index.ts b/src/auto-hmr/index.ts index 4e8b08e75..61ac68b09 100644 --- a/src/auto-hmr/index.ts +++ b/src/auto-hmr/index.ts @@ -1,5 +1,6 @@ import type { UnpluginOptions } from 'unplugin' import type { VariableDeclarator, ImportDeclaration } from 'estree' +import type { AstNode } from 'rollup' function nameFromDeclaration(node?: VariableDeclarator) { return node?.id.type === 'Identifier' ? node.id.name : '' @@ -27,13 +28,54 @@ function getHandleHotUpdateDeclaration(node?: ImportDeclaration, modulePath?: st ) } +function hasHandleHotUpdateCall(ast: AstNode) { + function traverse(node: any) { + if (!node) return false; + + if ( + node.type === 'CallExpression' && + node.callee.type === 'Identifier' && + node.callee.name === 'handleHotUpdate' + ) { + return true; + } + + // e.g.: autoRouter.handleHotUpdate() + if ( + node.type === 'CallExpression' && + node.callee.type === 'MemberExpression' && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'handleHotUpdate' + ) { + return true; + } + + if (typeof node !== 'object') return false; + + for (const key in node) { + if (key === 'type' || key === 'loc' || key === 'range') continue; + + const child = node[key]; + if (Array.isArray(child)) { + for (const item of child) { + if (traverse(item)) return true; + } + } else if (typeof child === 'object' && child !== null) { + if (traverse(child)) return true; + } + } + + return false; + } + + return traverse(ast); +} + interface AutoHmrPluginOptions { modulePath: string } export function createAutoHmrPlugin({ modulePath }: AutoHmrPluginOptions): UnpluginOptions { - const handleHotUpdateCallRegex = /handleHotUpdate\([\s\S]*?\)/ - return { name: 'unplugin-vue-router-auto-hmr', enforce: 'post', @@ -49,7 +91,7 @@ export function createAutoHmrPlugin({ modulePath }: AutoHmrPluginOptions): Unplu const ast = this.parse(code) - let isImported: boolean = false + let isImported = false let routerName: string | undefined let routerDeclaration: VariableDeclarator | undefined @@ -74,7 +116,7 @@ export function createAutoHmrPlugin({ modulePath }: AutoHmrPluginOptions): Unplu } if (routerName) { - const isHandleHotUpdateCalled = handleHotUpdateCallRegex.test(code) + const isCalledHandleHotUpdate = hasHandleHotUpdateCall(ast) const handleHotUpdateCode = [code] @@ -86,7 +128,7 @@ export function createAutoHmrPlugin({ modulePath }: AutoHmrPluginOptions): Unplu } // add handleHotUpdate call if not called - if (!isHandleHotUpdateCalled) { + if (!isCalledHandleHotUpdate) { handleHotUpdateCode.push(`handleHotUpdate(${routerName})`) }