From b48053a078f76acd6776d195e31f34ea39e915c1 Mon Sep 17 00:00:00 2001 From: Julien Huang Date: Sat, 11 Nov 2023 20:24:08 +0100 Subject: [PATCH 1/9] feat: add useCache and setCache for nuxtislands --- .../nuxt/src/app/components/nuxt-island.ts | 22 +++++++++++++++++-- .../components/runtime/server-component.ts | 4 +++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index 40162a15e3..e2de8e5b28 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -44,6 +44,22 @@ export default defineComponent({ source: { type: String, default: () => undefined + }, + /** + * use the NuxtIslandResponse which has been cached if available + * @default true + */ + useCache: { + type: Boolean, + default: true + }, + /** + * allows to set the NuxtIslandResponse into the cache for future updates + * @default true + */ + setCache: { + type: Boolean, + default: true } }, async setup (props, { slots }) { @@ -130,7 +146,9 @@ export default defineComponent({ appendResponseHeader(event, 'x-nitro-prerender', hints) } } - setPayload(key, result) + if (import.meta.client && props.setCache) { + setPayload(key, result) + } return result } const key = ref(0) @@ -167,7 +185,7 @@ export default defineComponent({ } if (import.meta.client) { - watch(props, debounce(() => fetchComponent(), 100)) + watch(props, debounce(() => fetchComponent(!props.useCache), 100)) } if (import.meta.client && !nuxtApp.isHydrating && props.lazy) { diff --git a/packages/nuxt/src/components/runtime/server-component.ts b/packages/nuxt/src/components/runtime/server-component.ts index e1f75e6a59..5345cb2114 100644 --- a/packages/nuxt/src/components/runtime/server-component.ts +++ b/packages/nuxt/src/components/runtime/server-component.ts @@ -6,12 +6,14 @@ export const createServerComponent = (name: string) => { return defineComponent({ name, inheritAttrs: false, - props: { lazy: Boolean }, + props: { lazy: Boolean, useCache: { type: Boolean, default: true }, setCache: { type: Boolean, default: true } }, setup (props, { attrs, slots }) { return () => { return h(NuxtIsland, { name, lazy: props.lazy, + useCache: props.useCache, + setCache: props.setCache, props: attrs }, slots) } From f2643d87795495e1ede46a90bcd2467c497cc39a Mon Sep 17 00:00:00 2001 From: julien huang Date: Wed, 15 Nov 2023 00:10:41 +0100 Subject: [PATCH 2/9] test: add tests --- test/nuxt/nuxt-island.test.ts | 112 +++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/test/nuxt/nuxt-island.test.ts b/test/nuxt/nuxt-island.test.ts index 8d23fe2419..8c663389b3 100644 --- a/test/nuxt/nuxt-island.test.ts +++ b/test/nuxt/nuxt-island.test.ts @@ -1,9 +1,11 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi, afterEach, beforeEach } from 'vitest' import { h } from 'vue' import { mountSuspended } from 'nuxt-vitest/utils' import { createServerComponent } from '../../packages/nuxt/src/components/runtime/server-component' import { createSimpleRemoteIslandProvider } from '../fixtures/remote-provider' import NuxtIsland from '../../packages/nuxt/src/app/components/nuxt-island' +import { flushPromises } from '@vue/test-utils' +import { useNuxtApp } from '../../packages/nuxt/src/app' vi.mock('#build/nuxt.config.mjs', async (original) => { return { @@ -21,6 +23,10 @@ vi.mock('vue', async (original) => { } }) +beforeEach(() => { + vi.mocked(h).mockClear() +}) + describe('runtime server component', () => { it('expect no data-v- attrbutes #23051', () => { // @ts-expect-error mock @@ -65,4 +71,108 @@ describe('runtime server component', () => { await server.close() }) + + + describe('Cache control', () => { + beforeEach(() => { + let count = 0 + const ogFetch = fetch + const stubFetch = vi.fn((...args: Parameters) => { + const [url] = args + + if (typeof url === 'string' && url.startsWith('/__nuxt_island')) { + count++ + return { + id: '123', + html: `
${count}
`, + state: {}, + head: { + link: [], + style: [] + }, + json() { + return this + } + } + } + return ogFetch(...args) + }) + vi.stubGlobal('fetch', stubFetch) + }) + + afterEach(() => { + vi.mocked(fetch).mockRestore() + }) + + it('expect to not use cached payload', async () => { + const wrapper = await mountSuspended( + createServerComponent('CacheTest'), { + props: { + useCache: false, + props: { + test: 1 + } + } + }) + + expect(fetch).toHaveBeenCalledOnce() + expect(wrapper.html()).toMatchInlineSnapshot('"
1
"') + await wrapper.setProps({ + useCache: false, + props: { + test: 2 + } + }) + + await flushPromises() + expect(fetch).toHaveBeenCalledTimes(2) + expect(wrapper.html()).toMatchInlineSnapshot('"
2
"') + await wrapper.setProps({ + useCache: false, + props: { + test: 1 + } + }) + await flushPromises() + // NuxtIsland should fetch again, because the cache is not used + expect(fetch).toHaveBeenCalledTimes(3) + expect(wrapper.html()).toMatchInlineSnapshot('"
3
"') + }) + + it('expect to use cached payload', async () => { + useNuxtApp().payload.data = {} + const wrapper = await mountSuspended( + createServerComponent('CacheTest'), { + props: { + useCache: true, + props: { + test: 1 + } + } + }) + + expect(fetch).toHaveBeenCalledOnce() + expect(wrapper.html()).toMatchInlineSnapshot('"
1
"') + await wrapper.setProps({ + useCache: true, + props: { + test: 2 + } + }) + + await flushPromises() + expect(fetch).toHaveBeenCalledTimes(2) + expect(wrapper.html()).toMatchInlineSnapshot('"
2
"') + await wrapper.setProps({ + useCache: true, + props: { + test: 2 + } + }) + await flushPromises() + // should not fetch the component, because the cache is used + expect(fetch).toHaveBeenCalledTimes(2) + expect(wrapper.html()).toMatchInlineSnapshot('"
2
"') + }) + }) }) From c94afe4fa0d2442b48072dff6a3f27699fdcc322 Mon Sep 17 00:00:00 2001 From: julien huang Date: Wed, 15 Nov 2023 00:16:20 +0100 Subject: [PATCH 3/9] docs: update doc --- docs/3.api/1.components/8.nuxt-island.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/3.api/1.components/8.nuxt-island.md b/docs/3.api/1.components/8.nuxt-island.md index 47a945e92b..9dd1f2f65d 100644 --- a/docs/3.api/1.components/8.nuxt-island.md +++ b/docs/3.api/1.components/8.nuxt-island.md @@ -36,6 +36,12 @@ Server only components use `` under the hood - **type**: `Record` - `source`: Remote source to call the island to render. - **type**: `string` +- `useCache`: Use the cached payload if available + - **type**: `boolean` + - **default**: `true` +- `setCache`: Cache the `NuxtIslandResponse` for reuse with `useCache` prop. Note that this is always cached in SSR. + - **type**: `boolean` + - **default**: `true` ::callout{color="blue" icon="i-ph-info-duotone"} Remote islands need `experimental.componentIslands` to be `'local+remote'` in your `nuxt.config`. From 0214f703fa29694b087924228d1971a544a86e3c Mon Sep 17 00:00:00 2001 From: julien huang Date: Wed, 15 Nov 2023 22:55:28 +0100 Subject: [PATCH 4/9] test: test setCache --- test/nuxt/nuxt-island.test.ts | 61 ++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/test/nuxt/nuxt-island.test.ts b/test/nuxt/nuxt-island.test.ts index 8c663389b3..7bd50edee3 100644 --- a/test/nuxt/nuxt-island.test.ts +++ b/test/nuxt/nuxt-island.test.ts @@ -98,6 +98,8 @@ describe('runtime server component', () => { return ogFetch(...args) }) vi.stubGlobal('fetch', stubFetch) + + useNuxtApp().payload.data = {} }) afterEach(() => { @@ -140,7 +142,6 @@ describe('runtime server component', () => { }) it('expect to use cached payload', async () => { - useNuxtApp().payload.data = {} const wrapper = await mountSuspended( createServerComponent('CacheTest'), { props: { @@ -174,5 +175,63 @@ describe('runtime server component', () => { expect(fetch).toHaveBeenCalledTimes(2) expect(wrapper.html()).toMatchInlineSnapshot('"
2
"') }) + + it('expect server component to set the response into the payload', async () => { + const wrapper = await mountSuspended( + createServerComponent('CacheTest'), { + props: { + useCache: false, + setCache: true, + props: { + test: 1 + } + } + }) + + expect(fetch).toHaveBeenCalledOnce() + expect(wrapper.html()).toMatchInlineSnapshot('"
1
"') + + expect(Object.keys(useNuxtApp().payload.data).length).toBeGreaterThan(0) + }) + + it('expect server component to NOT set the response into the payload', async () => { + const wrapper = await mountSuspended( + createServerComponent('CacheTest'), { + props: { + useCache: false, + setCache: false, + props: { + test: 1 + } + } + }) + + expect(fetch).toHaveBeenCalledOnce() + expect(wrapper.html()).toMatchInlineSnapshot('"
1
"') + expect(Object.keys(useNuxtApp().payload.data).length).toBe(0) + await wrapper.setProps({ + useCache: false, + setCache: false, + props: { + test: 2 + } + }) + + await flushPromises() + expect(fetch).toHaveBeenCalledTimes(2) + expect(wrapper.html()).toMatchInlineSnapshot('"
2
"') + expect(Object.keys(useNuxtApp().payload.data).length).toBe(0) + await wrapper.setProps({ + useCache: false, + setCache: false, + props: { + test: 2 + } + }) + await flushPromises() + expect(fetch).toHaveBeenCalledTimes(3) + expect(wrapper.html()).toMatchInlineSnapshot('"
3
"') + expect(Object.keys(useNuxtApp().payload.data).length).toBe(0) + }) }) }) From 4dad40c12771cec3e1cee2c58ab75b2b6709d266 Mon Sep 17 00:00:00 2001 From: julien huang Date: Wed, 15 Nov 2023 22:56:04 +0100 Subject: [PATCH 5/9] test: refactor --- test/nuxt/nuxt-island.test.ts | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/test/nuxt/nuxt-island.test.ts b/test/nuxt/nuxt-island.test.ts index 7bd50edee3..646716f3d7 100644 --- a/test/nuxt/nuxt-island.test.ts +++ b/test/nuxt/nuxt-island.test.ts @@ -191,7 +191,7 @@ describe('runtime server component', () => { expect(fetch).toHaveBeenCalledOnce() expect(wrapper.html()).toMatchInlineSnapshot('"
1
"') - expect(Object.keys(useNuxtApp().payload.data).length).toBeGreaterThan(0) + expect(Object.keys(useNuxtApp().payload.data).length).toBe(1) }) it('expect server component to NOT set the response into the payload', async () => { @@ -208,30 +208,7 @@ describe('runtime server component', () => { expect(fetch).toHaveBeenCalledOnce() expect(wrapper.html()).toMatchInlineSnapshot('"
1
"') - expect(Object.keys(useNuxtApp().payload.data).length).toBe(0) - await wrapper.setProps({ - useCache: false, - setCache: false, - props: { - test: 2 - } - }) - - await flushPromises() - expect(fetch).toHaveBeenCalledTimes(2) - expect(wrapper.html()).toMatchInlineSnapshot('"
2
"') - expect(Object.keys(useNuxtApp().payload.data).length).toBe(0) - await wrapper.setProps({ - useCache: false, - setCache: false, - props: { - test: 2 - } - }) - await flushPromises() - expect(fetch).toHaveBeenCalledTimes(3) - expect(wrapper.html()).toMatchInlineSnapshot('"
3
"') - expect(Object.keys(useNuxtApp().payload.data).length).toBe(0) + expect(Object.keys(useNuxtApp().payload.data).length).toBe(0) }) }) }) From acc23a0b72bce7a56af44e66e4453321c16ca3db Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 23:07:29 +0000 Subject: [PATCH 6/9] [autofix.ci] apply automated fixes --- test/nuxt/nuxt-island.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/nuxt/nuxt-island.test.ts b/test/nuxt/nuxt-island.test.ts index 646716f3d7..644d46eab1 100644 --- a/test/nuxt/nuxt-island.test.ts +++ b/test/nuxt/nuxt-island.test.ts @@ -1,10 +1,10 @@ -import { describe, expect, it, vi, afterEach, beforeEach } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { h } from 'vue' import { mountSuspended } from 'nuxt-vitest/utils' +import { flushPromises } from '@vue/test-utils' import { createServerComponent } from '../../packages/nuxt/src/components/runtime/server-component' import { createSimpleRemoteIslandProvider } from '../fixtures/remote-provider' import NuxtIsland from '../../packages/nuxt/src/app/components/nuxt-island' -import { flushPromises } from '@vue/test-utils' import { useNuxtApp } from '../../packages/nuxt/src/app' vi.mock('#build/nuxt.config.mjs', async (original) => { From 211900eb3109cbb10093679d0e6291880153f130 Mon Sep 17 00:00:00 2001 From: Julien Huang Date: Thu, 13 Jun 2024 23:39:16 +0200 Subject: [PATCH 7/9] fix: fix on page change --- .../nuxt/src/app/components/nuxt-island.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index bb3a7fb840..a7cb43cfbc 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -67,21 +67,22 @@ export default defineComponent({ type: String, default: () => undefined, }, + dangerouslyLoadClientComponents: { + type: Boolean, + default: false, + }, /** * use the NuxtIslandResponse which has been cached if available * @default true */ useCache: { type: Boolean, - default: true - }, - dangerouslyLoadClientComponents: { - type: Boolean, - default: false, + default: true, }, }, emits: ['error'], async setup (props, { slots, expose, emit }) { + console.log(props.useCache) let canTeleport = import.meta.server const teleportKey = ref(0) const key = ref(0) @@ -194,9 +195,7 @@ export default defineComponent({ appendResponseHeader(event!, 'x-nitro-prerender', hints) } } - if (import.meta.client) { - setPayload(key, result) - } + setPayload(key, result) return result } @@ -251,9 +250,9 @@ export default defineComponent({ } if (import.meta.client && !instance.vnode.el && props.lazy) { - fetchComponent() + fetchComponent(!instance.vnode.el && !props.useCache) } else if (import.meta.server || !instance.vnode.el || !nuxtApp.payload.serverRendered) { - await fetchComponent() + await fetchComponent(!instance.vnode.el && !props.useCache) } else if (selectiveClient && canLoadClientComponent.value) { await loadComponents(props.source, payloads.components) } From 5dfc163547f82e07617c75e9a5f53bd1c6e638df Mon Sep 17 00:00:00 2001 From: Julien Huang Date: Thu, 13 Jun 2024 23:39:51 +0200 Subject: [PATCH 8/9] chore: remove log --- packages/nuxt/src/app/components/nuxt-island.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index a7cb43cfbc..d8d1c748d6 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -82,7 +82,6 @@ export default defineComponent({ }, emits: ['error'], async setup (props, { slots, expose, emit }) { - console.log(props.useCache) let canTeleport = import.meta.server const teleportKey = ref(0) const key = ref(0) From 0efca895bac3fbba234afe8c4fe5be444c3acdfd Mon Sep 17 00:00:00 2001 From: Julien Huang Date: Fri, 14 Jun 2024 00:21:40 +0200 Subject: [PATCH 9/9] test: add test --- test/nuxt/nuxt-island.test.ts | 81 ++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/test/nuxt/nuxt-island.test.ts b/test/nuxt/nuxt-island.test.ts index a12123884f..3d761567f8 100644 --- a/test/nuxt/nuxt-island.test.ts +++ b/test/nuxt/nuxt-island.test.ts @@ -1,5 +1,4 @@ -import { beforeEach } from 'node:test' -import { describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { defineComponent, h, nextTick, popScopeId, pushScopeId } from 'vue' import { mountSuspended } from '@nuxt/test-utils/runtime' import { createServerComponent } from '../../packages/nuxt/src/components/runtime/server-component' @@ -34,6 +33,11 @@ function expectNoConsoleIssue () { beforeEach(() => { consoleError.mockClear() consoleWarn.mockClear() + useNuxtApp().payload.data = {} +}) + +afterEach(() => { + if (vi.isMockFunction(fetch)) { vi.mocked(fetch).mockReset() } }) describe('runtime server component', () => { @@ -135,6 +139,22 @@ describe('runtime server component', () => { }) it('expect NuxtIsland to have parent scopeId', async () => { + const stubFetch = vi.fn(() => { + return { + id: '1234', + html: `
hello
`, + head: { + link: [], + style: [], + }, + json () { + return this + }, + } + }) + + vi.stubGlobal('fetch', stubFetch) + const wrapper = await mountSuspended(defineComponent({ render () { pushScopeId('data-v-654e2b21') @@ -330,6 +350,63 @@ describe('client components', () => { " `) + vi.mocked(fetch).mockReset() expectNoConsoleIssue() }) }) + +describe('reuse paylaod', () => { + let count = 0 + + const stubFetch = () => { + count++ + return { + id: '123', + html: `
${count.toString()}
`, + state: {}, + head: { + link: [], + style: [], + }, + json () { + return this + }, + } + } + + beforeEach(() => { + count = 0 + vi.mocked(fetch).mockReset() + vi.stubGlobal('fetch', vi.fn(stubFetch)) + }) + it('expect payload to be reused', async () => { + const component1 = await mountSuspended(createServerComponent('reuseCache')) + expect(fetch).toHaveBeenCalledOnce() + expect(component1.html()).toBe('
1
') + await component1.unmount() + expect(fetch).toHaveBeenCalledOnce() + const component2 = await mountSuspended(createServerComponent('reuseCache')) + expect(component2.html()).toBe('
1
') + }) + it('expect to re-fetch the island', async () => { + const component = await mountSuspended(createServerComponent('withoutCache'), { + props: { + useCache: false, + onError (e) { + console.log(e) + }, + }, + }) + await nextTick() + expect(fetch).toHaveBeenCalledOnce() + expect(component.html()).toBe('
1
') + await component.unmount() + const component2 = await mountSuspended(createServerComponent('withoutCache'), { + props: { + useCache: false, + }, + }) + expect(fetch).toHaveBeenCalledTimes(2) + expect(component2.html()).toBe('
2
') + }) +})