Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

feat(nuxt): navigateTo supports external redirects #5022

Merged
merged 17 commits into from
Aug 24, 2022
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
37 changes: 30 additions & 7 deletions packages/nuxt/src/app/composables/router.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getCurrentInstance, inject } from 'vue'
import type { Router, RouteLocationNormalizedLoaded, NavigationGuard, RouteLocationNormalized, RouteLocationRaw, NavigationFailure } from 'vue-router'
import type { Router, RouteLocationNormalizedLoaded, NavigationGuard, RouteLocationNormalized, RouteLocationRaw, NavigationFailure, RouteLocationPathRaw } from 'vue-router'
import { sendRedirect } from 'h3'
import { joinURL } from 'ufo'
import { hasProtocol, joinURL, parseURL } from 'ufo'
import { useNuxtApp, useRuntimeConfig } from '#app'

export const useRouter = () => {
Expand Down Expand Up @@ -58,26 +58,49 @@ const isProcessingMiddleware = () => {

export interface NavigateToOptions {
replace?: boolean
redirectCode?: number
redirectCode?: number,
external?: boolean
}

export const navigateTo = (to: RouteLocationRaw | undefined | null, options: NavigateToOptions = {}): Promise<void | NavigationFailure> | RouteLocationRaw => {
if (!to) {
to = '/'
}
// Early redirect on client-side since only possible option is redirectCode and not applied
if (process.client && isProcessingMiddleware()) {

const toPath = typeof to === 'string' ? to : ((to as RouteLocationPathRaw).path || '/')
const isExternal = hasProtocol(toPath, true)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why you check protocol for external? may be I want to redirect within current domain, i.e. respond with headers

HTTP/1.1 302 Found
Location: /my-legacy-page

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you please open an issue with a sandbox to track? πŸ™πŸΌ

if (isExternal && !options.external) {
throw new Error('Navigating to external URL is not allowed by default. Use `nagivateTo (url, { external: true })`.')
}
if (isExternal && parseURL(toPath).protocol === 'script:') {
throw new Error('Cannot navigate to an URL with script protocol.')
}

// Early redirect on client-side
if (!isExternal && isProcessingMiddleware()) {
return to
}

const router = useRouter()

if (process.server) {
const nuxtApp = useNuxtApp()
if (nuxtApp.ssrContext && nuxtApp.ssrContext.event) {
const redirectLocation = joinURL(useRuntimeConfig().app.baseURL, router.resolve(to).fullPath || '/')
return nuxtApp.callHook('app:redirected').then(() => sendRedirect(nuxtApp.ssrContext!.event, redirectLocation, options.redirectCode || 302))
const redirectLocation = isExternal ? toPath : joinURL(useRuntimeConfig().app.baseURL, router.resolve(to).fullPath || '/')
return nuxtApp.callHook('app:redirected').then(() => sendRedirect(nuxtApp.ssrContext!.event, redirectLocation, options.redirectCode || 301))
}
}

// Client-side redirection using vue-router
if (isExternal) {
if (options.replace) {
location.replace(toPath)
} else {
location.href = toPath
}
return Promise.resolve()
Copy link
Member

@pi0 pi0 Aug 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last thing to ensure: In nuxt 2, we had this problem with external navigations that during navigation, rest of logic goes on. And solution was a custom made error because it was sync. I think we could do something like a never resolving promise that avoids this. (will make a reproduction and implement in later PR btw need more testing) => https://github.com/nuxt/framework/issues/6906

}

return options.replace ? router.replace(to) : router.push(to)
}

Expand Down
8 changes: 8 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ describe('navigate', () => {
})
})

describe('navigate external', () => {
it('should redirect to example.com', async () => {
const { headers } = await fetch('/navigate-to-external/', { redirect: 'manual' })

expect(headers.get('location')).toEqual('https://example.com/')
})
})

describe('errors', () => {
it('should render a JSON error page', async () => {
const res = await fetch('/error', {
Expand Down
7 changes: 7 additions & 0 deletions test/fixtures/basic/pages/navigate-to-external.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<div>You should not see me</div>
</template>

<script setup>
await navigateTo('https://example.com/', { external: true, replace: true })
</script>