From c14217ecc47b11259888d563e601c7a5e02c935e Mon Sep 17 00:00:00 2001 From: Jianqi Pan Date: Thu, 19 Oct 2023 00:47:31 +0900 Subject: [PATCH 01/12] fix(link): `target=_blank` nuxt link should join baseURL --- packages/nuxt/src/app/components/nuxt-link.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index 4d8db7f1dbb1..73024db6baee 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -1,7 +1,7 @@ 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, parseQuery, parseURL, joinURL, withTrailingSlash, withoutTrailingSlash } from 'ufo' import { preloadRouteComponents } from '../composables/preload' import { onNuxtReady } from '../composables/ready' @@ -280,7 +280,12 @@ 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 + let href = typeof to.value === 'object' ? router.resolve(to.value)?.href ?? null : to.value || null + + // joins with `baseURL` if it's a relative URL + if (!hasProtocol(href, { acceptRelative: true })) { + href = joinURL(useRuntimeConfig().app.baseURL, href) + } // Resolves `target` value const target = props.target || null From 91e103cb857d969949e2594b40f7ca0987511eb7 Mon Sep 17 00:00:00 2001 From: Jianqi Pan Date: Thu, 19 Oct 2023 01:01:39 +0900 Subject: [PATCH 02/12] fix(nuxt): import useRuntimeConfig --- packages/nuxt/src/app/components/nuxt-link.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index 73024db6baee..c8743c4851fa 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -6,7 +6,7 @@ import { hasProtocol, parseQuery, parseURL, joinURL, withTrailingSlash, withoutT 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 From 1f1d4f8eac93059cde4386d3bc100f4c56d0f3b9 Mon Sep 17 00:00:00 2001 From: Jianqi Pan Date: Thu, 19 Oct 2023 01:10:16 +0900 Subject: [PATCH 03/12] fix(link): type check --- packages/nuxt/src/app/components/nuxt-link.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index c8743c4851fa..427d982e64a3 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -283,7 +283,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) { let href = typeof to.value === 'object' ? router.resolve(to.value)?.href ?? null : to.value || null // joins with `baseURL` if it's a relative URL - if (!hasProtocol(href, { acceptRelative: true })) { + if (href && !hasProtocol(href, { acceptRelative: true })) { href = joinURL(useRuntimeConfig().app.baseURL, href) } From e818e12ed4eebaab97156a8e4864ef9e7fd4db83 Mon Sep 17 00:00:00 2001 From: Jianqi Pan Date: Thu, 19 Oct 2023 01:50:10 +0900 Subject: [PATCH 04/12] fix(link): pass unit test --- packages/nuxt/src/app/components/nuxt-link.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index 427d982e64a3..c9cda16a433e 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -1,8 +1,7 @@ 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, joinURL, withTrailingSlash, withoutTrailingSlash } from 'ufo' - +import { hasProtocol, joinURL, parseQuery, parseURL, withLeadingSlash, withTrailingSlash, withoutTrailingSlash } from 'ufo' import { preloadRouteComponents } from '../composables/preload' import { onNuxtReady } from '../composables/ready' import { navigateTo, useRouter } from '../composables/router' @@ -282,11 +281,13 @@ export function defineNuxtLink (options: NuxtLinkOptions) { // converts `""` to `null` to prevent the attribute from being added as empty (`href=""`) let href = typeof to.value === 'object' ? router.resolve(to.value)?.href ?? null : to.value || null - // joins with `baseURL` if it's a relative URL - if (href && !hasProtocol(href, { acceptRelative: true })) { - href = joinURL(useRuntimeConfig().app.baseURL, href) + // joins with `baseURL` if it's an relative link + if (import.meta.client) { + if (href && href.startsWith('/') && !href.startsWith('//')) { + const baseURL = useRuntimeConfig().app.baseURL + href = joinURL(withLeadingSlash(withTrailingSlash(baseURL)), href) + } } - // Resolves `target` value const target = props.target || null From 6de4dc81054000b1ecbb32d574ccc40e645c78e4 Mon Sep 17 00:00:00 2001 From: Jianqi Pan Date: Thu, 19 Oct 2023 01:54:48 +0900 Subject: [PATCH 05/12] fix(link): ignore href start with baseURL --- packages/nuxt/src/app/components/nuxt-link.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index c9cda16a433e..7c4ad93bdef0 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -282,9 +282,9 @@ export function defineNuxtLink (options: NuxtLinkOptions) { let href = typeof to.value === 'object' ? router.resolve(to.value)?.href ?? null : to.value || null // joins with `baseURL` if it's an relative link - if (import.meta.client) { - if (href && href.startsWith('/') && !href.startsWith('//')) { - const baseURL = useRuntimeConfig().app.baseURL + if (import.meta.client && href && href.startsWith('/') && !href.startsWith('//')) { + const baseURL = useRuntimeConfig().app.baseURL + if (href !== '/' && !href.startsWith(baseURL)) { href = joinURL(withLeadingSlash(withTrailingSlash(baseURL)), href) } } From 506d95832d6a5ca0140890632f05f62007099c7f Mon Sep 17 00:00:00 2001 From: Jianqi Pan Date: Thu, 19 Oct 2023 14:50:11 +0900 Subject: [PATCH 06/12] Update packages/nuxt/src/app/components/nuxt-link.ts Co-authored-by: Alex Liu --- packages/nuxt/src/app/components/nuxt-link.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index 7c4ad93bdef0..70cf77cf1b1e 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -282,7 +282,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) { let href = typeof to.value === 'object' ? router.resolve(to.value)?.href ?? null : to.value || null // joins with `baseURL` if it's an relative link - if (import.meta.client && href && href.startsWith('/') && !href.startsWith('//')) { + if (href && href.startsWith('/') && !href.startsWith('//')) { const baseURL = useRuntimeConfig().app.baseURL if (href !== '/' && !href.startsWith(baseURL)) { href = joinURL(withLeadingSlash(withTrailingSlash(baseURL)), href) From 98a1985bc7b3a5ed21293d891ad5ff4790fa7b59 Mon Sep 17 00:00:00 2001 From: Jianqi Pan Date: Thu, 19 Oct 2023 16:21:51 +0900 Subject: [PATCH 07/12] chore(test): add text case & mock --- packages/nuxt/test/nuxt-link.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/nuxt/test/nuxt-link.test.ts b/packages/nuxt/test/nuxt-link.test.ts index a3743de40dc2..c1e766366c65 100644 --- a/packages/nuxt/test/nuxt-link.test.ts +++ b/packages/nuxt/test/nuxt-link.test.ts @@ -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,21 @@ describe('nuxt-link:propsOrAttributes', () => { it('defaults to `null`', () => { expect(nuxtLink({ to: 'https://nuxtjs.org' }).props.target).toBe(null) }) + + it('target="_blank" with baseURL', () => { + vi.mocked(useRuntimeConfig).withImplementation(() => { + return { + app: { + baseURL: '/base' + } + } as any + }, () => { + expect(nuxtLink({ to: '/to', target: '_blank' }).props.href).toBe('/base/to') + expect(nuxtLink({ to: '/base/to', target: '_blank' }).props.href).toBe('/base/to') + 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', () => { From f4d4be400b634fed27857ebc45def167a109fc2a Mon Sep 17 00:00:00 2001 From: Jianqi Pan Date: Fri, 20 Oct 2023 21:21:28 +0900 Subject: [PATCH 08/12] Update packages/nuxt/src/app/components/nuxt-link.ts Co-authored-by: Lucie <25330882+lihbr@users.noreply.github.com> --- packages/nuxt/src/app/components/nuxt-link.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index 70cf77cf1b1e..3521beb4e7bd 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -284,7 +284,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) { // joins with `baseURL` if it's an relative link if (href && href.startsWith('/') && !href.startsWith('//')) { const baseURL = useRuntimeConfig().app.baseURL - if (href !== '/' && !href.startsWith(baseURL)) { + if (baseURL !== '/' && !href.startsWith(baseURL)) { href = joinURL(withLeadingSlash(withTrailingSlash(baseURL)), href) } } From f4a3799f7368f1b839ecb0d258f756c7c0767d0c Mon Sep 17 00:00:00 2001 From: Jianqi Pan Date: Fri, 20 Oct 2023 21:26:54 +0900 Subject: [PATCH 09/12] chore(test): rename & separate test case --- packages/nuxt/test/nuxt-link.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/test/nuxt-link.test.ts b/packages/nuxt/test/nuxt-link.test.ts index c1e766366c65..b8b09e4602d6 100644 --- a/packages/nuxt/test/nuxt-link.test.ts +++ b/packages/nuxt/test/nuxt-link.test.ts @@ -136,7 +136,7 @@ describe('nuxt-link:propsOrAttributes', () => { expect(nuxtLink({ to: 'https://nuxtjs.org' }).props.target).toBe(null) }) - it('target="_blank" with baseURL', () => { + it('prefixes target="_blank" internal links with baseURL', () => { vi.mocked(useRuntimeConfig).withImplementation(() => { return { app: { @@ -144,8 +144,21 @@ describe('nuxt-link:propsOrAttributes', () => { } } as any }, () => { + expect(nuxtLink({ to: '/', target: '_blank' }).props.href).toBe('/base/') + expect(nuxtLink({ to: '/base', target: '_blank' }).props.href).toBe('/base') expect(nuxtLink({ to: '/to', target: '_blank' }).props.href).toBe('/base/to') expect(nuxtLink({ to: '/base/to', target: '_blank' }).props.href).toBe('/base/to') + }) + }) + + 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') }) From b08b978349be1b4a05687eef4d889b9249b7f43f Mon Sep 17 00:00:00 2001 From: Lucie <25330882+lihbr@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:45:58 +0200 Subject: [PATCH 10/12] chore: typo --- packages/nuxt/src/app/components/nuxt-link.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index 3521beb4e7bd..f2e8494baf4a 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -281,7 +281,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) { // converts `""` to `null` to prevent the attribute from being added as empty (`href=""`) let href = typeof to.value === 'object' ? router.resolve(to.value)?.href ?? null : to.value || null - // joins with `baseURL` if it's an relative link + // Joins relative links with `baseURL` if (href && href.startsWith('/') && !href.startsWith('//')) { const baseURL = useRuntimeConfig().app.baseURL if (baseURL !== '/' && !href.startsWith(baseURL)) { From 85832850a6b37cc9b2257db4d42e9813e9965c9f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 20 Oct 2023 15:24:04 +0100 Subject: [PATCH 11/12] fix: resolve urls consistently with and without `_target: 'blank'` - move `useRuntimeConfig()` out of render function - respect `trailingSlashBehavior` - don't reimplement `hasProtocol` --- packages/nuxt/src/app/components/nuxt-link.ts | 22 ++++++++++--------- packages/nuxt/test/nuxt-link.test.ts | 12 +++++++--- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index f2e8494baf4a..2a223adbb38e 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -1,7 +1,8 @@ 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, joinURL, parseQuery, parseURL, withLeadingSlash, 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' @@ -169,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 = computed(() => { @@ -179,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(() => { // External prop is explicitly set @@ -196,7 +201,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) { return false } - return to.value === '' || hasProtocol(to.value, { acceptRelative: true }) + return to.value === '' || isProtocolURL.value }) // Prefetching @@ -279,15 +284,12 @@ 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=""`) - let 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) + : to.value || null - // Joins relative links with `baseURL` - if (href && href.startsWith('/') && !href.startsWith('//')) { - const baseURL = useRuntimeConfig().app.baseURL - if (baseURL !== '/' && !href.startsWith(baseURL)) { - href = joinURL(withLeadingSlash(withTrailingSlash(baseURL)), href) - } - } // Resolves `target` value const target = props.target || null diff --git a/packages/nuxt/test/nuxt-link.test.ts b/packages/nuxt/test/nuxt-link.test.ts index b8b09e4602d6..6cf247886d84 100644 --- a/packages/nuxt/test/nuxt-link.test.ts +++ b/packages/nuxt/test/nuxt-link.test.ts @@ -144,10 +144,16 @@ describe('nuxt-link:propsOrAttributes', () => { } } as any }, () => { - expect(nuxtLink({ to: '/', target: '_blank' }).props.href).toBe('/base/') - expect(nuxtLink({ to: '/base', target: '_blank' }).props.href).toBe('/base') + 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/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') }) }) From a959e44d2b9c331b01c87af3266d8f9142d15128 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 20 Oct 2023 16:01:10 +0100 Subject: [PATCH 12/12] fix: assert type --- packages/nuxt/src/app/components/nuxt-link.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index 2a223adbb38e..d2ee73ca47a0 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -287,7 +287,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) { 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) + ? resolveTrailingSlashBehavior(joinURL(config.app.baseURL, to.value), router.resolve) as string : to.value || null // Resolves `target` value