Skip to content
Open
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
4 changes: 4 additions & 0 deletions playground/pages/tabs/tab1/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ useHead({
title: 'Tab 1',
})
const isExploreEnabled = ref(true)

const route = useRoute()

console.log(route.name)
</script>

<template>
Expand Down
3 changes: 3 additions & 0 deletions playground/plugins/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default defineNuxtPlugin(() => {
console.log('ran plugin')
})
53 changes: 27 additions & 26 deletions src/runtime/plugins/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import type { PageMeta, Plugin, RouteMiddleware } from '#app'
import { getRouteRules } from '#app/composables/manifest'
import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
import { clearError, showError, useError } from '#app/composables/error'
import { onNuxtReady } from '#app/composables/ready'
import { navigateTo } from '#app/composables/router'

// @ts-expect-error virtual file
Expand Down Expand Up @@ -79,7 +78,11 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
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) {
// also syncing route if there is no component (like with route redirect or alias) to prevent unnecessary updates
const toComponent = to.matched[0]?.components?.default
const fromComponent = from.matched[0]?.components?.default

if (!fromComponent || toComponent === fromComponent) {
syncCurrentRoute()
}
})
Expand Down Expand Up @@ -141,14 +144,36 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
}

const initialLayout = nuxtApp.payload.state._layout

const pluginsReady = new Promise<void>((resolve) => {
nuxtApp.hooks.hookOnce('app:created', async () => {
delete nuxtApp._processingMiddleware
resolve()
})
})

let needsCorrection = import.meta.client

router.beforeEach(async (to, from) => {
// wait for plugins to finish
await pluginsReady
await nuxtApp.callHook('page:loading:start')

to.meta = reactive(to.meta)
if (nuxtApp.isHydrating && initialLayout && !isReadonly(to.meta.layout)) {
to.meta.layout = initialLayout as Exclude<PageMeta['layout'], Ref | false>
}
nuxtApp._processingMiddleware = true

if (needsCorrection) {
needsCorrection = false
// #4920, #4982
if ('name' in resolvedInitialRoute) {
resolvedInitialRoute.name = undefined
}
return { ...resolvedInitialRoute, replace: true }
}

if (import.meta.client || !nuxtApp.ssrContext?.islandContext) {
type MiddlewareDef = string | RouteMiddleware
const middlewareEntries = new Set<MiddlewareDef>([...globalMiddleware, ...nuxtApp._middleware.global])
Expand Down Expand Up @@ -219,30 +244,6 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
await nuxtApp.callHook('page:loading:end')
})

nuxtApp.hooks.hookOnce('app:created', async () => {
delete nuxtApp._processingMiddleware
})

onNuxtReady(async () => {
try {
if (import.meta.client) {
// #4920, #4982
if ('name' in resolvedInitialRoute) {
resolvedInitialRoute.name = undefined
}
await router.replace({
...resolvedInitialRoute,
force: true,
})
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
catch (error: any) {
// We'll catch middleware errors or deliberate exceptions here
await nuxtApp.runWithContext(() => showError(error))
}
})

return { provide: { router } }
},
})
Expand Down
64 changes: 64 additions & 0 deletions test/e2e/ssr.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,68 @@ describe('nuxt ionic', async () => {

await page.close()
})

it('runs plugin and middleware in correct order', 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 plugin')
expect(logs).toContain('ran middleware')

// Plugin should run before middleware
const pluginIndex = logs.indexOf('ran plugin')
const middlewareIndex = logs.indexOf('ran middleware')
expect(pluginIndex).toBeGreaterThanOrEqual(0)
expect(middlewareIndex).toBeGreaterThanOrEqual(0)
expect(pluginIndex).toBeLessThan(middlewareIndex)

await page.close()
})

it('has correct route information when landing on page', 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')

// Route name should be logged and not be undefined or null
const routeNameLog = logs.find(log => log && log !== 'undefined' && log !== 'null' && !log.startsWith('ran'))
expect(routeNameLog).toBeDefined()
expect(routeNameLog).not.toBe('undefined')
expect(routeNameLog).not.toBe('null')

await page.close()
})

it('maintains correct route information during navigation', async () => {
const logs: string[] = []
const page = await createPage()
page.on('console', (msg) => {
logs.push(msg.text())
})

// Navigate to tab1
await page.goto(url('/tabs/tab1'))
await page.waitForLoadState('networkidle')

logs.length = 0

// Navigate to tab2
await page.goto(url('/tabs/tab2'))
await page.waitForLoadState('networkidle')

// Route should not be undefined during or after navigation
const hasUndefinedRoute = logs.some(log => log === 'undefined' || log === 'null')
expect(hasUndefinedRoute).toBe(false)

await page.close()
})
})