diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index 9457099918..fbf424777f 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -104,12 +104,15 @@ export default defineComponent({ } } - const payloadSlots: NonNullable = {} - const payloadComponents: NonNullable = {} + const payloads: Required> = { + 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('') @@ -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 diff --git a/test/nuxt/nuxt-island.test.ts b/test/nuxt/nuxt-island.test.ts index fc73407ccb..7640c91f37 100644 --- a/test/nuxt/nuxt-island.test.ts +++ b/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' @@ -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('
2
') - 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: `
hello
`, + state: {}, + head: { + link: [], + style: [] + }, + components: { + [componentId]: { + html: '
fallback
', + 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(` + "
hello
+
client component
+
+
+ + " + `) + + // @ts-expect-error mock + vi.mocked(fetch).mockImplementation(() => ({ + id: '123', + html: `
hello
fallback
`, + state: {}, + head: { + link: [], + style: [] + }, + components: {}, + json() { + return this + } + })) + + await wrapper.vm.$.exposed!.refresh() + await nextTick() + expect(wrapper.html()).toMatchInlineSnapshot( ` + "
hello
+
fallback
+
+
" + `) + + vi.mocked(fetch).mockReset() + expectNoConsoleIssue() + }) +}) \ No newline at end of file