fix(nuxt): proxy headers to islands + returned prerender hints (#21740)

This commit is contained in:
Daniel Roe 2023-06-25 17:38:15 +01:00 committed by GitHub
parent 5b7f52870c
commit 88bc32d42a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 74 additions and 47 deletions

View File

@ -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<NuxtIslandResponse>(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,

View File

@ -367,7 +367,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
await nitroApp.hooks.callHook('render:island', islandResponse, { event, islandContext })
const response: RenderResponse = {
const response = {
body: JSON.stringify(islandResponse, null, 2),
statusCode: event.node.res.statusCode,
statusMessage: event.node.res.statusMessage,
@ -375,7 +375,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
'content-type': 'application/json;charset=utf-8',
'x-powered-by': 'Nuxt'
}
}
} satisfies RenderResponse
if (process.env.prerender) {
ISLAND_CACHE!.set(`/__nuxt_island/${islandContext!.name}_${islandContext!.id}`, response)
}
@ -383,7 +383,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
}
// Construct HTML response
const response: RenderResponse = {
const response = {
body: renderHTMLDocument(htmlContext),
statusCode: event.node.res.statusCode,
statusMessage: event.node.res.statusMessage,
@ -391,7 +391,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
'content-type': 'text/html;charset=utf-8',
'x-powered-by': 'Nuxt'
}
}
} satisfies RenderResponse
return response
})
@ -456,7 +456,7 @@ async function renderInlineStyles (usedModules: Set<string> | string[]) {
}
function renderPayloadResponse (ssrContext: NuxtSSRContext) {
return <RenderResponse> {
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 }) {

View File

@ -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')

View File

@ -4,6 +4,12 @@
</div>
</template>
<script setup>
import { appendResponseHeader } from 'h3'
appendResponseHeader(useRequestEvent(), 'x-nitro-prerender', '/some/url/from/server-only/component')
</script>
<style>
:root {
--server-only: 'server-only';