From ca65f150b368af416c0e2fce71bf13b951c8853b Mon Sep 17 00:00:00 2001 From: Julien Huang Date: Fri, 11 Oct 2024 12:31:46 +0200 Subject: [PATCH] fix(nuxt): allow islands to manipulate head client-side (#29186) --- .../nuxt/src/app/components/nuxt-island.ts | 24 ++++++++++--------- test/basic.test.ts | 9 +++++++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index c6bb6d8657..5d353c8375 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -1,9 +1,9 @@ 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 { hash } from 'ohash' import { appendResponseHeader } from 'h3' -import { injectHead } from '@unhead/vue' +import { type ActiveHeadEntry, type Head, injectHead } from '@unhead/vue' import { randomUUID } from 'uncrypto' import { joinURL, withQuery } from 'ufo' import type { FetchResponse } from 'ofetch' @@ -90,11 +90,13 @@ export default defineComponent({ const instance = getCurrentInstance()! const event = useRequestEvent() + let activeHead: ActiveHeadEntry + // 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 mounted = ref(false) onMounted(() => { mounted.value = true; teleportKey.value++ }) - + onBeforeUnmount(() => { if (activeHead) { activeHead.dispose() } }) function setPayload (key: string, result: NuxtIslandResponse) { const toRevive: Partial = {} 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) { // must await next tick for Teleport to work correctly with static node re-rendering nextTick(() => { @@ -250,14 +260,6 @@ export default defineComponent({ 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) => { if (!html.value || error.value) { return [slots.fallback?.({ error: error.value }) ?? createVNode('div')] diff --git a/test/basic.test.ts b/test/basic.test.ts index 01a135223e..1c65baa6bb 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -1984,6 +1984,15 @@ describe('server components/islands', () => { expect(html).toContain('') }) + 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('') + await page.close() + }) + it.skipIf(isDev)('should allow server-only components to set prerender hints', async () => { // @ts-expect-error ssssh! untyped secret property const publicDir = useTestContext().nuxt._nitro.options.output.publicDir