fix(nuxt): revert back to object syntax for island head (#28656)

This commit is contained in:
Daniel Roe 2024-08-22 14:57:10 +01:00 committed by GitHub
parent f6d8ee33c1
commit 0b3e2b18c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 82 additions and 80 deletions

View File

@ -248,9 +248,10 @@ export default defineComponent({
if (import.meta.server || nuxtApp.isHydrating) { if (import.meta.server || nuxtApp.isHydrating) {
// re-push head into active head instance // re-push head into active head instance
(nuxtApp.payload.data[`${props.name}_${hashId.value}`] as NuxtIslandResponse)?.head?.forEach((h) => { const responseHead = (nuxtApp.payload.data[`${props.name}_${hashId.value}`] as NuxtIslandResponse)?.head
head.push(h) if (responseHead) {
}) head.push(responseHead)
}
} }
return (_ctx: any, _cache: any) => { return (_ctx: any, _cache: any) => {

View File

@ -78,7 +78,7 @@ export interface NuxtIslandContext {
export interface NuxtIslandResponse { export interface NuxtIslandResponse {
id?: string id?: string
html: string html: string
head: Head[] head: Head
props?: Record<string, Record<string, any>> props?: Record<string, Record<string, any>>
components?: Record<string, NuxtIslandClientResponse> components?: Record<string, NuxtIslandClientResponse>
slots?: Record<string, NuxtIslandSlotResponse> slots?: Record<string, NuxtIslandSlotResponse>
@ -461,9 +461,24 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Response for component islands // Response for component islands
if (isRenderingIsland && islandContext) { if (isRenderingIsland && islandContext) {
const islandHead: Head = {}
for (const entry of head.headEntries()) {
for (const [key, value] of Object.entries(resolveUnrefHeadInput(entry.input) as Head)) {
const currentValue = islandHead[key as keyof Head]
if (Array.isArray(currentValue)) {
currentValue.push(...value)
}
islandHead[key as keyof Head] = value
}
}
// TODO: remove for v4
islandHead.link = islandHead.link || []
islandHead.style = islandHead.style || []
const islandResponse: NuxtIslandResponse = { const islandResponse: NuxtIslandResponse = {
id: islandContext.id, id: islandContext.id,
head: (head.headEntries().map(h => resolveUnrefHeadInput(h.input) as Head)), head: islandHead,
html: getServerComponentHTML(_rendered.html), html: getServerComponentHTML(_rendered.html),
components: getClientIslandResponse(ssrContext), components: getClientIslandResponse(ssrContext),
slots: getSlotIslandResponse(ssrContext), slots: getSlotIslandResponse(ssrContext),

View File

@ -8,7 +8,7 @@ import { $fetch as _$fetch, createPage, fetch, isDev, setup, startServer, url, u
import { $fetchComponent } from '@nuxt/test-utils/experimental' import { $fetchComponent } from '@nuxt/test-utils/experimental'
import { resolveUnrefHeadInput } from '@unhead/vue' 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' import type { NuxtIslandResponse } from '#app'
@ -2145,15 +2145,15 @@ describe('component islands', () => {
result.html = result.html.replace(/ data-island-uid="[^"]*"/g, '') result.html = result.html.replace(/ data-island-uid="[^"]*"/g, '')
if (isDev()) { if (isDev()) {
result.head = resolveHead(result.head).map(h => ({ 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 */))
...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))
} }
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
{ {
"head": [], "head": {
"link": [],
"style": [],
},
"html": "<pre data-island-uid> Route: /foo "html": "<pre data-island-uid> Route: /foo
</pre>", </pre>",
} }
@ -2167,15 +2167,15 @@ describe('component islands', () => {
}), }),
})) }))
if (isDev()) { if (isDev()) {
result.head = resolveHead(result.head).map(h => ({ 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 */))
...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.html = result.html.replaceAll(/ (data-island-uid|data-island-component)="([^"]*)"/g, '') result.html = result.html.replaceAll(/ (data-island-uid|data-island-component)="([^"]*)"/g, '')
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
{ {
"head": [], "head": {
"link": [],
"style": [],
},
"html": "<div data-island-uid><div> count is above 2 </div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--> that was very long ... <div id="long-async-component-count">3</div> <!--[--><div style="display: contents;" data-island-uid data-island-slot="test"><!--teleport start--><!--teleport end--></div><!--]--><p>hello world !!!</p><!--[--><div style="display: contents;" data-island-uid data-island-slot="hello"><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--><!--[--><div style="display: contents;" data-island-uid data-island-slot="fallback"><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--></div>", "html": "<div data-island-uid><div> count is above 2 </div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--> that was very long ... <div id="long-async-component-count">3</div> <!--[--><div style="display: contents;" data-island-uid data-island-slot="test"><!--teleport start--><!--teleport end--></div><!--]--><p>hello world !!!</p><!--[--><div style="display: contents;" data-island-uid data-island-slot="hello"><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--><!--[--><div style="display: contents;" data-island-uid data-island-slot="fallback"><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--></div>",
"slots": { "slots": {
"default": { "default": {
@ -2225,10 +2225,7 @@ describe('component islands', () => {
}), }),
})) }))
if (isDev()) { if (isDev()) {
result.head = result.head.map(h => ({ 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')))
...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.props = {} result.props = {}
result.components = {} result.components = {}
@ -2238,7 +2235,10 @@ describe('component islands', () => {
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
{ {
"components": {}, "components": {},
"head": [], "head": {
"link": [],
"style": [],
},
"html": "<div data-island-uid> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">2</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div>", "html": "<div data-island-uid> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">2</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div>",
"props": {}, "props": {},
"slots": {}, "slots": {},
@ -2250,10 +2250,11 @@ describe('component islands', () => {
it('render server component with selective client hydration', async () => { it('render server component with selective client hydration', async () => {
const result = await $fetch<NuxtIslandResponse>('/__nuxt_island/ServerWithClient') const result = await $fetch<NuxtIslandResponse>('/__nuxt_island/ServerWithClient')
if (isDev()) { if (isDev()) {
result.head = resolveHead(result.head).map(h => ({ 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 */))
...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 */)), if (!result.head.link) {
})).filter(h => Object.values(h).some(h => !Array.isArray(h) || h.length)) delete result.head.link
}
} }
const { components } = result const { components } = result
result.components = {} result.components = {}
@ -2265,7 +2266,10 @@ describe('component islands', () => {
expect(result).toMatchInlineSnapshot(` expect(result).toMatchInlineSnapshot(`
{ {
"components": {}, "components": {},
"head": [], "head": {
"link": [],
"style": [],
},
"html": "<div data-island-uid> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component below is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-uid data-island-component="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>", "html": "<div data-island-uid> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component below is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-uid data-island-component="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>",
"slots": {}, "slots": {},
} }
@ -2294,16 +2298,14 @@ describe('component islands', () => {
if (isDev()) { if (isDev()) {
const fixtureDir = normalize(fileURLToPath(new URL('./fixtures/basic', import.meta.url))) const fixtureDir = normalize(fileURLToPath(new URL('./fixtures/basic', import.meta.url)))
for (const head of result.head) { for (const key in result.head) {
for (const key in head) { if (key === 'link') {
if (key === 'link') { result.head[key] = result.head[key]?.map((h) => {
head[key] = head[key]?.map((h) => { if (h.href) {
if (h.href) { h.href = resolveUnrefHeadInput(h.href).replace(fixtureDir, '/<rootDir>').replaceAll('//', '/')
h.href = resolveUnrefHeadInput(h.href).replace(fixtureDir, '/<rootDir>').replaceAll('//', '/') }
} return h
return h })
})
}
} }
} }
} }
@ -2311,35 +2313,30 @@ describe('component islands', () => {
// TODO: fix rendering of styles in webpack // TODO: fix rendering of styles in webpack
if (!isDev() && !isWebpack) { if (!isDev() && !isWebpack) {
expect(normaliseIslandResult(result).head).toMatchInlineSnapshot(` expect(normaliseIslandResult(result).head).toMatchInlineSnapshot(`
[ {
{ "link": [],
"style": [ "style": [
{ {
"innerHTML": "pre[data-v-xxxxx]{color:blue}", "innerHTML": "pre[data-v-xxxxx]{color:blue}",
}, },
], ],
}, }
]
`) `)
} else if (isDev() && !isWebpack) { } else if (isDev() && !isWebpack) {
// TODO: resolve dev bug triggered by earlier fetch of /vueuse-head page // 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 // https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/runtime/nitro/renderer.ts#L139
result.head = resolveHead(result.head).map(h => ({ result.head.link = result.head.link?.filter(l => typeof l.href !== 'string' || !l.href.includes('SharedComponent'))
...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))
expect(result.head).toMatchInlineSnapshot(` expect(result.head).toMatchInlineSnapshot(`
[ {
{ "link": [
"link": [ {
{ "href": "/_nuxt/components/islands/PureComponent.vue?vue&type=style&index=0&scoped=c0c0cf89&lang.css",
"href": "/_nuxt/components/islands/PureComponent.vue?vue&type=style&index=0&scoped=c0c0cf89&lang.css", "rel": "stylesheet",
"rel": "stylesheet", },
}, ],
], "style": [],
}, }
]
`) `)
} }
@ -2684,24 +2681,19 @@ describe('Node.js compatibility for client-side', () => {
}) })
function normaliseIslandResult (result: NuxtIslandResponse) { function normaliseIslandResult (result: NuxtIslandResponse) {
return { if (result.head.style) {
...result, for (const style of result.head.style) {
head: result.head.map((h) => { if (typeof style !== 'string') {
if (h.style) { if (style.innerHTML) {
for (const style of h.style) { style.innerHTML = (style.innerHTML as string).replace(/data-v-[a-z0-9]+/g, 'data-v-xxxxx')
if (typeof style !== 'string') { }
if (style.innerHTML) { if (style.key) {
style.innerHTML = (style.innerHTML as string).replace(/data-v-[a-z0-9]+/g, 'data-v-xxxxx') style.key = style.key.replace(/-[a-z0-9]+$/i, '')
}
if (style.key) {
style.key = style.key.replace(/-[a-z0-9]+$/i, '')
}
}
} }
return h
} }
}), }
} }
return result
} }
describe('import components', () => { describe('import components', () => {

View File

@ -5,8 +5,6 @@ import { parse } from 'devalue'
import { reactive, ref, shallowReactive, shallowRef } from 'vue' import { reactive, ref, shallowReactive, shallowRef } from 'vue'
import { createError } from 'h3' import { createError } from 'h3'
import { getBrowser, url, useTestContext } from '@nuxt/test-utils/e2e' 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' export const isRenderingJson = process.env.TEST_PAYLOAD !== 'js'
@ -128,7 +126,3 @@ export function parseData (html: string) {
attrs: _attrs, attrs: _attrs,
} }
} }
export function resolveHead (head: Head[]) {
return head.map(i => resolveUnrefHeadInput(i) as Head)
}