diff --git a/examples/app-vitest-workspace/app2/nuxt.config.ts b/examples/app-vitest-workspace/app2/nuxt.config.ts
index d7b01c438..c154ef59d 100644
--- a/examples/app-vitest-workspace/app2/nuxt.config.ts
+++ b/examples/app-vitest-workspace/app2/nuxt.config.ts
@@ -1,6 +1,7 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
modules: ['@nuxtjs/color-mode', '@nuxt/ui'],
+ pages: false,
devtools: { enabled: true },
compatibilityDate: '2024-04-03',
})
diff --git a/examples/app-vitest-workspace/app2/test/nuxt/components.spec.ts b/examples/app-vitest-workspace/app2/test/nuxt/components.spec.ts
index 6d2aff508..483b4de3d 100644
--- a/examples/app-vitest-workspace/app2/test/nuxt/components.spec.ts
+++ b/examples/app-vitest-workspace/app2/test/nuxt/components.spec.ts
@@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import Badge from '@nuxt/ui/components/Badge.vue'
+import Breadcrumb from '@nuxt/ui/components/Breadcrumb.vue'
+import Header from '@nuxt/ui/components/Header.vue'
describe('components', () => {
it('ui/Badge', async () => {
@@ -12,4 +14,41 @@ describe('components', () => {
})
expect(wrapper.text()).toBe('Hello')
})
+
+ it('ui/Header', async () => {
+ const wrapper = await mountSuspended(Header, {
+ props: {
+ title: 'App',
+ to: '/',
+ },
+ })
+ expect(wrapper.text()).toBe('App')
+ expect(wrapper.find('a')?.attributes('href')).toBe('/')
+ })
+
+ it('ui/Breadcrumb', async () => {
+ const component = await mountSuspended(Breadcrumb, {
+ props: {
+ items: [
+ {
+ label: 'Home',
+ to: '/',
+ },
+ {
+ label: 'Help',
+ to: '/help',
+ },
+ ],
+ },
+ })
+
+ const links = component.findAll('a')
+ expect(links).toHaveLength(2)
+
+ expect(links[0]?.text()).toBe('Home')
+ expect(links[0]?.attributes('href')).toBe('/')
+
+ expect(links[1]?.text()).toBe('Help')
+ expect(links[1]?.attributes('href')).toBe('/help')
+ })
})
diff --git a/examples/app-vitest-workspace/app4/app/pages/index.vue b/examples/app-vitest-workspace/app4/app/pages/index.vue
new file mode 100644
index 000000000..7bd9480b5
--- /dev/null
+++ b/examples/app-vitest-workspace/app4/app/pages/index.vue
@@ -0,0 +1,3 @@
+
+ Index
+
diff --git a/examples/app-vitest-workspace/app4/nuxt.config.ts b/examples/app-vitest-workspace/app4/nuxt.config.ts
new file mode 100644
index 000000000..b377d542b
--- /dev/null
+++ b/examples/app-vitest-workspace/app4/nuxt.config.ts
@@ -0,0 +1,8 @@
+// https://nuxt.com/docs/api/configuration/nuxt-config
+export default defineNuxtConfig({
+ imports: {
+ autoImport: false,
+ },
+ devtools: { enabled: true },
+ compatibilityDate: '2024-04-03',
+})
diff --git a/examples/app-vitest-workspace/app4/test/nuxt/pages-off/nuxt-link.spec.ts b/examples/app-vitest-workspace/app4/test/nuxt/pages-off/nuxt-link.spec.ts
new file mode 100644
index 000000000..34b172c2c
--- /dev/null
+++ b/examples/app-vitest-workspace/app4/test/nuxt/pages-off/nuxt-link.spec.ts
@@ -0,0 +1,42 @@
+import { afterEach, describe, expect, it } from 'vitest'
+import { enableAutoUnmount } from '@vue/test-utils'
+import { mountSuspended } from '@nuxt/test-utils/runtime'
+
+import { NuxtLink } from '#components'
+
+type DefaultSlotProp = { isActive: boolean, isExactActive: boolean }
+
+describe('NuxtLink without pages', () => {
+ enableAutoUnmount(afterEach)
+
+ it('should mount', async () => {
+ const wrapper = await mountSuspended(NuxtLink, {
+ props: {
+ to: '/',
+ },
+ slots: {
+ default: () => 'Home',
+ },
+ })
+
+ const a = wrapper.find('a')
+ expect(a.exists()).toBe(true)
+ expect(a.attributes('href')).toBe('/')
+ expect(a.text()).toBe('Home')
+ })
+
+ it('should mount custom', async () => {
+ const wrapper = await mountSuspended(NuxtLink, {
+ props: {
+ to: '/',
+ custom: true,
+ },
+ slots: {
+ default: ({ isActive, isExactActive }: DefaultSlotProp) =>
+ `Home:${isActive}:${isExactActive}`,
+ },
+ })
+
+ expect(wrapper.text()).toBe('Home:undefined:undefined')
+ })
+})
diff --git a/examples/app-vitest-workspace/app4/test/nuxt/pages-on/nuxt-link.spec.ts b/examples/app-vitest-workspace/app4/test/nuxt/pages-on/nuxt-link.spec.ts
new file mode 100644
index 000000000..a238e08b0
--- /dev/null
+++ b/examples/app-vitest-workspace/app4/test/nuxt/pages-on/nuxt-link.spec.ts
@@ -0,0 +1,42 @@
+import { afterEach, describe, expect, it } from 'vitest'
+import { enableAutoUnmount } from '@vue/test-utils'
+import { mountSuspended } from '@nuxt/test-utils/runtime'
+
+import { NuxtLink } from '#components'
+
+type DefaultSlotProp = { isActive: boolean, isExactActive: boolean }
+
+describe('NuxtLink with pages', () => {
+ enableAutoUnmount(afterEach)
+
+ it('should mount', async () => {
+ const wrapper = await mountSuspended(NuxtLink, {
+ props: {
+ to: '/',
+ },
+ slots: {
+ default: () => 'Home',
+ },
+ })
+
+ const a = wrapper.find('a')
+ expect(a.exists()).toBe(true)
+ expect(a.attributes('href')).toBe('/')
+ expect(a.text()).toBe('Home')
+ })
+
+ it('should mount custom', async () => {
+ const wrapper = await mountSuspended(NuxtLink, {
+ props: {
+ to: '/',
+ custom: true,
+ },
+ slots: {
+ default: ({ isActive, isExactActive }: DefaultSlotProp) =>
+ `Home:${isActive}:${isExactActive}`,
+ },
+ })
+
+ expect(wrapper.text()).toBe('Home:true:true')
+ })
+})
diff --git a/examples/app-vitest-workspace/app4/tsconfig.json b/examples/app-vitest-workspace/app4/tsconfig.json
new file mode 100644
index 000000000..6ae5970c7
--- /dev/null
+++ b/examples/app-vitest-workspace/app4/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "files": [],
+ "references": [
+ {
+ "path": "./.nuxt/tsconfig.app.json"
+ },
+ {
+ "path": "./.nuxt/tsconfig.server.json"
+ },
+ {
+ "path": "./.nuxt/tsconfig.shared.json"
+ },
+ {
+ "path": "./.nuxt/tsconfig.node.json"
+ }
+ ]
+}
diff --git a/examples/app-vitest-workspace/app4/vitest.pages-off.config.ts b/examples/app-vitest-workspace/app4/vitest.pages-off.config.ts
new file mode 100644
index 000000000..c953af449
--- /dev/null
+++ b/examples/app-vitest-workspace/app4/vitest.pages-off.config.ts
@@ -0,0 +1,17 @@
+import { fileURLToPath } from 'node:url'
+import { defineVitestProject } from '@nuxt/test-utils/config'
+
+export default defineVitestProject({
+ test: {
+ name: 'nuxt-app4:pages-off',
+ include: ['test/nuxt/pages-off/*.spec.ts'],
+ environmentOptions: {
+ nuxt: {
+ rootDir: fileURLToPath(new URL('.', import.meta.url)),
+ overrides: {
+ pages: false,
+ },
+ },
+ },
+ },
+})
diff --git a/examples/app-vitest-workspace/app4/vitest.pages-on.config.ts b/examples/app-vitest-workspace/app4/vitest.pages-on.config.ts
new file mode 100644
index 000000000..dc7be2729
--- /dev/null
+++ b/examples/app-vitest-workspace/app4/vitest.pages-on.config.ts
@@ -0,0 +1,17 @@
+import { fileURLToPath } from 'node:url'
+import { defineVitestProject } from '@nuxt/test-utils/config'
+
+export default defineVitestProject({
+ test: {
+ name: 'nuxt-app4:pages-on',
+ include: ['test/nuxt/pages-on/*.spec.ts'],
+ environmentOptions: {
+ nuxt: {
+ rootDir: fileURLToPath(new URL('.', import.meta.url)),
+ overrides: {
+ pages: true,
+ },
+ },
+ },
+ },
+})
diff --git a/examples/app-vitest-workspace/package.json b/examples/app-vitest-workspace/package.json
index 851033ee0..ab58a455f 100644
--- a/examples/app-vitest-workspace/package.json
+++ b/examples/app-vitest-workspace/package.json
@@ -7,7 +7,11 @@
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
- "dev:prepare": "nuxt prepare app1 && nuxt prepare app2 && nuxt prepare app3",
+ "dev:prepare": "pnpm run '/dev:prepare:.*/'",
+ "dev:prepare:app1": "nuxt prepare app1",
+ "dev:prepare:app2": "nuxt prepare app2",
+ "dev:prepare:app3": "nuxt prepare app3",
+ "dev:prepare:app4": "nuxt prepare app4",
"postinstall": "pnpm dev:prepare",
"test": "vitest run"
},
diff --git a/src/runtime-utils/components/RouterLink.ts b/src/runtime-utils/components/RouterLink.ts
index f26904920..1422da17a 100644
--- a/src/runtime-utils/components/RouterLink.ts
+++ b/src/runtime-utils/components/RouterLink.ts
@@ -1,5 +1,12 @@
-import { defineComponent, h } from '#imports'
-import { useLink } from 'vue-router'
+import { defineComponent, h, useRouter, useNuxtApp } from '#imports'
+import type { useLink as useLinkFn } from 'vue-router'
+
+function getUseLink(nuxtApp: ReturnType) {
+ const linkComponent = nuxtApp.vueApp._context.components.RouterLink
+ if (!linkComponent || typeof linkComponent !== 'object') return undefined
+ if (typeof linkComponent.useLink !== 'function') return undefined
+ return linkComponent.useLink.bind(linkComponent) as typeof useLinkFn
+}
export const RouterLink = defineComponent({
functional: true,
@@ -16,6 +23,31 @@ export const RouterLink = defineComponent({
ariaCurrentValue: String,
},
setup: (props, { slots }) => {
+ const app = useNuxtApp()
+ const useLink = getUseLink(app)
+
+ if (!useLink) {
+ const navigate = () => {}
+ const router = useRouter()
+ return () => {
+ const route = router.resolve(props.to)
+
+ return props.custom
+ ? slots.default?.({ href: route.href, navigate, route })
+ : h(
+ 'a',
+ {
+ href: route.href,
+ onClick: (e: MouseEvent) => {
+ e.preventDefault()
+ return navigate()
+ },
+ },
+ slots,
+ )
+ }
+ }
+
const link = useLink(props)
return () => {