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
parent 889b6a891c
commit a0124712f3
No known key found for this signature in database
GPG Key ID: 3714AB03996F442B
4 changed files with 82 additions and 80 deletions

View File

@ -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) => {

View File

@ -79,7 +79,7 @@ export interface NuxtIslandContext {
export interface NuxtIslandResponse {
id?: string
html: string
head: Head[]
head: Head
props?: Record<string, Record<string, any>>
components?: Record<string, NuxtIslandClientResponse>
slots?: Record<string, NuxtIslandSlotResponse>
@ -479,9 +479,24 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Response for component islands
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 = {
id: islandContext.id,
head: (head.headEntries().map(h => resolveUnrefHeadInput(h.input) as Head)),
head: islandHead,
html: getServerComponentHTML(htmlContext.body),
components: getClientIslandResponse(ssrContext),
slots: getSlotIslandResponse(ssrContext),

View File

@ -8,7 +8,7 @@ import { $fetch, createPage, fetch, isDev, setup, startServer, url, useTestConte
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'
@ -2148,15 +2148,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": "<pre data-island-uid> Route: /foo
</pre>",
}
@ -2170,15 +2170,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": "<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": {
"default": {
@ -2228,10 +2228,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 = {}
@ -2241,7 +2238,10 @@ describe('component islands', () => {
expect(result).toMatchInlineSnapshot(`
{
"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>",
"props": {},
"slots": {},
@ -2253,10 +2253,11 @@ describe('component islands', () => {
it('render server component with selective client hydration', async () => {
const result = await $fetch<NuxtIslandResponse>('/__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 = {}
@ -2268,7 +2269,10 @@ describe('component islands', () => {
expect(result).toMatchInlineSnapshot(`
{
"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>",
"slots": {},
}
@ -2297,16 +2301,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, '/<rootDir>').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, '/<rootDir>').replaceAll('//', '/')
}
return h
})
}
}
}
@ -2314,35 +2316,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": [],
}
`)
}
@ -2687,24 +2684,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', () => {

View File

@ -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)
}