feat(nuxt): add dedicated #teleports element for ssr teleports (#25043)

This commit is contained in:
Alexander Lichter 2024-03-11 15:33:49 +01:00 committed by GitHub
parent 27f9c55fc2
commit 536998727a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 69 additions and 8 deletions

View File

@ -29,7 +29,7 @@ import unheadPlugins from '#internal/unhead-plugins.mjs'
// eslint-disable-next-line import/no-restricted-paths // eslint-disable-next-line import/no-restricted-paths
import type { NuxtPayload, NuxtSSRContext } from '#app' import type { NuxtPayload, NuxtSSRContext } from '#app'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { appHead, appRootId, appRootTag } from '#internal/nuxt.config.mjs' import { appHead, appRootId, appRootTag, appTeleportId, appTeleportTag } from '#internal/nuxt.config.mjs'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { buildAssetsURL, publicAssetsURL } from '#paths' import { buildAssetsURL, publicAssetsURL } from '#paths'
@ -137,7 +137,7 @@ const getSSRRenderer = lazyCachedFunction(async () => {
if (import.meta.dev && process.env.NUXT_VITE_NODE_OPTIONS) { if (import.meta.dev && process.env.NUXT_VITE_NODE_OPTIONS) {
renderer.rendererContext.updateManifest(await getClientManifest()) renderer.rendererContext.updateManifest(await getClientManifest())
} }
return `<${appRootTag}${appRootId ? ` id="${appRootId}"` : ''}>${html}</${appRootTag}>` return APP_ROOT_OPEN_TAG + html + APP_ROOT_CLOSE_TAG
} }
return renderer return renderer
@ -149,10 +149,11 @@ const getSPARenderer = lazyCachedFunction(async () => {
// @ts-expect-error virtual file // @ts-expect-error virtual file
const spaTemplate = await import('#spa-template').then(r => r.template).catch(() => '') const spaTemplate = await import('#spa-template').then(r => r.template).catch(() => '')
.then(r => APP_ROOT_OPEN_TAG + r + APP_ROOT_CLOSE_TAG)
const options = { const options = {
manifest, manifest,
renderToString: () => `<${appRootTag}${appRootId ? ` id="${appRootId}"` : ''}>${spaTemplate}</${appRootTag}>`, renderToString: () => spaTemplate,
buildAssetsURL buildAssetsURL
} }
// Create SPA renderer and cache the result for all requests // Create SPA renderer and cache the result for all requests
@ -230,8 +231,15 @@ async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
return ctx return ctx
} }
const HAS_APP_TELEPORTS = !!(appTeleportTag && appTeleportId)
const APP_TELEPORT_OPEN_TAG = HAS_APP_TELEPORTS ? `<${appTeleportTag} id="${appTeleportId}">` : ''
const APP_TELEPORT_CLOSE_TAG = HAS_APP_TELEPORTS ? `</${appTeleportTag}>` : ''
const APP_ROOT_OPEN_TAG = `<${appRootTag}${appRootId ? ` id="${appRootId}"` : ''}>`
const APP_ROOT_CLOSE_TAG = `</${appRootTag}>`
const PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /\/_payload.json(\?.*)?$/ : /\/_payload.js(\?.*)?$/ const PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /\/_payload.json(\?.*)?$/ : /\/_payload.js(\?.*)?$/
const ROOT_NODE_REGEX = new RegExp(`^<${appRootTag}${appRootId ? ` id="${appRootId}"` : ''}>([\\s\\S]*)</${appRootTag}>$`) const ROOT_NODE_REGEX = new RegExp(`^${APP_ROOT_OPEN_TAG}([\\s\\S]*)${APP_ROOT_CLOSE_TAG}$`)
const PRERENDER_NO_SSR_ROUTES = new Set(['/index.html', '/200.html', '/404.html']) const PRERENDER_NO_SSR_ROUTES = new Set(['/index.html', '/200.html', '/404.html'])
@ -459,7 +467,10 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
head: normalizeChunks([headTags, ssrContext.styles]), head: normalizeChunks([headTags, ssrContext.styles]),
bodyAttrs: bodyAttrs ? [bodyAttrs] : [], bodyAttrs: bodyAttrs ? [bodyAttrs] : [],
bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]), bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]),
body: [process.env.NUXT_COMPONENT_ISLANDS ? replaceIslandTeleports(ssrContext, _rendered.html) : _rendered.html], body: [
process.env.NUXT_COMPONENT_ISLANDS ? replaceIslandTeleports(ssrContext, _rendered.html) : _rendered.html,
APP_TELEPORT_OPEN_TAG + (HAS_APP_TELEPORTS ? joinTags([ssrContext.teleports?.[`#${appTeleportId}`]]) : '') + APP_TELEPORT_CLOSE_TAG
],
bodyAppend: [bodyTags] bodyAppend: [bodyTags]
} }
@ -534,7 +545,7 @@ function normalizeChunks (chunks: (string | undefined)[]) {
return chunks.filter(Boolean).map(i => i!.trim()) return chunks.filter(Boolean).map(i => i!.trim())
} }
function joinTags (tags: string[]) { function joinTags (tags: Array<string | undefined>) {
return tags.join('') return tags.join('')
} }

View File

@ -184,6 +184,21 @@ export default defineUntypedSchema({
*/ */
rootTag: { rootTag: {
$resolve: val => val || 'div' $resolve: val => val || 'div'
},
/**
* Customize Nuxt root element tag.
*/
teleportTag: {
$resolve: val => val || 'div'
},
/**
* Customize Nuxt Teleport element id.
* @type {string | false}
*/
teleportId: {
$resolve: val => val === false ? false : (val || 'teleports')
} }
}, },

View File

@ -2470,6 +2470,23 @@ describe('keepalive', () => {
}) })
}) })
describe('teleports', () => {
it('should append teleports to body', async () => {
const html = await $fetch('/teleport')
// Teleport is prepended to body, before the __nuxt div
expect(html).toContain('<div>Teleport</div><!--teleport anchor--><div id="__nuxt">')
// Teleport start and end tag are rendered as expected
expect(html).toContain('<div><!--teleport start--><!--teleport end--><h1>Normal content</h1></div>')
})
it('should render teleports to app teleports element', async () => {
const html = await $fetch('/nuxt-teleport')
// Teleport is appended to body, after the __nuxt div
expect(html).toContain('<div><!--teleport start--><!--teleport end--><h1>Normal content</h1></div></div></div><span id="nuxt-teleport"><div>Nuxt Teleport</div><!--teleport anchor--></span><script')
})
})
describe('Node.js compatibility for client-side', () => { describe('Node.js compatibility for client-side', () => {
it('should work', async () => { it('should work', async () => {
const { page } = await renderPage('/node-compat') const { page } = await renderPage('/node-compat')

View File

@ -32,7 +32,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
const serverDir = join(rootDir, '.output/server') const serverDir = join(rootDir, '.output/server')
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"204k"') expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"205k"')
const modules = await analyzeSizes('node_modules/**/*', serverDir) const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"1335k"') expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"1335k"')
@ -72,7 +72,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
const serverDir = join(rootDir, '.output-inline/server') const serverDir = join(rootDir, '.output-inline/server')
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"523k"') expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"524k"')
const modules = await analyzeSizes('node_modules/**/*', serverDir) const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"78.0k"') expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"78.0k"')

View File

@ -14,6 +14,8 @@ export default defineNuxtConfig({
app: { app: {
pageTransition: true, pageTransition: true,
layoutTransition: true, layoutTransition: true,
teleportId: 'nuxt-teleport',
teleportTag: 'span',
head: { head: {
charset: 'utf-8', charset: 'utf-8',
link: [undefined], link: [undefined],

View File

@ -0,0 +1,8 @@
<template>
<div>
<teleport to="#nuxt-teleport">
<div>Nuxt Teleport</div>
</teleport>
<h1>Normal content</h1>
</div>
</template>

View File

@ -0,0 +1,8 @@
<template>
<div>
<teleport to="body">
<div>Teleport</div>
</teleport>
<h1>Normal content</h1>
</div>
</template>