diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index fce05c322c..c243a8a7b3 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -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('nuxt-app') @@ -23,7 +25,7 @@ export interface RuntimeNuxtHooks { 'app:created': (app: App) => HookResult 'app:beforeMount': (app: App) => HookResult 'app:mounted': (app: App) => HookResult - 'app:rendered': () => HookResult + 'app:rendered': (ctx: NuxtRenderContext) => HookResult 'app:redirected': () => HookResult 'app:suspense:resolve': (Component?: VNode) => HookResult 'app:error': (err: any) => HookResult diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index fe4ba0859d..28eb2ae885 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -2,31 +2,32 @@ 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'] +export 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 interface NuxtRenderContext { + ssrContext: NuxtSSRContext + html: { + htmlAttrs: string[] + head: string[] + bodyAttrs: string[] + bodyPreprend: string[] + body: string[] + bodyAppend: string[] + } +} + +export interface NuxtRenderResponse { + body: string, + statusCode: number, + statusMessage?: string, + headers: Record } // @ts-ignore @@ -132,50 +133,81 @@ 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 } - - if (event.res.writableEnded) { - return - } + const _rendered = await renderer.renderToString(ssrContext).catch((err) => { + if (!ssrError) { throw err } + }) // Handle errors + if (!_rendered) { + return + } 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([ + ``, + _rendered.renderScripts(), + renderedMeta.bodyScripts + ]) + } } - const html = await renderHTML(ssrContext.payload, rendered, ssrContext) - event.res.setHeader('Content-Type', 'text/html;charset=UTF-8') - return html + // 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 response + const response: NuxtRenderResponse = { + body: renderHTMLDocument(rendered), + statusCode: event.res.statusCode, + statusMessage: event.res.statusMessage, + headers: { + 'Content-Type': 'text/html;charset=UTF-8', + 'X-Powered-By': 'Nuxt' + } + } + + // Allow extending the response + await nitroApp.hooks.callHook('nuxt:app:response', { response }) + + // Send HTML response + if (!event.res.headersSent) { + for (const header in response.headers) { + event.res.setHeader(header, response.headers[header]) + } + event.res.statusCode = response.statusCode + event.res.statusMessage = response.statusMessage + } + if (!event.res.writableEnded) { + event.res.end(response.body) + } }) -async function renderHTML (payload: any, rendered: RenderResult, ssrContext: NuxtSSRContext) { - const state = `` - - 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 (fn: () => Promise): () => Promise { let res: Promise | null = null return () => { @@ -185,3 +217,23 @@ function lazyCachedFunction (fn: () => Promise): () => Promise { 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 ` + +${joinTags(rendered.html.head)} +${joinTags(rendered.html.bodyPreprend)}${joinTags(rendered.html.body)}${joinTags(rendered.html.bodyAppend)} +` +} diff --git a/packages/nuxt/src/core/templates.ts b/packages/nuxt/src/core/templates.ts index 49f4015724..57299a0a6f 100644 --- a/packages/nuxt/src/core/templates.ts +++ b/packages/nuxt/src/core/templates.ts @@ -74,26 +74,6 @@ export const serverPluginTemplate = { } } -export const appViewTemplate = { - filename: 'views/document.template.mjs', - write: true, - getContents () { - return `export default (params) => \` - - - - \${params.HEAD} - - -\${params.BODY_PREPEND} - \${params.APP} - - -\` -` - } -} - export const pluginsDeclaration = { filename: 'types/plugins.d.ts', getContents: (ctx: TemplateContext) => {