From 88bc32d42ade89bf8bf222f02e4b9ba91e69a7f9 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sun, 25 Jun 2023 17:38:15 +0100 Subject: [PATCH] fix(nuxt): proxy headers to islands + returned prerender hints (#21740) --- .../nuxt/src/app/components/nuxt-island.ts | 20 +++-- .../nuxt/src/core/runtime/nitro/renderer.ts | 12 +-- test/basic.test.ts | 83 +++++++++++-------- .../components/ServerOnlyComponent.server.vue | 6 ++ 4 files changed, 74 insertions(+), 47 deletions(-) diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index e2e8c27bea..53b1d6f062 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -4,6 +4,8 @@ import { hash } from 'ohash' import { appendResponseHeader } from 'h3' import { useHead } from '@unhead/vue' import { randomUUID } from 'uncrypto' +import { withQuery } from 'ufo' + // eslint-disable-next-line import/no-restricted-paths import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer' import { getFragmentHTML, getSlotProps } from './utils' @@ -40,6 +42,7 @@ export default defineComponent({ const hashId = computed(() => hash([props.name, props.props, props.context])) const instance = getCurrentInstance()! const event = useRequestEvent() + const eventFetch = process.server ? event.fetch : globalThis.fetch const mounted = ref(false) onMounted(() => { mounted.value = true }) @@ -78,13 +81,18 @@ export default defineComponent({ appendResponseHeader(event, 'x-nitro-prerender', url) } // TODO: Validate response - const result = await $fetch(url, { - responseType: 'json', - params: { - ...props.context, - props: props.props ? JSON.stringify(props.props) : undefined + const r = await eventFetch(withQuery(url, { + ...props.context, + props: props.props ? JSON.stringify(props.props) : undefined + })) + const result = await r.json() as NuxtIslandResponse + // TODO: support passing on more headers + if (process.server && process.env.prerender) { + const hints = r.headers.get('x-nitro-prerender') + if (hints) { + appendResponseHeader(event, 'x-nitro-prerender', hints) } - }) + } nuxtApp.payload.data[key] = { __nuxt_island: { key, diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index ef50b5882f..5e694af843 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -367,7 +367,7 @@ export default defineRenderHandler(async (event): Promise | string[]) { } function renderPayloadResponse (ssrContext: NuxtSSRContext) { - return { + return { body: process.env.NUXT_JSON_PAYLOADS ? stringify(splitPayload(ssrContext).payload, ssrContext._payloadReducers) : `export default ${devalue(splitPayload(ssrContext).payload)}`, @@ -466,7 +466,7 @@ function renderPayloadResponse (ssrContext: NuxtSSRContext) { 'content-type': process.env.NUXT_JSON_PAYLOADS ? 'application/json;charset=utf-8' : 'text/javascript;charset=utf-8', 'x-powered-by': 'Nuxt' } - } + } satisfies RenderResponse } function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }) { diff --git a/test/basic.test.ts b/test/basic.test.ts index 4243f0d63f..73a8292ba9 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -1,9 +1,10 @@ +import { readdir } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { joinURL, withQuery } from 'ufo' import { isCI, isWindows } from 'std-env' -import { normalize } from 'pathe' -import { $fetch, createPage, fetch, isDev, setup, startServer, url } from '@nuxt/test-utils' +import { join, normalize } from 'pathe' +import { $fetch, createPage, fetch, isDev, setup, startServer, url, useTestContext } from '@nuxt/test-utils' import { $fetchComponent } from '@nuxt/test-utils/experimental' import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer' @@ -418,39 +419,6 @@ describe('pages', () => { await page.close() }) - it('/islands', async () => { - const page = await createPage('/islands') - await page.waitForLoadState('networkidle') - await page.locator('#increase-pure-component').click() - await page.waitForResponse(response => response.url().includes('/__nuxt_island/') && response.status() === 200) - await page.waitForLoadState('networkidle') - expect(await page.locator('#slot-in-server').first().innerHTML()).toContain('Slot with in .server component') - expect(await page.locator('#test-slot').first().innerHTML()).toContain('Slot with name test') - - // test fallback slot with v-for - expect(await page.locator('.fallback-slot-content').all()).toHaveLength(2) - // test islands update - expect(await page.locator('.box').innerHTML()).toContain('"number": 101,') - await page.locator('#update-server-components').click() - await Promise.all([ - page.waitForResponse(response => response.url().includes('/__nuxt_island/LongAsyncComponent') && response.status() === 200), - page.waitForResponse(response => response.url().includes('/__nuxt_island/AsyncServerComponent') && response.status() === 200) - ]) - await page.waitForLoadState('networkidle') - expect(await page.locator('#async-server-component-count').innerHTML()).toContain(('1')) - expect(await page.locator('#long-async-component-count').innerHTML()).toContain('1') - - // test islands slots interactivity - await page.locator('#first-sugar-counter button').click() - expect(await page.locator('#first-sugar-counter').innerHTML()).toContain('Sugar Counter 13') - - // test islands mounted client side with slot - await page.locator('#show-island').click() - expect(await page.locator('#island-mounted-client-side').innerHTML()).toContain('Interactive testing slot post SSR') - - await page.close() - }) - it('/legacy-async-data-fail', async () => { const response = await fetch('/legacy-async-data-fail').then(r => r.text()) expect(response).not.toContain('don\'t look at this') @@ -1246,6 +1214,51 @@ describe.skipIf(isDev() || isWebpack)('inlining component styles', () => { }) }) +describe('server components/islands', () => { + it('/islands', async () => { + const page = await createPage('/islands') + await page.waitForLoadState('networkidle') + await page.locator('#increase-pure-component').click() + await page.waitForResponse(response => response.url().includes('/__nuxt_island/') && response.status() === 200) + await page.waitForLoadState('networkidle') + expect(await page.locator('#slot-in-server').first().innerHTML()).toContain('Slot with in .server component') + expect(await page.locator('#test-slot').first().innerHTML()).toContain('Slot with name test') + + // test fallback slot with v-for + expect(await page.locator('.fallback-slot-content').all()).toHaveLength(2) + // test islands update + expect(await page.locator('.box').innerHTML()).toContain('"number": 101,') + await page.locator('#update-server-components').click() + await Promise.all([ + page.waitForResponse(response => response.url().includes('/__nuxt_island/LongAsyncComponent') && response.status() === 200), + page.waitForResponse(response => response.url().includes('/__nuxt_island/AsyncServerComponent') && response.status() === 200) + ]) + await page.waitForLoadState('networkidle') + expect(await page.locator('#async-server-component-count').innerHTML()).toContain(('1')) + expect(await page.locator('#long-async-component-count').innerHTML()).toContain('1') + + // test islands slots interactivity + await page.locator('#first-sugar-counter button').click() + expect(await page.locator('#first-sugar-counter').innerHTML()).toContain('Sugar Counter 13') + + // test islands mounted client side with slot + await page.locator('#show-island').click() + expect(await page.locator('#island-mounted-client-side').innerHTML()).toContain('Interactive testing slot post SSR') + + await page.close() + }) + + it.skipIf(isDev)('should allow server-only components to set prerender hints', async () => { + // @ts-expect-error ssssh! untyped secret property + const publicDir = useTestContext().nuxt._nitro.options.output.publicDir + expect(await readdir(join(publicDir, 'some', 'url', 'from', 'server-only', 'component')).catch(() => [])).toContain( + isRenderingJson + ? '_payload.json' + : '_payload.js' + ) + }) +}) + describe.skipIf(isDev() || isWindows || !isRenderingJson)('prefetching', () => { it('should prefetch components', async () => { await expectNoClientErrors('/prefetch/components') diff --git a/test/fixtures/basic/components/ServerOnlyComponent.server.vue b/test/fixtures/basic/components/ServerOnlyComponent.server.vue index e35fc7cb60..fc3dc80e47 100644 --- a/test/fixtures/basic/components/ServerOnlyComponent.server.vue +++ b/test/fixtures/basic/components/ServerOnlyComponent.server.vue @@ -4,6 +4,12 @@ + +