Skip to content

Commit 4925e7e

Browse files
committed
feat: handle hmr for pages
Fix #292 Close #404
1 parent f5fc2ec commit 4925e7e

File tree

14 files changed

+144
-116
lines changed

14 files changed

+144
-116
lines changed

client.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
declare module 'vue-router/auto-routes' {
2-
import type { RouteRecordRaw } from 'vue-router'
2+
import type { RouteRecordRaw, Router } from 'vue-router'
33

44
/**
55
* Array of routes generated by unplugin-vue-router
66
*/
77
export const routes: RouteRecordRaw[]
8+
9+
/**
10+
* Setups hot module replacement for routes.
11+
* @param router - The router instance
12+
*/
13+
export function handleHotUpdate(router: Router): void
814
}
915

1016
declare module 'vue-router' {

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@
171171
"yaml": "^2.4.5"
172172
},
173173
"peerDependencies": {
174-
"vue-router": "^4.3.0"
174+
"vue-router": "^4.4.0"
175175
},
176176
"peerDependenciesMeta": {
177177
"vue-router": {
@@ -212,7 +212,7 @@
212212
"vitepress": "1.2.3",
213213
"vitest": "^1.6.0",
214214
"vue": "^3.4.27",
215-
"vue-router": "4.4.0-alpha.2",
215+
"vue-router": "4.4.0-alpha.3",
216216
"vue-router-mock": "^1.1.0",
217217
"vue-tsc": "^2.0.21",
218218
"vuefire": "^3.1.23",

playground/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
<!DOCTYPE html>
1+
<!doctype html>
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
</head>
77
<body>
88
<a
9-
href="/__inspect/#/module?id=virtual:vue-router/auto-routes&index=0"
9+
href="/__inspect/#/module?id=/__vue-router/auto-routes&index=0"
1010
target="_blank"
1111
style="margin-top: 10px; display: block"
1212
>visit /__inspect/ to inspect the intermediate state</a

playground/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@
2020
"mande": "^2.0.9",
2121
"pinia": "^2.1.7",
2222
"vue": "^3.4.27",
23-
"vue-router": "4.4.0-alpha.2"
23+
"vue-router": "4.4.0-alpha.3"
2424
}
2525
}

playground/src/App.vue

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,37 @@ import type {
88
import { ref } from 'vue'
99
import { routes } from 'vue-router/auto-routes'
1010
11-
function test(
12-
a: RouteLocationResolved<'/[name]'>,
13-
b: RouteLocationNormalizedLoaded<'/[name]'>,
14-
c: RouteLocation<'/[name]'>
15-
) {}
11+
console.log(`We have ${routes.length} routes.`)
1612
13+
const router = useRouter()
1714
const route = useRoute()
18-
if (route.name === '/deep/nesting/works/[[files]]+') {
19-
route.params.files
20-
}
2115
22-
console.log(`We have ${routes.length} routes.`)
16+
const targetRoute = ref('')
2317
24-
const router = useRouter()
18+
function _test() {
19+
function test(
20+
a: RouteLocationResolved<'/[name]'>,
21+
b: RouteLocationNormalizedLoaded<'/[name]'>,
22+
c: RouteLocation<'/[name]'>
23+
) {}
2524
26-
router.resolve('/:name')
27-
router.resolve({ name: '/[name]', params: { name: 'hello' } }).params.name
25+
if (route.name === '/deep/nesting/works/[[files]]+') {
26+
route.params.files
27+
}
2828
29-
useLink({ to: '/articles/2' }).route.value.name
30-
useLink({ to: { path: '/articles/:id' } })
31-
useLink({ to: { name: '/[name]', params: { name: 2 } } }).route.value.params
32-
.name
33-
// useLink({ name: '/[name]', params: { name: 2 } }).route.value.params.name
34-
useLink({ to: ref({ name: '/[name]', params: { name: 2 } }) }).route.value.name
29+
router.resolve('/:name')
30+
router.resolve({ name: '/[name]', params: { name: 'hello' } }).params.name
3531
36-
const customRoute = useRoute('/deep/nesting/works/custom-path')
32+
useLink({ to: '/articles/2' }).route.value.name
33+
useLink({ to: { path: '/articles/:id' } })
34+
useLink({ to: { name: '/[name]', params: { name: 2 } } }).route.value.params
35+
.name
36+
// useLink({ name: '/[name]', params: { name: 2 } }).route.value.params.name
37+
useLink({ to: ref({ name: '/[name]', params: { name: 2 } }) }).route.value
38+
.name
39+
40+
const customRoute = useRoute('/deep/nesting/works/custom-path')
41+
}
3742
</script>
3843

3944
<template>
@@ -93,8 +98,19 @@ const customRoute = useRoute('/deep/nesting/works/custom-path')
9398
</RouterLink>
9499
</nav>
95100
</div>
101+
<div>
102+
<form @submit.prevent="router.push(targetRoute)">
103+
<label>
104+
Navigate to:
105+
<input type="text" v-model="targetRoute" />
106+
</label>
107+
<button>Go</button>
108+
</form>
109+
</div>
96110
</header>
97111

112+
<hr />
113+
98114
<RouterView />
99115
<hr />
100116
<RouterView name="named" />

playground/src/pages/index.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,10 @@ definePage({
1414
</template>
1515

1616
<route lang="json">
17-
{ "name": "home" }
17+
{
18+
"name": "home",
19+
"meta": {
20+
"n": 5
21+
}
22+
}
1823
</route>

playground/src/router.ts

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,14 @@
11
import { createRouter, createWebHistory } from 'vue-router'
2-
import { routes } from 'vue-router/auto-routes'
2+
import { routes, handleHotUpdate } from 'vue-router/auto-routes'
33
import type { RouteRecordInfo, ParamValue } from 'vue-router'
44

55
export const router = createRouter({
66
history: createWebHistory(),
77
routes,
88
})
99

10-
// let removeRoutes: Array<() => void> = []
11-
// for (const route of routes) {
12-
// removeRoutes.push(router.addRoute(route))
13-
// }
14-
1510
if (import.meta.hot) {
16-
// How to trigger this? tried virtual: /@id/
17-
const id = 'vue-router/auto-routes'
18-
// import.meta.hot.accept('vue-router/auto-routes', (mod) => {
19-
// console.log('✨ got new routes', mod)
20-
// })
21-
// import.meta.hot.accept(id, (mod) => {
22-
// console.log('✨ got new routes', mod)
23-
// })
24-
// import.meta.hot.accept((mod) => {
25-
// console.log('🔁 reloading routes from router...', mod)
26-
// console.log(mod!.router.getRoutes())
27-
// })
28-
29-
// NOTE: this approach doesn't make sense and doesn't work anyway: it seems to fetch an outdated version of the file
30-
// import.meta.hot.on('vite:beforeUpdate', async (payload) => {
31-
// console.log('🔁 vite:beforeUpdate', payload)
32-
// const routesUpdate = payload.updates.find(
33-
// (update) => update.path === 'virtual:vue-router/auto-routes'
34-
// )
35-
// if (routesUpdate) {
36-
// console.log('🔁 reloading routes from router...')
37-
// const { routes } = await import(
38-
// '/@id/' + routesUpdate.path + `?t=${routesUpdate.timestamp}`
39-
// )
40-
// for (const removeRoute of removeRoutes) {
41-
// console.log('Removing route...')
42-
// removeRoute()
43-
// }
44-
// removeRoutes = []
45-
// for (const route of routes) {
46-
// removeRoutes.push(router.addRoute(route))
47-
// }
48-
// router.replace('')
49-
// }
50-
// })
11+
handleHotUpdate(router)
5112
}
5213

5314
// manual extension of route types

playground/vite.config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@ export default defineConfig({
7777
// can add multiple routes folders
7878
{
7979
src: 'src/pages',
80-
// can even add params
81-
// path: '[lang]/',
8280
},
8381
{
8482
src: 'src/docs',

pnpm-lock.yaml

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/context.ts

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -107,42 +107,37 @@ export function createRoutesContext(options: ResolvedOptions) {
107107

108108
async function writeRouteInfoToNode(node: TreeNode, filePath: string) {
109109
const content = await fs.readFile(filePath, 'utf8')
110-
// TODO: cache the result of parsing the SFC so the transform can reuse the parsing
110+
// TODO: cache the result of parsing the SFC (in the extractDefinePageAndName) so the transform can reuse the parsing
111111
node.hasDefinePage ||= content.includes('definePage')
112+
// TODO: track if it changed and to not always trigger HMR
112113
const definedPageNameAndPath = extractDefinePageNameAndPath(
113114
content,
114115
filePath
115116
)
117+
// TODO: track if it changed and if generateRoutes should be called again
116118
const routeBlock = getRouteBlock(filePath, content, options)
117119
// TODO: should warn if hasDefinePage and customRouteBlock
118120
// if (routeBlock) logger.log(routeBlock)
119121
node.setCustomRouteBlock(filePath, {
120122
...routeBlock,
121123
...definedPageNameAndPath,
122124
})
123-
124-
// TODO: if definePage changed
125-
server?.invalidate(filePath + '?definePage&vue&lang.tsx')
126-
server?.invalidate(asVirtualId(MODULE_ROUTES_PATH))
127-
128-
// TODO: only if needed
129-
// server?.updateRoutes()
130125
}
131126

132127
async function addPage(
133128
{ filePath, routePath }: HandlerContext,
134129
triggerExtendRoute = false
135130
) {
136131
logger.log(`added "${routePath}" for "${filePath}"`)
137-
// TODO: handle top level named view HMR
138-
139132
const node = routeTree.insert(routePath, filePath)
140-
141133
await writeRouteInfoToNode(node, filePath)
142134

143135
if (triggerExtendRoute) {
144136
await options.extendRoute?.(new EditableTreeNode(node))
145137
}
138+
139+
// TODO: trigger HMR vue-router/auto
140+
server?.updateRoutes()
146141
}
147142

148143
async function updatePage({ filePath, routePath }: HandlerContext) {
@@ -154,11 +149,15 @@ export function createRoutesContext(options: ResolvedOptions) {
154149
}
155150
await writeRouteInfoToNode(node, filePath)
156151
await options.extendRoute?.(new EditableTreeNode(node))
152+
// no need to manually trigger the update of vue-router/auto-routes because
153+
// the change of the vue file will trigger HMR
157154
}
158155

159156
function removePage({ filePath, routePath }: HandlerContext) {
160157
logger.log(`remove "${routePath}" for "${filePath}"`)
161158
routeTree.removeChild(filePath)
159+
// TODO: HMR vue-router/auto
160+
server?.updateRoutes()
162161
}
163162

164163
function setupWatcher(watcher: RoutesFolderWatcher) {
@@ -182,16 +181,37 @@ export function createRoutesContext(options: ResolvedOptions) {
182181
// unlinkDir event
183182
}
184183

185-
let lastAutoRoutes: string | undefined
186184
function generateRoutes() {
187185
const importsMap = new ImportsMap()
188186

189-
const routesExport = `export const routes = ${generateRouteRecord(
187+
const routeList = `export const routes = ${generateRouteRecord(
190188
routeTree,
191189
options,
192190
importsMap
193-
)}`
194-
// TODO: should we put some HMR code for routes here or should it be at the router creation level (that would be easier to replace the routes)
191+
)}\n`
192+
193+
let hmr = `
194+
export function handleHotUpdate(_router) {
195+
if (import.meta.hot) {
196+
import.meta.hot.data.router = _router
197+
}
198+
}
199+
200+
if (import.meta.hot) {
201+
import.meta.hot.accept((mod) => {
202+
const router = import.meta.hot.data.router
203+
if (!router) {
204+
console.error('❌ router not found')
205+
return
206+
}
207+
router.clearRoutes()
208+
for (const route of mod.routes) {
209+
router.addRoute(route)
210+
}
211+
router.replace('')
212+
})
213+
}
214+
`
195215

196216
// generate the list of imports
197217
let imports = importsMap.toString()
@@ -200,13 +220,7 @@ export function createRoutesContext(options: ResolvedOptions) {
200220
imports += '\n'
201221
}
202222

203-
const newAutoRoutes = `${imports}${routesExport}\n`
204-
205-
if (lastAutoRoutes !== newAutoRoutes) {
206-
// cache hit, triigger HMR (not working yet)
207-
server?.updateRoutes()
208-
lastAutoRoutes = newAutoRoutes
209-
}
223+
const newAutoRoutes = `${imports}${routeList}${hmr}\n`
210224

211225
// prepend it to the code
212226
return newAutoRoutes

0 commit comments

Comments
 (0)