Skip to content

Commit

Permalink
fix(nuxt): resolve internal target: blank links with base (#23751)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jannchie committed Oct 20, 2023
1 parent 7fcdce2 commit ffa6b6e
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 4 deletions.
16 changes: 12 additions & 4 deletions packages/nuxt/src/app/components/nuxt-link.ts
@@ -1,12 +1,12 @@
import type { ComputedRef, DefineComponent, InjectionKey, PropType } from 'vue'
import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, resolveComponent } from 'vue'
import type { RouteLocation, RouteLocationRaw } from '#vue-router'
import { hasProtocol, parseQuery, parseURL, withTrailingSlash, withoutTrailingSlash } from 'ufo'
import { hasProtocol, joinURL, parseQuery, parseURL, withTrailingSlash, withoutTrailingSlash } from 'ufo'

import { preloadRouteComponents } from '../composables/preload'
import { onNuxtReady } from '../composables/ready'
import { navigateTo, useRouter } from '../composables/router'
import { useNuxtApp } from '../nuxt'
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
import { cancelIdleCallback, requestIdleCallback } from '../compat/idle-callback'

// @ts-expect-error virtual file
Expand Down Expand Up @@ -170,6 +170,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
},
setup (props, { slots }) {
const router = useRouter()
const config = useRuntimeConfig()

// Resolving `to` value from `to` and `href` props
const to: ComputedRef<string | RouteLocationRaw> = computed(() => {
Expand All @@ -180,6 +181,9 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
return resolveTrailingSlashBehavior(path, router.resolve)
})

// Lazily check whether to.value has a protocol
const isProtocolURL = computed(() => typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true }))

// Resolving link type
const isExternal = computed<boolean>(() => {
// External prop is explicitly set
Expand All @@ -197,7 +201,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
return false
}

return to.value === '' || hasProtocol(to.value, { acceptRelative: true })
return to.value === '' || isProtocolURL.value
})

// Prefetching
Expand Down Expand Up @@ -280,7 +284,11 @@ export function defineNuxtLink (options: NuxtLinkOptions) {

// Resolves `to` value if it's a route location object
// converts `""` to `null` to prevent the attribute from being added as empty (`href=""`)
const href = typeof to.value === 'object' ? router.resolve(to.value)?.href ?? null : to.value || null
const href = typeof to.value === 'object'
? router.resolve(to.value)?.href ?? null
: (to.value && !props.external && !isProtocolURL.value)
? resolveTrailingSlashBehavior(joinURL(config.app.baseURL, to.value), router.resolve) as string
: to.value || null

// Resolves `target` value
const target = props.target || null
Expand Down
44 changes: 44 additions & 0 deletions packages/nuxt/test/nuxt-link.test.ts
Expand Up @@ -2,6 +2,16 @@ import { describe, expect, it, vi } from 'vitest'
import type { RouteLocation, RouteLocationRaw } from 'vue-router'
import type { NuxtLinkOptions, NuxtLinkProps } from '../src/app/components/nuxt-link'
import { defineNuxtLink } from '../src/app/components/nuxt-link'
import { useRuntimeConfig } from '../src/app/nuxt'

// mocks `useRuntimeConfig()`
vi.mock('../src/app/nuxt', () => ({
useRuntimeConfig: vi.fn(() => ({
app: {
baseURL: '/'
}
}))
}))

// Mocks `h()`
vi.mock('vue', async () => {
Expand Down Expand Up @@ -125,6 +135,40 @@ describe('nuxt-link:propsOrAttributes', () => {
it('defaults to `null`', () => {
expect(nuxtLink({ to: 'https://nuxtjs.org' }).props.target).toBe(null)
})

it('prefixes target="_blank" internal links with baseURL', () => {
vi.mocked(useRuntimeConfig).withImplementation(() => {
return {
app: {
baseURL: '/base'
}
} as any
}, () => {
expect(nuxtLink({ to: '/', target: '_blank' }).props.href).toBe('/base')
expect(nuxtLink({ to: '/base', target: '_blank' }).props.href).toBe('/base/base')
expect(nuxtLink({ to: '/to', target: '_blank' }).props.href).toBe('/base/to')
expect(nuxtLink({ to: '/base/to', target: '_blank' }).props.href).toBe('/base/base/to')
expect(nuxtLink({ to: '//base/to', target: '_blank' }).props.href).toBe('//base/to')
expect(nuxtLink({ to: '//to.com/thing', target: '_blank' }).props.href).toBe('//to.com/thing')
expect(nuxtLink({ to: 'https://test.com/to', target: '_blank' }).props.href).toBe('https://test.com/to')

expect(nuxtLink({ to: '/', target: '_blank' }, { trailingSlash: 'append' }).props.href).toBe('/base/')
expect(nuxtLink({ to: '/base/', target: '_blank' }, { trailingSlash: 'remove' }).props.href).toBe('/base/base')
})
})

it('excludes the baseURL for external links', () => {
vi.mocked(useRuntimeConfig).withImplementation(() => {
return {
app: {
baseURL: '/base'
}
} as any
}, () => {
expect(nuxtLink({ to: 'http://nuxtjs.org/app/about', target: '_blank' }).props.href).toBe('http://nuxtjs.org/app/about')
expect(nuxtLink({ to: '//nuxtjs.org/app/about', target: '_blank' }).props.href).toBe('//nuxtjs.org/app/about')
})
})
})

describe('rel', () => {
Expand Down

0 comments on commit ffa6b6e

Please sign in to comment.