Skip to content

Commit 73b9847

Browse files
authored
fix: update router plugin to support latest nuxt (nuxt-modules#560)
1 parent f6396af commit 73b9847

File tree

11 files changed

+2968
-2699
lines changed

11 files changed

+2968
-2699
lines changed

Diff for: .nuxtrc

-2
This file was deleted.

Diff for: docs/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@
1111
"devDependencies": {
1212
"@nuxt-themes/docus": "1.15.0",
1313
"@nuxtjs/plausible": "1.0.0",
14-
"nuxt": "3.9.1"
14+
"nuxt": "3.11.2"
1515
}
1616
}

Diff for: package.json

+9-8
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@
5757
"@capacitor/cli": "^6.0.0",
5858
"@capacitor/core": "^6.0.0",
5959
"@ionic/cli": "^7.2.0",
60-
"@ionic/vue": "^8.0.0",
61-
"@ionic/vue-router": "^8.0.0",
62-
"@nuxt/kit": "^3.9.1",
60+
"@ionic/vue": "^8.1.2",
61+
"@ionic/vue-router": "^8.1.2",
62+
"@nuxt/kit": "^3.11.2",
6363
"ionicons": "^7.3.1",
6464
"pathe": "^1.1.2",
6565
"pkg-types": "^1.0.3",
@@ -69,7 +69,7 @@
6969
"devDependencies": {
7070
"@nuxt/eslint-config": "0.3.10",
7171
"@nuxt/module-builder": "0.6.0",
72-
"@nuxt/schema": "3.9.1",
72+
"@nuxt/schema": "3.11.2",
7373
"@nuxt/test-utils": "3.12.1",
7474
"@types/node": "20.12.12",
7575
"@vitest/coverage-v8": "1.4.0",
@@ -78,16 +78,17 @@
7878
"expect-type": "0.19.0",
7979
"husky": "9.0.11",
8080
"lint-staged": "15.2.2",
81-
"nuxt": "3.9.1",
81+
"nuxt": "3.11.2",
8282
"playwright": "1.43.1",
8383
"typescript": "5.4.5",
8484
"vitest": "1.4.0",
85-
"vue": "3.4.24",
85+
"vue": "3.4.27",
8686
"vue-tsc": "2.0.19"
8787
},
8888
"resolutions": {
89-
"@nuxt/kit": "3.9.1",
90-
"@nuxt/schema": "3.9.1",
89+
"@nuxt/content": "2.12.1",
90+
"@nuxt/kit": "3.11.2",
91+
"@nuxt/schema": "3.11.2",
9192
"@nuxtjs/ionic": "link:.",
9293
"consola": "^3.2.3"
9394
},

Diff for: playground/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
"@capacitor/filesystem": "6.0.0",
1313
"@capacitor/preferences": "6.0.0",
1414
"@nuxtjs/ionic": "latest",
15-
"nuxt": "3.9.1"
15+
"nuxt": "3.11.2"
1616
}
1717
}

Diff for: pnpm-lock.yaml

+2,678-2,454
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: src/module.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export default defineNuxtModule<ModuleOptions>({
129129
}
130130

131131
// Set up Ionic Core
132-
addPlugin(resolve(runtimeDir, 'ionic'))
132+
addPlugin(resolve(runtimeDir, 'plugins/ionic'))
133133

134134
// Add Nuxt Vue custom utility components
135135
setupUtilityComponents()

Diff for: src/parts/router.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@ export const setupRouter = () => {
2020
return
2121
}
2222

23-
const ROUTER_PLUGIN_RE = /nuxt3?\/dist\/(app\/plugins|pages\/runtime)\/(plugins\/)?router/
23+
const ROUTER_PLUGIN_RE = /nuxt(3|-nightly)?\/dist\/(app\/plugins|pages\/runtime)\/(plugins\/)?router/
24+
const PAGE_USAGE_PLUGIN_RE = /nuxt(3|-nightly)?\/dist\/(app\/plugins|pages\/runtime)\/(plugins\/)?check-if-page-unused/
2425
const ionicRouterPlugin = {
25-
src: resolve(runtimeDir, 'router'),
26+
src: resolve(runtimeDir, 'plugins/router'),
2627
mode: 'all',
2728
} as const
2829

2930
nuxt.hook('modules:done', () => {
3031
nuxt.hook('app:resolve', (app) => {
32+
app.plugins = app.plugins.filter(p => !PAGE_USAGE_PLUGIN_RE.test(p.src))
3133
const routerPlugin = app.plugins.findIndex(p => ROUTER_PLUGIN_RE.test(p.src))
3234
if (routerPlugin !== -1) {
3335
app.plugins.splice(routerPlugin, 1, ionicRouterPlugin)
File renamed without changes.

Diff for: src/runtime/plugins/router.ts

+273
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import {
2+
createMemoryHistory,
3+
createRouter,
4+
createWebHashHistory,
5+
createWebHistory,
6+
} from '@ionic/vue-router'
7+
8+
import { isReadonly, reactive, shallowReactive, shallowRef } from 'vue'
9+
import type { Ref } from 'vue'
10+
import type { RouteLocation, Router } from 'vue-router'
11+
import { createError } from 'h3'
12+
import { isEqual, withoutBase } from 'ufo'
13+
14+
import type { PageMeta, Plugin, RouteMiddleware } from '#app'
15+
import { getRouteRules } from '#app/composables/manifest'
16+
import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
17+
import { clearError, showError, useError } from '#app/composables/error'
18+
import { onNuxtReady } from '#app/composables/ready'
19+
import { navigateTo } from '#app/composables/router'
20+
21+
// @ts-expect-error virtual file
22+
import { globalMiddleware, namedMiddleware } from '#build/middleware'
23+
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
24+
// @ts-expect-error virtual file
25+
import routerOptions from '#build/router.options'
26+
// @ts-expect-error virtual file
27+
import _routes from '#build/routes'
28+
29+
const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
30+
name: 'nuxt-ionic:router',
31+
enforce: 'pre',
32+
async setup(nuxtApp) {
33+
let routerBase = useRuntimeConfig().app.baseURL
34+
if (routerOptions.hashMode && !routerBase.includes('#')) {
35+
// allow the user to provide a `#` in the middle: `/base/#/app`
36+
routerBase += '#'
37+
}
38+
39+
const history
40+
= routerOptions.history?.(routerBase)
41+
?? (import.meta.client
42+
? routerOptions.hashMode
43+
? createWebHashHistory(routerBase)
44+
: createWebHistory(routerBase)
45+
: createMemoryHistory(routerBase))
46+
47+
const routes = routerOptions.routes?.(_routes) ?? _routes
48+
49+
const router = createRouter({
50+
...routerOptions,
51+
history,
52+
routes,
53+
})
54+
55+
if (import.meta.client && 'scrollRestoration' in window.history) {
56+
window.history.scrollRestoration = 'auto'
57+
}
58+
nuxtApp.vueApp.use(router)
59+
60+
const previousRoute = shallowRef(router.currentRoute.value)
61+
router.afterEach((_to, from) => {
62+
previousRoute.value = from
63+
})
64+
65+
Object.defineProperty(nuxtApp.vueApp.config.globalProperties, 'previousRoute', {
66+
get: () => previousRoute.value,
67+
})
68+
69+
const initialURL = import.meta.server
70+
? nuxtApp.ssrContext!.url
71+
: createCurrentLocation(routerBase, window.location, nuxtApp.payload.path)
72+
73+
// Allows suspending the route object until page navigation completes
74+
const _route = shallowRef(router.currentRoute.value)
75+
const syncCurrentRoute = () => {
76+
_route.value = router.currentRoute.value
77+
}
78+
nuxtApp.hook('page:finish', syncCurrentRoute)
79+
router.afterEach((to, from) => {
80+
// We won't trigger suspense if the component is reused between routes
81+
// so we need to update the route manually
82+
if (to.matched[0]?.components?.default === from.matched[0]?.components?.default) {
83+
syncCurrentRoute()
84+
}
85+
})
86+
87+
// https://github.com/vuejs/router/blob/main/packages/router/src/router.ts#L1225-L1233
88+
const route = {} as RouteLocation
89+
for (const key in _route.value) {
90+
Object.defineProperty(route, key, {
91+
get: () => _route.value[key as keyof RouteLocation],
92+
})
93+
}
94+
95+
nuxtApp._route = shallowReactive(route)
96+
97+
nuxtApp._middleware = nuxtApp._middleware || {
98+
global: [],
99+
named: {},
100+
}
101+
102+
const error = useError()
103+
if (import.meta.client || !nuxtApp.ssrContext?.islandContext) {
104+
router.afterEach(async (to, _from, failure) => {
105+
delete nuxtApp._processingMiddleware
106+
107+
if (import.meta.client && !nuxtApp.isHydrating && error.value) {
108+
// Clear any existing errors
109+
await nuxtApp.runWithContext(clearError)
110+
}
111+
if (failure) {
112+
await nuxtApp.callHook('page:loading:end')
113+
}
114+
if (import.meta.server && failure?.type === 4 /* ErrorTypes.NAVIGATION_ABORTED */) {
115+
return
116+
}
117+
if (to.matched.length === 0) {
118+
await nuxtApp.runWithContext(() => showError(createError({
119+
statusCode: 404,
120+
fatal: false,
121+
statusMessage: `Page not found: ${to.fullPath}`,
122+
data: {
123+
path: to.fullPath,
124+
},
125+
})))
126+
}
127+
else if (import.meta.server && to.redirectedFrom && to.fullPath !== initialURL) {
128+
await nuxtApp.runWithContext(() => navigateTo(to.fullPath || '/'))
129+
}
130+
})
131+
}
132+
133+
const resolvedInitialRoute = import.meta.client && initialURL !== router.currentRoute.value.fullPath
134+
? router.resolve(initialURL)
135+
: router.currentRoute.value
136+
syncCurrentRoute()
137+
138+
if (import.meta.server && nuxtApp.ssrContext?.islandContext) {
139+
// We're in an island context, and don't need to handle middleware or redirections
140+
return { provide: { router } }
141+
}
142+
143+
const initialLayout = nuxtApp.payload.state._layout
144+
router.beforeEach(async (to, from) => {
145+
await nuxtApp.callHook('page:loading:start')
146+
to.meta = reactive(to.meta)
147+
if (nuxtApp.isHydrating && initialLayout && !isReadonly(to.meta.layout)) {
148+
to.meta.layout = initialLayout as Exclude<PageMeta['layout'], Ref | false>
149+
}
150+
nuxtApp._processingMiddleware = true
151+
152+
if (import.meta.client || !nuxtApp.ssrContext?.islandContext) {
153+
type MiddlewareDef = string | RouteMiddleware
154+
const middlewareEntries = new Set<MiddlewareDef>([...globalMiddleware, ...nuxtApp._middleware.global])
155+
for (const component of to.matched) {
156+
const componentMiddleware = component.meta.middleware as MiddlewareDef | MiddlewareDef[]
157+
if (!componentMiddleware) {
158+
continue
159+
}
160+
for (const entry of toArray(componentMiddleware)) {
161+
middlewareEntries.add(entry)
162+
}
163+
}
164+
165+
if (isAppManifestEnabled) {
166+
const routeRules = await nuxtApp.runWithContext(() => getRouteRules(to.path))
167+
168+
if (routeRules.appMiddleware) {
169+
for (const key in routeRules.appMiddleware) {
170+
if (routeRules.appMiddleware[key]) {
171+
middlewareEntries.add(key)
172+
}
173+
else {
174+
middlewareEntries.delete(key)
175+
}
176+
}
177+
}
178+
}
179+
180+
for (const entry of middlewareEntries) {
181+
const middleware
182+
= typeof entry === 'string'
183+
? nuxtApp._middleware.named[entry]
184+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
185+
|| await namedMiddleware[entry]?.().then((r: any) => r.default || r)
186+
: entry
187+
188+
if (!middleware) {
189+
if (import.meta.dev) {
190+
throw new Error(`Unknown route middleware: '${entry}'. Valid middleware: ${Object.keys(namedMiddleware).map(mw => `'${mw}'`).join(', ')}.`)
191+
}
192+
throw new Error(`Unknown route middleware: '${entry}'.`)
193+
}
194+
195+
const result = await nuxtApp.runWithContext(() => middleware(to, from))
196+
if (import.meta.server || (!nuxtApp.payload.serverRendered && nuxtApp.isHydrating)) {
197+
if (result === false || result instanceof Error) {
198+
const error = result || createError({
199+
statusCode: 404,
200+
statusMessage: `Page Not Found: ${initialURL}`,
201+
})
202+
await nuxtApp.runWithContext(() => showError(error))
203+
return false
204+
}
205+
}
206+
207+
if (result === true) {
208+
continue
209+
}
210+
if (result || result === false) {
211+
return result
212+
}
213+
}
214+
}
215+
})
216+
217+
router.onError(async () => {
218+
delete nuxtApp._processingMiddleware
219+
await nuxtApp.callHook('page:loading:end')
220+
})
221+
222+
nuxtApp.hooks.hookOnce('app:created', async () => {
223+
delete nuxtApp._processingMiddleware
224+
})
225+
226+
onNuxtReady(async () => {
227+
try {
228+
if (import.meta.client) {
229+
// #4920, #4982
230+
if ('name' in resolvedInitialRoute) {
231+
resolvedInitialRoute.name = undefined
232+
}
233+
await router.replace({
234+
...resolvedInitialRoute,
235+
force: true,
236+
})
237+
}
238+
}
239+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
240+
catch (error: any) {
241+
// We'll catch middleware errors or deliberate exceptions here
242+
await nuxtApp.runWithContext(() => showError(error))
243+
}
244+
})
245+
246+
return { provide: { router } }
247+
},
248+
})
249+
250+
// https://github.com/vuejs/router/blob/4a0cc8b9c1e642cdf47cc007fa5bbebde70afc66/packages/router/src/history/html5.ts#L37
251+
function createCurrentLocation(base: string, location: Location, renderedPath?: string): string {
252+
const { pathname, search, hash } = location
253+
// allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
254+
const hashPos = base.indexOf('#')
255+
if (hashPos > -1) {
256+
const slicePos = hash.includes(base.slice(hashPos)) ? base.slice(hashPos).length : 1
257+
let pathFromHash = hash.slice(slicePos)
258+
// prepend the starting slash to hash so the url starts with /#
259+
if (pathFromHash[0] !== '/') {
260+
pathFromHash = '/' + pathFromHash
261+
}
262+
return withoutBase(pathFromHash, '')
263+
}
264+
const displayedPath = withoutBase(pathname, base)
265+
const path = !renderedPath || isEqual(displayedPath, renderedPath, { trailingSlash: true }) ? displayedPath : renderedPath
266+
return path + (path.includes('?') ? '' : search) + hash
267+
}
268+
269+
function toArray<T> (value: T | T[]): T[] {
270+
return Array.isArray(value) ? value : [value]
271+
}
272+
273+
export default plugin

0 commit comments

Comments
 (0)