fix(nuxt): resolve internal target: blank links with base (#23751)

This commit is contained in:
Jianqi Pan 2023-10-21 00:33:45 +09:00 committed by GitHub
parent 7fcdce26b2
commit ffa6b6e60c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 56 additions and 4 deletions

View File

@ -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
@ -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(() => {
@ -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
@ -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
@ -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

View File

@ -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 () => {
@ -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', () => {