mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +00:00
fix(nuxt): proxy headers to islands + returned prerender hints (#21740)
This commit is contained in:
parent
5b7f52870c
commit
88bc32d42a
@ -4,6 +4,8 @@ import { hash } from 'ohash'
|
|||||||
import { appendResponseHeader } from 'h3'
|
import { appendResponseHeader } from 'h3'
|
||||||
import { useHead } from '@unhead/vue'
|
import { useHead } from '@unhead/vue'
|
||||||
import { randomUUID } from 'uncrypto'
|
import { randomUUID } from 'uncrypto'
|
||||||
|
import { withQuery } from 'ufo'
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-restricted-paths
|
// eslint-disable-next-line import/no-restricted-paths
|
||||||
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
|
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
|
||||||
import { getFragmentHTML, getSlotProps } from './utils'
|
import { getFragmentHTML, getSlotProps } from './utils'
|
||||||
@ -40,6 +42,7 @@ export default defineComponent({
|
|||||||
const hashId = computed(() => hash([props.name, props.props, props.context]))
|
const hashId = computed(() => hash([props.name, props.props, props.context]))
|
||||||
const instance = getCurrentInstance()!
|
const instance = getCurrentInstance()!
|
||||||
const event = useRequestEvent()
|
const event = useRequestEvent()
|
||||||
|
const eventFetch = process.server ? event.fetch : globalThis.fetch
|
||||||
const mounted = ref(false)
|
const mounted = ref(false)
|
||||||
onMounted(() => { mounted.value = true })
|
onMounted(() => { mounted.value = true })
|
||||||
|
|
||||||
@ -78,13 +81,18 @@ export default defineComponent({
|
|||||||
appendResponseHeader(event, 'x-nitro-prerender', url)
|
appendResponseHeader(event, 'x-nitro-prerender', url)
|
||||||
}
|
}
|
||||||
// TODO: Validate response
|
// TODO: Validate response
|
||||||
const result = await $fetch<NuxtIslandResponse>(url, {
|
const r = await eventFetch(withQuery(url, {
|
||||||
responseType: 'json',
|
...props.context,
|
||||||
params: {
|
props: props.props ? JSON.stringify(props.props) : undefined
|
||||||
...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] = {
|
nuxtApp.payload.data[key] = {
|
||||||
__nuxt_island: {
|
__nuxt_island: {
|
||||||
key,
|
key,
|
||||||
|
@ -367,7 +367,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
|
|
||||||
await nitroApp.hooks.callHook('render:island', islandResponse, { event, islandContext })
|
await nitroApp.hooks.callHook('render:island', islandResponse, { event, islandContext })
|
||||||
|
|
||||||
const response: RenderResponse = {
|
const response = {
|
||||||
body: JSON.stringify(islandResponse, null, 2),
|
body: JSON.stringify(islandResponse, null, 2),
|
||||||
statusCode: event.node.res.statusCode,
|
statusCode: event.node.res.statusCode,
|
||||||
statusMessage: event.node.res.statusMessage,
|
statusMessage: event.node.res.statusMessage,
|
||||||
@ -375,7 +375,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
'content-type': 'application/json;charset=utf-8',
|
'content-type': 'application/json;charset=utf-8',
|
||||||
'x-powered-by': 'Nuxt'
|
'x-powered-by': 'Nuxt'
|
||||||
}
|
}
|
||||||
}
|
} satisfies RenderResponse
|
||||||
if (process.env.prerender) {
|
if (process.env.prerender) {
|
||||||
ISLAND_CACHE!.set(`/__nuxt_island/${islandContext!.name}_${islandContext!.id}`, response)
|
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
|
// Construct HTML response
|
||||||
const response: RenderResponse = {
|
const response = {
|
||||||
body: renderHTMLDocument(htmlContext),
|
body: renderHTMLDocument(htmlContext),
|
||||||
statusCode: event.node.res.statusCode,
|
statusCode: event.node.res.statusCode,
|
||||||
statusMessage: event.node.res.statusMessage,
|
statusMessage: event.node.res.statusMessage,
|
||||||
@ -391,7 +391,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
'content-type': 'text/html;charset=utf-8',
|
'content-type': 'text/html;charset=utf-8',
|
||||||
'x-powered-by': 'Nuxt'
|
'x-powered-by': 'Nuxt'
|
||||||
}
|
}
|
||||||
}
|
} satisfies RenderResponse
|
||||||
|
|
||||||
return response
|
return response
|
||||||
})
|
})
|
||||||
@ -456,7 +456,7 @@ async function renderInlineStyles (usedModules: Set<string> | string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderPayloadResponse (ssrContext: NuxtSSRContext) {
|
function renderPayloadResponse (ssrContext: NuxtSSRContext) {
|
||||||
return <RenderResponse> {
|
return {
|
||||||
body: process.env.NUXT_JSON_PAYLOADS
|
body: process.env.NUXT_JSON_PAYLOADS
|
||||||
? stringify(splitPayload(ssrContext).payload, ssrContext._payloadReducers)
|
? stringify(splitPayload(ssrContext).payload, ssrContext._payloadReducers)
|
||||||
: `export default ${devalue(splitPayload(ssrContext).payload)}`,
|
: `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',
|
'content-type': process.env.NUXT_JSON_PAYLOADS ? 'application/json;charset=utf-8' : 'text/javascript;charset=utf-8',
|
||||||
'x-powered-by': 'Nuxt'
|
'x-powered-by': 'Nuxt'
|
||||||
}
|
}
|
||||||
}
|
} satisfies RenderResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }) {
|
function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }) {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
import { readdir } from 'node:fs/promises'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { joinURL, withQuery } from 'ufo'
|
import { joinURL, withQuery } from 'ufo'
|
||||||
import { isCI, isWindows } from 'std-env'
|
import { isCI, isWindows } from 'std-env'
|
||||||
import { normalize } from 'pathe'
|
import { join, normalize } from 'pathe'
|
||||||
import { $fetch, createPage, fetch, isDev, setup, startServer, url } from '@nuxt/test-utils'
|
import { $fetch, createPage, fetch, isDev, setup, startServer, url, useTestContext } from '@nuxt/test-utils'
|
||||||
import { $fetchComponent } from '@nuxt/test-utils/experimental'
|
import { $fetchComponent } from '@nuxt/test-utils/experimental'
|
||||||
|
|
||||||
import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer'
|
import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer'
|
||||||
@ -418,39 +419,6 @@ describe('pages', () => {
|
|||||||
await page.close()
|
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 () => {
|
it('/legacy-async-data-fail', async () => {
|
||||||
const response = await fetch('/legacy-async-data-fail').then(r => r.text())
|
const response = await fetch('/legacy-async-data-fail').then(r => r.text())
|
||||||
expect(response).not.toContain('don\'t look at this')
|
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', () => {
|
describe.skipIf(isDev() || isWindows || !isRenderingJson)('prefetching', () => {
|
||||||
it('should prefetch components', async () => {
|
it('should prefetch components', async () => {
|
||||||
await expectNoClientErrors('/prefetch/components')
|
await expectNoClientErrors('/prefetch/components')
|
||||||
|
@ -4,6 +4,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { appendResponseHeader } from 'h3'
|
||||||
|
|
||||||
|
appendResponseHeader(useRequestEvent(), 'x-nitro-prerender', '/some/url/from/server-only/component')
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--server-only: 'server-only';
|
--server-only: 'server-only';
|
||||||
|
Loading…
Reference in New Issue
Block a user