Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"lint-staged": "^13.0.3",
"nuxt": "^3.0.0-rc.8",
"pinst": "^3.0.0",
"playwright": "^1.25.2",
"prettier": "^2.7.1",
"release-it": "^15.4.1",
"typescript": "^4.8.2",
Expand Down
3 changes: 3 additions & 0 deletions playground/middleware/auth.global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default defineNuxtRouteMiddleware(to => {
console.log('ran middleware')
})
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 5 additions & 25 deletions src/parts/router.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { existsSync } from 'node:fs'
import { useNuxt, useLogger, addPlugin } from '@nuxt/kit'
import { useNuxt, useLogger } from '@nuxt/kit'
import { join, resolve } from 'pathe'
import { runtimeDir } from '../utils'

Expand All @@ -21,7 +21,6 @@ export const setupRouter = () => {
return
}

addPlugin(resolve(runtimeDir, 'router'))
nuxt.options.vite.optimizeDeps = nuxt.options.vite.optimizeDeps || {}
nuxt.options.vite.optimizeDeps.include = nuxt.options.vite.optimizeDeps.include || []
nuxt.options.vite.optimizeDeps.include.push('@ionic/vue-router')
Expand All @@ -31,32 +30,13 @@ export const setupRouter = () => {
app.plugins = app.plugins.filter(
p => !p.src.match(/nuxt3?\/dist\/(app\/plugins|pages\/runtime)\/router/)
)
app.plugins.unshift({
src: resolve(runtimeDir, 'router'),
mode: 'all',
})
})
})

// Remove Nuxt useRoute & useRouter composables
nuxt.hook('autoImports:sources', sources => {
for (const source of sources) {
if (source.from === '#app') {
source.imports = source.imports.filter(
i => typeof i !== 'string' || !['useRoute', 'useRouter'].includes(i)
)
}
}
sources.push({
from: 'vue-router',
imports: ['useRouter', 'useRoute'],
})
})

// Remove vue-router types
nuxt.hook('prepare:types', ({ references }) => {
const index = references.findIndex(i => 'types' in i && i.types === 'vue-router')
if (index !== -1) {
references.splice(index, 1)
}
})

// Add default ionic root layout
nuxt.hook('app:resolve', app => {
if (
Expand Down
188 changes: 181 additions & 7 deletions src/runtime/router.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,195 @@
import { createRouter, createWebHistory, createMemoryHistory } from '@ionic/vue-router'

import { defineNuxtPlugin, useRuntimeConfig } from '#imports'
import { computed, ComputedRef, reactive, shallowRef } from 'vue'
import { createWebHashHistory, NavigationGuard, RouteLocation } from 'vue-router'
import { createError } from 'h3'
import { withoutBase, isEqual } from 'ufo'
import {
callWithNuxt,
defineNuxtPlugin,
useRuntimeConfig,
showError,
clearError,
navigateTo,
useError,
useState,
} from '#app'

// @ts-expect-error virtual module
import { globalMiddleware, namedMiddleware } from '#build/middleware'
// @ts-expect-error virtual module
import routerOptions from '#build/router.options'
// @ts-expect-error virtual module generated by Nuxt
import routes from '#build/routes'
import _routes from '#build/routes'

export default defineNuxtPlugin(async nuxtApp => {
let routerBase = useRuntimeConfig().app.baseURL
if (routerOptions.hashMode && !routerBase.includes('#')) {
// allow the user to provide a `#` in the middle: `/base/#/app`
routerBase += '#'
}

const history =
routerOptions.history?.(routerBase) ??
(process.client
? routerOptions.hashMode
? createWebHashHistory(routerBase)
: createWebHistory(routerBase)
: createMemoryHistory(routerBase))

export default defineNuxtPlugin(nuxtApp => {
const config = useRuntimeConfig()
const baseURL = config.app.baseURL
const routes = routerOptions.routes?.(_routes) ?? _routes

const initialURL = process.server
? nuxtApp.ssrContext!.url
: createCurrentLocation(routerBase, window.location)
const router = createRouter({
...routerOptions,
history: process.server ? createMemoryHistory(baseURL) : createWebHistory(baseURL),
history,
routes,
})

nuxtApp.vueApp.use(router)
const previousRoute = shallowRef(router.currentRoute.value)
router.afterEach((_to, from) => {
previousRoute.value = from
})

Object.defineProperty(nuxtApp.vueApp.config.globalProperties, 'previousRoute', {
get: () => previousRoute.value,
})

// Allows suspending the route object until page navigation completes
const _route = shallowRef(router.resolve(initialURL) as RouteLocation)
const syncCurrentRoute = () => {
_route.value = router.currentRoute.value
}
nuxtApp.hook('page:finish', syncCurrentRoute)
router.afterEach((to, from) => {
// We won't trigger suspense if the component is reused between routes
// so we need to update the route manually
if (to.matched[0]?.components?.default === from.matched[0]?.components?.default) {
syncCurrentRoute()
}
})

// https://github.com/vuejs/router/blob/main/packages/router/src/router.ts#L1225-L1233
const route = {} as { [K in keyof RouteLocation]: ComputedRef<RouteLocation[K]> }
for (const key in _route.value) {
route[key as 'path'] = computed(() => _route.value[key as 'path'])
}

nuxtApp._route = reactive(route)

nuxtApp._middleware = nuxtApp._middleware || {
global: [],
named: {},
}

const error = useError()

const initialLayout = useState<string>('_layout')
router.beforeEach(async (to, from) => {
to.meta = reactive(to.meta)
if (nuxtApp.isHydrating) {
to.meta.layout = initialLayout.value ?? to.meta.layout
}
nuxtApp._processingMiddleware = true

type MiddlewareDef = string | NavigationGuard
const middlewareEntries = new Set<MiddlewareDef>([
...globalMiddleware,
...nuxtApp._middleware.global,
])
for (const component of to.matched) {
const componentMiddleware = component.meta.middleware as MiddlewareDef | MiddlewareDef[]
if (!componentMiddleware) {
continue
}
if (Array.isArray(componentMiddleware)) {
for (const entry of componentMiddleware) {
middlewareEntries.add(entry)
}
} else {
middlewareEntries.add(componentMiddleware)
}
}

for (const entry of middlewareEntries) {
const middleware =
typeof entry === 'string'
? nuxtApp._middleware.named[entry] ||
(await namedMiddleware[entry]?.().then((r: any) => r.default || r))
: entry

if (!middleware) {
if (process.dev) {
throw new Error(
`Unknown route middleware: '${entry}'. Valid middleware: ${Object.keys(namedMiddleware)
.map(mw => `'${mw}'`)
.join(', ')}.`
)
}
throw new Error(`Unknown route middleware: '${entry}'.`)
}

const result = await callWithNuxt(nuxtApp, middleware, [to, from])
if (process.server || (!nuxtApp.payload.serverRendered && nuxtApp.isHydrating)) {
if (result === false || result instanceof Error) {
const error =
result ||
createError({
statusMessage: `Route navigation aborted: ${initialURL}`,
})
return callWithNuxt(nuxtApp, showError, [error])
}
}
if (result || result === false) {
return result
}
}
})

router.afterEach(async to => {
delete nuxtApp._processingMiddleware

if (process.client && !nuxtApp.isHydrating && error.value) {
// Clear any existing errors
await callWithNuxt(nuxtApp, clearError)
}
if (to.matched.length === 0) {
callWithNuxt(nuxtApp, showError, [
createError({
statusCode: 404,
fatal: false,
statusMessage: `Page not found: ${to.fullPath}`,
}),
])
} else if (process.server && to.matched[0].name === '404' && nuxtApp.ssrContext) {
nuxtApp.ssrContext.event.res.statusCode = 404
} else if (process.server) {
const currentURL = to.fullPath || '/'
if (!isEqual(currentURL, initialURL)) {
await callWithNuxt(nuxtApp, navigateTo, [currentURL])
}
}
})

return { provide: { router } }
})

// https://github.com/vuejs/router/blob/4a0cc8b9c1e642cdf47cc007fa5bbebde70afc66/packages/router/src/history/html5.ts#L37
function createCurrentLocation (base: string, location: Location): string {
const { pathname, search, hash } = location
// allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
const hashPos = base.indexOf('#')
if (hashPos > -1) {
const slicePos = hash.includes(base.slice(hashPos)) ? base.slice(hashPos).length : 1
let pathFromHash = hash.slice(slicePos)
// prepend the starting slash to hash so the url starts with /#
if (pathFromHash[0] !== '/') {
pathFromHash = '/' + pathFromHash
}
return withoutBase(pathFromHash, '')
}
const path = withoutBase(pathname, base)
return path + search + hash
}
14 changes: 13 additions & 1 deletion test/e2e/ssr.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/* @vitest-environment node */
import { fileURLToPath } from 'node:url'
import { setup, $fetch } from '@nuxt/test-utils'
import { setup, $fetch, createPage, url } from '@nuxt/test-utils'
import { describe, expect, it } from 'vitest'

describe('nuxt ionic', async () => {
await setup({
server: true,
browser: true,
rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
})

Expand All @@ -22,4 +23,15 @@ describe('nuxt ionic', async () => {
'<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">'
)
})

it('runs middleware on client-side', async () => {
const logs: string[] = []
const page = await createPage()
page.on('console', msg => {
logs.push(msg.text())
})
await page.goto(url('/tabs/tab1'))
await page.waitForLoadState('networkidle')
expect(logs).toContain('ran middleware')
})
})