Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

feat(nuxt): app:rendered hook #6042

Merged
merged 18 commits into from
Jul 27, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/nuxt/src/app/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type { RuntimeConfig } from '@nuxt/schema'
import { getContext } from 'unctx'
import type { SSRContext } from 'vue-bundle-renderer'
import type { CompatibilityEvent } from 'h3'
// eslint-disable-next-line import/no-restricted-paths
import type { NuxtRenderContext } from '../core/runtime/nitro/renderer'

const nuxtAppCtx = getContext<NuxtApp>('nuxt-app')

Expand All @@ -23,7 +25,7 @@ export interface RuntimeNuxtHooks {
'app:created': (app: App<Element>) => HookResult
'app:beforeMount': (app: App<Element>) => HookResult
'app:mounted': (app: App<Element>) => HookResult
'app:rendered': () => HookResult
'app:rendered': (ctx: NuxtRenderContext) => HookResult
'app:redirected': () => HookResult
'app:suspense:resolve': (Component?: VNode) => HookResult
'app:error': (err: any) => HookResult
Expand Down
130 changes: 79 additions & 51 deletions packages/nuxt/src/core/runtime/nitro/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,25 @@ import { createRenderer } from 'vue-bundle-renderer'
import { eventHandler, useQuery } from 'h3'
import devalue from '@nuxt/devalue'
import { renderToString as _renderToString } from 'vue/server-renderer'

import type { NuxtApp } from '#app'

// @ts-ignore
import { useRuntimeConfig } from '#internal/nitro'
import { useRuntimeConfig, useNitroApp } from '#internal/nitro'
// @ts-ignore
import { buildAssetsURL } from '#paths'
// @ts-ignore
import htmlTemplate from '#build/views/document.template.mjs'

type NuxtSSRContext = NuxtApp['ssrContext']

interface RenderResult {
html: any
renderResourceHints: () => string
renderStyles: () => string
renderScripts: () => string
meta?: Partial<{
htmlAttrs?: string,
bodyAttrs: string,
headAttrs: string,
headTags: string,
bodyScriptsPrepend : string,
bodyScripts : string
}>

export type NuxtSSRContext = NuxtApp['ssrContext']

export interface NuxtRenderContext {
ssrContext: NuxtSSRContext
html: {
htmlAttrs: string[]
head: string[]
bodyAttrs: string[]
bodyPreprend: string[]
body: string[]
bodyAppend: string[]
}
}

// @ts-ignore
Expand Down Expand Up @@ -132,50 +126,64 @@ export default eventHandler(async (event) => {

// Render app
const renderer = (process.env.NUXT_NO_SSR || ssrContext.noSSR) ? await getSPARenderer() : await getSSRRenderer()
const rendered = await renderer.renderToString(ssrContext).catch((e) => {
if (!ssrError) { throw e }
}) as RenderResult

// If we error on rendering error page, we bail out and directly return to the error handler
if (!rendered) { return }
const _rendered = await renderer.renderToString(ssrContext).catch((err) => {
if (!ssrError) { throw err }
})

if (event.res.writableEnded) {
// Handle errors
if (!_rendered) {
return
}

// Handle errors
if (ssrContext.error && !ssrError) {
throw ssrContext.error
}

if (ssrContext.nuxt?.hooks) {
await ssrContext.nuxt.hooks.callHook('app:rendered')
// Render meta
const renderedMeta = await ssrContext.renderMeta()

// Create render conrtext
const rendered: NuxtRenderContext = {
ssrContext,
html: {
htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]),
head: normalizeChunks([
renderedMeta.headTags,
_rendered.renderResourceHints(),
_rendered.renderStyles(),
ssrContext.styles
]),
bodyAttrs: normalizeChunks([renderedMeta.bodyAttrs]),
bodyPreprend: normalizeChunks([
renderedMeta.bodyScriptsPrepend,
ssrContext.teleports?.body
]),
body: [
// TODO: Rename to _rendered.body in next vue-bundle-renderer
_rendered.html
],
bodyAppend: normalizeChunks([
`<script>window.__NUXT__=${devalue(ssrContext.payload)}</script>`,
_rendered.renderScripts(),
renderedMeta.bodyScripts
])
}
}

const html = await renderHTML(ssrContext.payload, rendered, ssrContext)
// Allow hooking into the rendered result
const nitroApp = useNitroApp()
await ssrContext.nuxt.hooks.callHook('app:rendered', rendered)
await nitroApp.hooks.callHook('nuxt:app:rendered', rendered)

// Construct HTML template
const html = renderHTMLDocument(rendered)
await nitroApp.hooks.callHook('nuxt:app:rendered:html', { html })

// Send HTML response
if (event.res.writableEnded) { return }
event.res.setHeader('Content-Type', 'text/html;charset=UTF-8')
return html
})

async function renderHTML (payload: any, rendered: RenderResult, ssrContext: NuxtSSRContext) {
const state = `<script>window.__NUXT__=${devalue(payload)}</script>`

rendered.meta = rendered.meta || {}
if (ssrContext.renderMeta) {
Object.assign(rendered.meta, await ssrContext.renderMeta())
}

return htmlTemplate({
HTML_ATTRS: (rendered.meta.htmlAttrs || ''),
HEAD_ATTRS: (rendered.meta.headAttrs || ''),
HEAD: (rendered.meta.headTags || '') +
rendered.renderResourceHints() + rendered.renderStyles() + (ssrContext.styles || ''),
BODY_ATTRS: (rendered.meta.bodyAttrs || ''),
BODY_PREPEND: (ssrContext.teleports?.body || ''),
APP: (rendered.meta.bodyScriptsPrepend || '') + rendered.html + state + rendered.renderScripts() + (rendered.meta.bodyScripts || '')
})
}

function lazyCachedFunction <T> (fn: () => Promise<T>): () => Promise<T> {
let res: Promise<T> | null = null
return () => {
Expand All @@ -185,3 +193,23 @@ function lazyCachedFunction <T> (fn: () => Promise<T>): () => Promise<T> {
return res
}
}

function normalizeChunks (chunks: string[]) {
return chunks.filter(Boolean).map(i => i.trim())
}

function joinTags (tags: string[]) {
return tags.join('')
}

function joinAttrs (chunks: string[]) {
return chunks.join(' ')
}

function renderHTMLDocument (rendered: NuxtRenderContext) {
return `<!DOCTYPE html>
<html ${joinAttrs(rendered.html.htmlAttrs)}>
<head>${joinTags(rendered.html.head)}</head>
<body ${joinAttrs(rendered.html.bodyAttrs)}>${joinTags(rendered.html.bodyPreprend)}${joinTags(rendered.html.body)}${joinTags(rendered.html.bodyAppend)}</body>
</html>`
}
20 changes: 0 additions & 20 deletions packages/nuxt/src/core/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,26 +74,6 @@ export const serverPluginTemplate = {
}
}

export const appViewTemplate = {
filename: 'views/document.template.mjs',
write: true,
getContents () {
return `export default (params) => \`<!DOCTYPE html>
<html \${params.HTML_ATTRS}>

<head \${params.HEAD_ATTRS}>
\${params.HEAD}
</head>

<body \${params.BODY_ATTRS}>\${params.BODY_PREPEND}
\${params.APP}
</body>

</html>\`
`
}
}

export const pluginsDeclaration = {
filename: 'types/plugins.d.ts',
getContents: (ctx: TemplateContext) => {
Expand Down