Skip to content

Commit

Permalink
fix(nuxt): overwrite island payload instead of merging (#25299)
Browse files Browse the repository at this point in the history
  • Loading branch information
huang-julien committed Jan 19, 2024
1 parent 92ba515 commit a57b428
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 13 deletions.
27 changes: 15 additions & 12 deletions packages/nuxt/src/app/components/nuxt-island.ts
Expand Up @@ -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>('')
Expand All @@ -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
})
Expand All @@ -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
})
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) => {
Expand All @@ -250,20 +253,20 @@ 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)]
}))
}
}
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
Expand Down
106 changes: 105 additions & 1 deletion test/nuxt/nuxt-island.test.ts
@@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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()
})
})

0 comments on commit a57b428

Please sign in to comment.