fix(nuxt): allow islands to manipulate head client-side (#29186)

This commit is contained in:
Julien Huang 2024-10-11 12:31:46 +02:00 committed by GitHub
parent 1a659b326a
commit ca65f150b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 22 additions and 11 deletions

View File

@ -1,9 +1,9 @@
import type { Component, PropType, VNode } from 'vue' import type { Component, PropType, VNode } from 'vue'
import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch, withMemo } from 'vue' import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch, withMemo } from 'vue'
import { debounce } from 'perfect-debounce' import { debounce } from 'perfect-debounce'
import { hash } from 'ohash' import { hash } from 'ohash'
import { appendResponseHeader } from 'h3' import { appendResponseHeader } from 'h3'
import { injectHead } from '@unhead/vue' import { type ActiveHeadEntry, type Head, injectHead } from '@unhead/vue'
import { randomUUID } from 'uncrypto' import { randomUUID } from 'uncrypto'
import { joinURL, withQuery } from 'ufo' import { joinURL, withQuery } from 'ufo'
import type { FetchResponse } from 'ofetch' import type { FetchResponse } from 'ofetch'
@ -90,11 +90,13 @@ export default defineComponent({
const instance = getCurrentInstance()! const instance = getCurrentInstance()!
const event = useRequestEvent() const event = useRequestEvent()
let activeHead: ActiveHeadEntry<Head>
// TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved // TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved
const eventFetch = import.meta.server ? event!.fetch : import.meta.dev ? $fetch.raw : globalThis.fetch const eventFetch = import.meta.server ? event!.fetch : import.meta.dev ? $fetch.raw : globalThis.fetch
const mounted = ref(false) const mounted = ref(false)
onMounted(() => { mounted.value = true; teleportKey.value++ }) onMounted(() => { mounted.value = true; teleportKey.value++ })
onBeforeUnmount(() => { if (activeHead) { activeHead.dispose() } })
function setPayload (key: string, result: NuxtIslandResponse) { function setPayload (key: string, result: NuxtIslandResponse) {
const toRevive: Partial<NuxtIslandResponse> = {} const toRevive: Partial<NuxtIslandResponse> = {}
if (result.props) { toRevive.props = result.props } if (result.props) { toRevive.props = result.props }
@ -215,6 +217,14 @@ export default defineComponent({
} }
} }
if (res?.head) {
if (activeHead) {
activeHead.patch(res.head)
} else {
activeHead = head.push(res.head)
}
}
if (import.meta.client) { if (import.meta.client) {
// must await next tick for Teleport to work correctly with static node re-rendering // must await next tick for Teleport to work correctly with static node re-rendering
nextTick(() => { nextTick(() => {
@ -250,14 +260,6 @@ export default defineComponent({
await loadComponents(props.source, payloads.components) await loadComponents(props.source, payloads.components)
} }
if (import.meta.server || nuxtApp.isHydrating) {
// re-push head into active head instance
const responseHead = (nuxtApp.payload.data[`${props.name}_${hashId.value}`] as NuxtIslandResponse)?.head
if (responseHead) {
head.push(responseHead)
}
}
return (_ctx: any, _cache: any) => { return (_ctx: any, _cache: any) => {
if (!html.value || error.value) { if (!html.value || error.value) {
return [slots.fallback?.({ error: error.value }) ?? createVNode('div')] return [slots.fallback?.({ error: error.value }) ?? createVNode('div')]

View File

@ -1984,6 +1984,15 @@ describe('server components/islands', () => {
expect(html).toContain('<meta name="author" content="Nuxt">') expect(html).toContain('<meta name="author" content="Nuxt">')
}) })
it('/server-page - client side navigation', async () => {
const { page } = await renderPage('/')
await page.getByText('to server page').click()
await page.waitForLoadState('networkidle')
expect(await page.innerHTML('head')).toContain('<meta name="author" content="Nuxt">')
await page.close()
})
it.skipIf(isDev)('should allow server-only components to set prerender hints', async () => { it.skipIf(isDev)('should allow server-only components to set prerender hints', async () => {
// @ts-expect-error ssssh! untyped secret property // @ts-expect-error ssssh! untyped secret property
const publicDir = useTestContext().nuxt._nitro.options.output.publicDir const publicDir = useTestContext().nuxt._nitro.options.output.publicDir