diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index f849e6b99c..c9a4619c54 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -248,9 +248,10 @@ export default defineComponent({ if (import.meta.server || nuxtApp.isHydrating) { // re-push head into active head instance - (nuxtApp.payload.data[`${props.name}_${hashId.value}`] as NuxtIslandResponse)?.head?.forEach((h) => { - head.push(h) - }) + const responseHead = (nuxtApp.payload.data[`${props.name}_${hashId.value}`] as NuxtIslandResponse)?.head + if (responseHead) { + head.push(responseHead) + } } return (_ctx: any, _cache: any) => { diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 4d41ca0d5e..3c5c34d5b2 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -78,7 +78,7 @@ export interface NuxtIslandContext { export interface NuxtIslandResponse { id?: string html: string - head: Head[] + head: Head props?: Record> components?: Record slots?: Record @@ -461,9 +461,24 @@ export default defineRenderHandler(async (event): Promise resolveUnrefHeadInput(h.input) as Head)), + head: islandHead, html: getServerComponentHTML(_rendered.html), components: getClientIslandResponse(ssrContext), slots: getSlotIslandResponse(ssrContext), diff --git a/test/basic.test.ts b/test/basic.test.ts index a0f440756d..303b7ce9f2 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -8,7 +8,7 @@ import { $fetch as _$fetch, createPage, fetch, isDev, setup, startServer, url, u import { $fetchComponent } from '@nuxt/test-utils/experimental' import { resolveUnrefHeadInput } from '@unhead/vue' -import { expectNoClientErrors, expectWithPolling, gotoPath, isRenderingJson, parseData, parsePayload, renderPage, resolveHead } from './utils' +import { expectNoClientErrors, expectWithPolling, gotoPath, isRenderingJson, parseData, parsePayload, renderPage } from './utils' import type { NuxtIslandResponse } from '#app' @@ -2145,15 +2145,15 @@ describe('component islands', () => { result.html = result.html.replace(/ data-island-uid="[^"]*"/g, '') if (isDev()) { - result.head = resolveHead(result.head).map(h => ({ - ...h, - link: h.link?.filter(l => typeof l.href !== 'string' || (!l.href.includes('_nuxt/components/islands/RouteComponent') && !l.href.includes('PureComponent') /* TODO: fix dev bug triggered by previous fetch of /islands */)), - })).filter(h => Object.values(h).some(h => !Array.isArray(h) || h.length)) + result.head.link = result.head.link?.filter(l => typeof l.href !== 'string' || (!l.href.includes('_nuxt/components/islands/RouteComponent') && !l.href.includes('PureComponent') /* TODO: fix dev bug triggered by previous fetch of /islands */)) } expect(result).toMatchInlineSnapshot(` { - "head": [], + "head": { + "link": [], + "style": [], + }, "html": "
    Route: /foo
         
", } @@ -2167,15 +2167,15 @@ describe('component islands', () => { }), })) if (isDev()) { - result.head = resolveHead(result.head).map(h => ({ - ...h, - link: h.link?.filter(l => typeof l.href !== 'string' || (!l.href.includes('_nuxt/components/islands/LongAsyncComponent') && !l.href.includes('PureComponent') /* TODO: fix dev bug triggered by previous fetch of /islands */)), - })).filter(h => Object.values(h).some(h => !Array.isArray(h) || h.length)) + result.head.link = result.head.link?.filter(l => typeof l.href !== 'string' || (!l.href.includes('_nuxt/components/islands/LongAsyncComponent') && !l.href.includes('PureComponent') /* TODO: fix dev bug triggered by previous fetch of /islands */)) } result.html = result.html.replaceAll(/ (data-island-uid|data-island-component)="([^"]*)"/g, '') expect(result).toMatchInlineSnapshot(` { - "head": [], + "head": { + "link": [], + "style": [], + }, "html": "
count is above 2
that was very long ...
3

hello world !!!

", "slots": { "default": { @@ -2225,10 +2225,7 @@ describe('component islands', () => { }), })) if (isDev()) { - result.head = result.head.map(h => ({ - ...h, - link: h.link?.filter(l => typeof l.href === 'string' && !l.href.includes('PureComponent') /* TODO: fix dev bug triggered by previous fetch of /islands */ && (!l.href.startsWith('_nuxt/components/islands/') || l.href.includes('AsyncServerComponent'))), - })).filter(h => Object.values(h).some(h => !Array.isArray(h) || h.length)) + result.head.link = result.head.link?.filter(l => typeof l.href === 'string' && !l.href.includes('PureComponent') /* TODO: fix dev bug triggered by previous fetch of /islands */ && (!l.href.startsWith('_nuxt/components/islands/') || l.href.includes('AsyncServerComponent'))) } result.props = {} result.components = {} @@ -2238,7 +2235,10 @@ describe('component islands', () => { expect(result).toMatchInlineSnapshot(` { "components": {}, - "head": [], + "head": { + "link": [], + "style": [], + }, "html": "
This is a .server (20ms) async component that was very long ...
2
Sugar Counter 12 x 1 = 12
", "props": {}, "slots": {}, @@ -2250,10 +2250,11 @@ describe('component islands', () => { it('render server component with selective client hydration', async () => { const result = await $fetch('/__nuxt_island/ServerWithClient') if (isDev()) { - result.head = resolveHead(result.head).map(h => ({ - ...h, - link: h.link?.filter(l => typeof l.href !== 'string' || (!l.href.includes('_nuxt/components/islands/LongAsyncComponent') && !l.href.includes('PureComponent') /* TODO: fix dev bug triggered by previous fetch of /islands */)), - })).filter(h => Object.values(h).some(h => !Array.isArray(h) || h.length)) + result.head.link = result.head.link?.filter(l => typeof l.href !== 'string' || (!l.href.includes('_nuxt/components/islands/LongAsyncComponent') && !l.href.includes('PureComponent') /* TODO: fix dev bug triggered by previous fetch of /islands */)) + + if (!result.head.link) { + delete result.head.link + } } const { components } = result result.components = {} @@ -2265,7 +2266,10 @@ describe('component islands', () => { expect(result).toMatchInlineSnapshot(` { "components": {}, - "head": [], + "head": { + "link": [], + "style": [], + }, "html": "
ServerWithClient.server.vue :

count: 0

This component should not be preloaded
a
b
c
This is not interactive
Sugar Counter 12 x 1 = 12
The component below is not a slot but declared as interactive
", "slots": {}, } @@ -2294,16 +2298,14 @@ describe('component islands', () => { if (isDev()) { const fixtureDir = normalize(fileURLToPath(new URL('./fixtures/basic', import.meta.url))) - for (const head of result.head) { - for (const key in head) { - if (key === 'link') { - head[key] = head[key]?.map((h) => { - if (h.href) { - h.href = resolveUnrefHeadInput(h.href).replace(fixtureDir, '/').replaceAll('//', '/') - } - return h - }) - } + for (const key in result.head) { + if (key === 'link') { + result.head[key] = result.head[key]?.map((h) => { + if (h.href) { + h.href = resolveUnrefHeadInput(h.href).replace(fixtureDir, '/').replaceAll('//', '/') + } + return h + }) } } } @@ -2311,35 +2313,30 @@ describe('component islands', () => { // TODO: fix rendering of styles in webpack if (!isDev() && !isWebpack) { expect(normaliseIslandResult(result).head).toMatchInlineSnapshot(` - [ - { - "style": [ - { - "innerHTML": "pre[data-v-xxxxx]{color:blue}", - }, - ], - }, - ] + { + "link": [], + "style": [ + { + "innerHTML": "pre[data-v-xxxxx]{color:blue}", + }, + ], + } `) } else if (isDev() && !isWebpack) { // TODO: resolve dev bug triggered by earlier fetch of /vueuse-head page // https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/runtime/nitro/renderer.ts#L139 - result.head = resolveHead(result.head).map(h => ({ - ...h, - link: h.link?.filter(l => typeof l.href !== 'string' || !l.href.includes('SharedComponent')), - })).filter(h => Object.values(h).some(h => !Array.isArray(h) || h.length)) + result.head.link = result.head.link?.filter(l => typeof l.href !== 'string' || !l.href.includes('SharedComponent')) expect(result.head).toMatchInlineSnapshot(` - [ - { - "link": [ - { - "href": "/_nuxt/components/islands/PureComponent.vue?vue&type=style&index=0&scoped=c0c0cf89&lang.css", - "rel": "stylesheet", - }, - ], - }, - ] + { + "link": [ + { + "href": "/_nuxt/components/islands/PureComponent.vue?vue&type=style&index=0&scoped=c0c0cf89&lang.css", + "rel": "stylesheet", + }, + ], + "style": [], + } `) } @@ -2684,24 +2681,19 @@ describe('Node.js compatibility for client-side', () => { }) function normaliseIslandResult (result: NuxtIslandResponse) { - return { - ...result, - head: result.head.map((h) => { - if (h.style) { - for (const style of h.style) { - if (typeof style !== 'string') { - if (style.innerHTML) { - style.innerHTML = (style.innerHTML as string).replace(/data-v-[a-z0-9]+/g, 'data-v-xxxxx') - } - if (style.key) { - style.key = style.key.replace(/-[a-z0-9]+$/i, '') - } - } + if (result.head.style) { + for (const style of result.head.style) { + if (typeof style !== 'string') { + if (style.innerHTML) { + style.innerHTML = (style.innerHTML as string).replace(/data-v-[a-z0-9]+/g, 'data-v-xxxxx') + } + if (style.key) { + style.key = style.key.replace(/-[a-z0-9]+$/i, '') } - return h } - }), + } } + return result } describe('import components', () => { diff --git a/test/utils.ts b/test/utils.ts index 49a8587be0..3e21fb0c27 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -5,8 +5,6 @@ import { parse } from 'devalue' import { reactive, ref, shallowReactive, shallowRef } from 'vue' import { createError } from 'h3' import { getBrowser, url, useTestContext } from '@nuxt/test-utils/e2e' -import type { Head } from '@unhead/vue' -import { resolveUnrefHeadInput } from '@unhead/vue' export const isRenderingJson = process.env.TEST_PAYLOAD !== 'js' @@ -128,7 +126,3 @@ export function parseData (html: string) { attrs: _attrs, } } - -export function resolveHead (head: Head[]) { - return head.map(i => resolveUnrefHeadInput(i) as Head) -}