Skip to content
This repository was archived by the owner on Apr 6, 2023. It is now read-only.
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
13 changes: 11 additions & 2 deletions packages/nuxt/src/app/components/nuxt-island.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { defineComponent, createStaticVNode, computed, ref, watch } from 'vue'
import { debounce } from 'perfect-debounce'
import { hash } from 'ohash'
import type { MetaObject } from '@nuxt/schema'
import { appendHeader } from 'h3'
// eslint-disable-next-line import/no-restricted-paths
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
import { useHead, useNuxtApp } from '#app'
import { useHead, useNuxtApp, useRequestEvent } from '#app'

const pKey = '_islandPromises'

Expand All @@ -27,13 +28,21 @@ export default defineComponent({
async setup (props) {
const nuxtApp = useNuxtApp()
const hashId = computed(() => hash([props.name, props.props, props.context]))

const event = useRequestEvent()

const html = ref<string>('')
const cHead = ref<MetaObject>({ link: [], style: [] })
useHead(cHead)

function _fetchComponent () {
const url = `/__nuxt_island/${props.name}:${hashId.value}`
if (process.server && process.env.prerender) {
// Hint to Nitro to prerender the island component
appendHeader(event, 'x-nitro-prerender', url)
}
// TODO: Validate response
return $fetch<NuxtIslandResponse>(`/__nuxt_island/${props.name}:${hashId.value}`, {
return $fetch<NuxtIslandResponse>(url, {
params: {
...props.context,
props: props.props ? JSON.stringify(props.props) : undefined
Expand Down
91 changes: 87 additions & 4 deletions packages/nuxt/src/components/runtime/server-component.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,99 @@
import { defineComponent, h } from 'vue'
// @ts-expect-error virtual import
import { NuxtIsland } from '#components'
import { defineComponent, createStaticVNode, computed, h, watch } from 'vue'
import { debounce } from 'perfect-debounce'
import { hash } from 'ohash'
import { appendHeader } from 'h3'

import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
import { useAsyncData, useHead, useNuxtApp, useRequestEvent } from '#app'

const pKey = '_islandPromises'

export const createServerComponent = (name: string) => {
return defineComponent({
name,
inheritAttrs: false,
setup (_props, { attrs }) {
return () => h(NuxtIsland, {
return () => h(NuxtServerComponent, {
name,
props: attrs
})
}
})
}

const NuxtServerComponent = defineComponent({
name: 'NuxtServerComponent',
props: {
name: {
type: String,
required: true
},
props: {
type: Object,
default: () => undefined
},
context: {
type: Object,
default: () => ({})
}
},
async setup (props) {
const nuxtApp = useNuxtApp()
const hashId = computed(() => hash([props.name, props.props, props.context]))

const event = useRequestEvent()

function _fetchComponent () {
const url = `/__nuxt_island/${props.name}:${hashId.value}`
if (process.server && process.env.prerender) {
// Hint to Nitro to prerender the island component
appendHeader(event, 'x-nitro-prerender', url)
}
// TODO: Validate response
return $fetch<NuxtIslandResponse>(url, {
params: {
...props.context,
props: props.props ? JSON.stringify(props.props) : undefined
}
})
}

const res = useAsyncData(
`${props.name}:${hashId.value}`,
async () => {
nuxtApp[pKey] = nuxtApp[pKey] || {}
if (!nuxtApp[pKey][hashId.value]) {
nuxtApp[pKey][hashId.value] = _fetchComponent().finally(() => {
delete nuxtApp[pKey][hashId.value]
})
}
const res: NuxtIslandResponse = await nuxtApp[pKey][hashId.value]
return {
html: res.html,
head: {
link: res.head.link,
style: res.head.style
}
}
}, {
immediate: process.server || !nuxtApp.isHydrating,
default: () => ({
html: '',
head: {
link: [], style: []
}
})
}
)

useHead(() => res.data.value!.head)

if (process.client) {
watch(props, debounce(() => res.execute(), 100))
}

await res

return () => createStaticVNode(res.data.value!.html, 1)
}
})
2 changes: 1 addition & 1 deletion packages/nuxt/src/core/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createHooks, createDebugger } from 'hookable'
import type { Nuxt, NuxtOptions, NuxtHooks } from '@nuxt/schema'
import type { LoadNuxtOptions } from '@nuxt/kit'
import { loadNuxtConfig, nuxtCtx, installModule, addComponent, addVitePlugin, addWebpackPlugin, tryResolveModule, addPlugin } from '@nuxt/kit'
/* eslint-disable import/no-restricted-paths */

import escapeRE from 'escape-string-regexp'
import fse from 'fs-extra'
import { withoutLeadingSlash } from 'ufo'
Expand Down
5 changes: 4 additions & 1 deletion test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV || isWindows)('payload rendering', ()
it('renders a payload', async () => {
const payload = await $fetch('/random/a/_payload.js', { responseType: 'text' })
expect(payload).toMatch(
/export default \{data:\{hey:{[^}]*},rand_a:\[[^\]]*\]\},prerenderedAt:\d*\}/
/export default \{data:\{hey:\{[^}]*\},rand_a:\[[^\]]*\],".*":\{html:".*server-only component.*",head:\{link:\[\],style:\[\]\}\}\},prerenderedAt:\d*\}/
)
})

Expand All @@ -930,13 +930,15 @@ describe.skipIf(process.env.NUXT_TEST_DEV || isWindows)('payload rendering', ()

// We are not triggering API requests in the payload
expect(requests).not.toContain(expect.stringContaining('/api/random'))
expect(requests).not.toContain(expect.stringContaining('/__nuxt_island'))
// requests.length = 0

await page.click('[href="/random/b"]')
await page.waitForLoadState('networkidle')

// We are not triggering API requests in the payload in client-side nav
expect(requests).not.toContain('/api/random')
expect(requests).not.toContain(expect.stringContaining('/__nuxt_island'))

// We are fetching a payload we did not prefetch
expect(requests).toContain('/random/b/_payload.js' + importSuffix)
Expand All @@ -950,6 +952,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV || isWindows)('payload rendering', ()

// We are not triggering API requests in the payload in client-side nav
expect(requests).not.toContain('/api/random')
expect(requests).not.toContain(expect.stringContaining('/__nuxt_island'))

// We are not refetching payloads we've already prefetched
// Note: we refetch on dev as urls differ between '' and '?import'
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/basic/pages/random/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<NuxtLink to="/random/c" prefetched-class="prefetched">
Random (C)
</NuxtLink>
<ServerOnlyComponent />
<br>

Random: {{ random }}
Expand Down
3 changes: 1 addition & 2 deletions test/fixtures/basic/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import type { AppConfig } from '@nuxt/schema'

import type { FetchError } from 'ofetch'
import type { NavigationFailure, RouteLocationNormalizedLoaded, RouteLocationRaw, useRouter as vueUseRouter } from 'vue-router'
import type { NavigateToOptions } from '~~/../../../packages/nuxt/dist/app/composables/router'
// eslint-disable-next-line import/order
import { isVue3 } from '#app'
import type { NavigateToOptions } from '~~/../../../packages/nuxt/dist/app/composables/router'
import { defineNuxtConfig } from '~~/../../../packages/nuxt/config'
import { useRouter } from '#imports'

Expand Down