fix(nuxt): overwrite island payload instead of merging (#25299)

This commit is contained in:
Julien Huang 2024-01-19 13:21:42 +01:00 committed by GitHub
parent 92ba515549
commit a57b428587
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 120 additions and 13 deletions

View File

@ -104,12 +104,15 @@ export default defineComponent({
}
}
const payloadSlots: NonNullable<NuxtIslandResponse['slots']> = {}
const payloadComponents: NonNullable<NuxtIslandResponse['components']> = {}
const payloads: Required<Pick<NuxtIslandResponse, 'slots' | 'components'>> = {
slots: {},
components: {}
}
if (nuxtApp.isHydrating) {
Object.assign(payloadSlots, toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.slots ?? {})
Object.assign(payloadComponents, toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.components ?? {})
payloads.slots = toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.slots ?? {}
payloads.components = toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.components ?? {}
}
const ssrHTML = ref<string>('')
@ -125,7 +128,7 @@ export default defineComponent({
let html = ssrHTML.value
if (import.meta.client && !canLoadClientComponent.value) {
for (const [key, value] of Object.entries(payloadComponents || {})) {
for (const [key, value] of Object.entries(payloads.components || {})) {
html = html.replace(new RegExp(` data-island-uid="${uid.value}" data-island-component="${key}"[^>]*>`), (full) => {
return full + value.html
})
@ -134,7 +137,7 @@ export default defineComponent({
return html.replaceAll(SLOT_FALLBACK_RE, (full, slotName) => {
if (!currentSlots.includes(slotName)) {
return full + (payloadSlots[slotName]?.fallback || '')
return full + (payloads.slots[slotName]?.fallback || '')
}
return full
})
@ -186,8 +189,8 @@ export default defineComponent({
ssrHTML.value = res.html.replaceAll(DATA_ISLAND_UID_RE, `data-island-uid="${uid.value}"`)
key.value++
error.value = null
Object.assign(payloadSlots, res.slots || {})
Object.assign(payloadComponents, res.components || {})
payloads.slots = res.slots || {}
payloads.components = res.components || {}
if (selectiveClient && import.meta.client) {
if (canLoadClientComponent.value && res.components) {
@ -226,7 +229,7 @@ export default defineComponent({
} else if (import.meta.server || !nuxtApp.isHydrating || !nuxtApp.payload.serverRendered) {
await fetchComponent()
} else if (selectiveClient && canLoadClientComponent.value) {
await loadComponents(props.source, payloadComponents)
await loadComponents(props.source, payloads.components)
}
return (_ctx: any, _cache: any) => {
@ -250,12 +253,12 @@ export default defineComponent({
teleports.push(createVNode(Teleport,
// use different selectors for even and odd teleportKey to force trigger the teleport
{ to: import.meta.client ? `${isKeyOdd ? 'div' : ''}[data-island-uid="${uid.value}"][data-island-slot="${slot}"]` : `uid=${uid.value};slot=${slot}` },
{ default: () => (payloadSlots[slot].props?.length ? payloadSlots[slot].props : [{}]).map((data: any) => slots[slot]?.(data)) })
{ default: () => (payloads.slots[slot].props?.length ? payloads.slots[slot].props : [{}]).map((data: any) => slots[slot]?.(data)) })
)
}
}
if (import.meta.server) {
for (const [id, info] of Object.entries(payloadComponents ?? {})) {
for (const [id, info] of Object.entries(payloads.components ?? {})) {
const { html } = info
teleports.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, {
default: () => [createStaticVNode(html, 1)]
@ -263,7 +266,7 @@ export default defineComponent({
}
}
if (selectiveClient && import.meta.client && canLoadClientComponent.value) {
for (const [id, info] of Object.entries(payloadComponents ?? {})) {
for (const [id, info] of Object.entries(payloads.components ?? {})) {
const { props } = info
const component = components!.get(id)!
// use different selectors for even and odd teleportKey to force trigger the teleport

View File

@ -1,3 +1,4 @@
import { beforeEach } from 'node:test'
import { describe, expect, it, vi } from 'vitest'
import { h, nextTick } from 'vue'
import { mountSuspended } from '@nuxt/test-utils/runtime'
@ -22,6 +23,19 @@ vi.mock('vue', async (original) => {
}
})
const consoleError = vi.spyOn(console, 'error')
const consoleWarn = vi.spyOn(console, 'warn')
function expectNoConsoleIssue() {
expect(consoleError).not.toHaveBeenCalled()
expect(consoleWarn).not.toHaveBeenCalled()
}
beforeEach(() => {
consoleError.mockClear()
consoleWarn.mockClear()
})
describe('runtime server component', () => {
it('expect no data-v- attrbutes #23051', () => {
// @ts-expect-error mock
@ -95,6 +109,96 @@ describe('runtime server component', () => {
expect(fetch).toHaveBeenCalledTimes(2)
await nextTick()
expect(component.html()).toBe('<div>2</div>')
vi.mocked(fetch).mockRestore()
vi.mocked(fetch).mockReset()
})
})
describe('client components', () => {
it('expect swapping nuxt-client should not trigger errors #25289', async () => {
const mockPath = '/nuxt-client.js'
const componentId = 'Client-12345'
vi.doMock(mockPath, () => ({
default: {
name: 'ClientComponent',
setup() {
return () => h('div', 'client component')
}
}
}))
const stubFetch = vi.fn(() => {
return {
id: '123',
html: `<div data-island-uid>hello<div data-island-uid data-island-component="${componentId}"></div></div>`,
state: {},
head: {
link: [],
style: []
},
components: {
[componentId]: {
html: '<div>fallback</div>',
props: {},
chunk: mockPath
}
},
json() {
return this
}
}
})
vi.stubGlobal('fetch', stubFetch)
const wrapper = await mountSuspended(NuxtIsland, {
props: {
name: 'NuxtClient',
props: {
force: true
}
},
attachTo: 'body'
})
expect(fetch).toHaveBeenCalledOnce()
expect(wrapper.html()).toMatchInlineSnapshot(`
"<div data-island-uid="3">hello<div data-island-uid="3" data-island-component="Client-12345">
<div>client component</div>
</div>
</div>
<!--teleport start-->
<!--teleport end-->"
`)
// @ts-expect-error mock
vi.mocked(fetch).mockImplementation(() => ({
id: '123',
html: `<div data-island-uid>hello<div><div>fallback</div></div></div>`,
state: {},
head: {
link: [],
style: []
},
components: {},
json() {
return this
}
}))
await wrapper.vm.$.exposed!.refresh()
await nextTick()
expect(wrapper.html()).toMatchInlineSnapshot( `
"<div data-island-uid="3">hello<div>
<div>fallback</div>
</div>
</div>"
`)
vi.mocked(fetch).mockReset()
expectNoConsoleIssue()
})
})