Skip to content
Open
143 changes: 143 additions & 0 deletions src/auto-hmr/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
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 : ''
}

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'
)
)
}

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 {
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 = 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 isCalledHandleHotUpdate = hasHandleHotUpdateCall(ast)

const handleHotUpdateCode = [code]

// add import if not imported
if (!isImported) {
handleHotUpdateCode.unshift(
`import { handleHotUpdate } from '${modulePath}'`
)
}

// add handleHotUpdate call if not called
if (!isCalledHandleHotUpdate) {
handleHotUpdateCode.push(`handleHotUpdate(${routerName})`)
}

return {
code: handleHotUpdateCode.join('\n')
}
}

return
}
}
}
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -202,6 +203,14 @@ export default createUnplugin<Options | undefined>((opt = {}, _meta) => {
)
}

if (options.autoHmr) {
plugins.push(
createAutoHmrPlugin({
modulePath: MODULE_ROUTES_PATH,
})
)
}

return plugins
})

Expand Down
6 changes: 6 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down