Skip to content

Commit

Permalink
feat(nuxt): add dedicated #teleports element for ssr teleports (#25043
Browse files Browse the repository at this point in the history
)
  • Loading branch information
manniL committed Mar 11, 2024
1 parent 27f9c55 commit 5369987
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 8 deletions.
23 changes: 17 additions & 6 deletions packages/nuxt/src/core/runtime/nitro/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import unheadPlugins from '#internal/unhead-plugins.mjs'
// eslint-disable-next-line import/no-restricted-paths
import type { NuxtPayload, NuxtSSRContext } from '#app'
// @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
import { buildAssetsURL, publicAssetsURL } from '#paths'

Expand Down Expand Up @@ -137,7 +137,7 @@ const getSSRRenderer = lazyCachedFunction(async () => {
if (import.meta.dev && process.env.NUXT_VITE_NODE_OPTIONS) {
renderer.rendererContext.updateManifest(await getClientManifest())
}
return `<${appRootTag}${appRootId ? ` id="${appRootId}"` : ''}>${html}</${appRootTag}>`
return APP_ROOT_OPEN_TAG + html + APP_ROOT_CLOSE_TAG
}

return renderer
Expand All @@ -149,10 +149,11 @@ const getSPARenderer = lazyCachedFunction(async () => {

// @ts-expect-error virtual file
const spaTemplate = await import('#spa-template').then(r => r.template).catch(() => '')
.then(r => APP_ROOT_OPEN_TAG + r + APP_ROOT_CLOSE_TAG)

const options = {
manifest,
renderToString: () => `<${appRootTag}${appRootId ? ` id="${appRootId}"` : ''}>${spaTemplate}</${appRootTag}>`,
renderToString: () => spaTemplate,
buildAssetsURL
}
// Create SPA renderer and cache the result for all requests
Expand Down Expand Up @@ -230,8 +231,15 @@ async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
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 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'])

Expand Down Expand Up @@ -459,7 +467,10 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
head: normalizeChunks([headTags, ssrContext.styles]),
bodyAttrs: bodyAttrs ? [bodyAttrs] : [],
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]
}

Expand Down Expand Up @@ -534,7 +545,7 @@ function normalizeChunks (chunks: (string | undefined)[]) {
return chunks.filter(Boolean).map(i => i!.trim())
}

function joinTags (tags: string[]) {
function joinTags (tags: Array<string | undefined>) {
return tags.join('')
}

Expand Down
15 changes: 15 additions & 0 deletions packages/schema/src/config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,21 @@ export default defineUntypedSchema({
*/
rootTag: {
$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')
}
},

Expand Down
17 changes: 17 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
it('should work', async () => {
const { page } = await renderPage('/node-compat')
Expand Down
4 changes: 2 additions & 2 deletions test/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
const serverDir = join(rootDir, '.output/server')

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)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"1335k"')
Expand Down Expand Up @@ -72,7 +72,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
const serverDir = join(rootDir, '.output-inline/server')

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)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"78.0k"')
Expand Down
2 changes: 2 additions & 0 deletions test/fixtures/basic/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export default defineNuxtConfig({
app: {
pageTransition: true,
layoutTransition: true,
teleportId: 'nuxt-teleport',
teleportTag: 'span',
head: {
charset: 'utf-8',
link: [undefined],
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/basic/pages/nuxt-teleport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template>
<div>
<teleport to="#nuxt-teleport">
<div>Nuxt Teleport</div>
</teleport>
<h1>Normal content</h1>
</div>
</template>
8 changes: 8 additions & 0 deletions test/fixtures/basic/pages/teleport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template>
<div>
<teleport to="body">
<div>Teleport</div>
</teleport>
<h1>Normal content</h1>
</div>
</template>

0 comments on commit 5369987

Please sign in to comment.