Nuxt/packages/vue-renderer/src/renderers/ssr.js

140 lines
4.3 KiB
JavaScript
Raw Normal View History

import path from 'path'
import crypto from 'crypto'
import fs from 'fs-extra'
import devalue from '@nuxt/devalue'
import { createBundleRenderer } from 'vue-server-renderer'
import BaseRenderer from './base'
export default class SSRRenderer extends BaseRenderer {
get rendererOptions() {
const hasModules = fs.existsSync(path.resolve(this.options.rootDir, 'node_modules'))
return {
clientManifest: this.serverContext.resources.clientManifest,
// for globally installed nuxt command, search dependencies in global dir
basedir: hasModules ? this.options.rootDir : __dirname,
...this.options.render.bundleRenderer
}
}
renderScripts(renderContext) {
return renderContext.renderScripts()
}
getPreloadFiles(renderContext) {
return renderContext.getPreloadFiles()
}
renderResourceHints(renderContext) {
return renderContext.renderResourceHints()
}
createRenderer() {
// Create bundle renderer for SSR
return createBundleRenderer(
this.serverContext.resources.serverManifest,
this.rendererOptions
)
}
async render(renderContext) {
// Call ssr:context hook to extend context from modules
await this.serverContext.nuxt.callHook('vue-renderer:ssr:prepareContext', renderContext)
// Call Vue renderer renderToString
let APP = await this.vueRenderer.renderToString(renderContext)
// Call ssr:context hook
await this.serverContext.nuxt.callHook('vue-renderer:ssr:context', renderContext)
// TODO: Remove in next major release
await this.serverContext.nuxt.callHook('render:routeContext', renderContext.nuxt)
// Fallback to empty response
if (!renderContext.nuxt.serverRendered) {
APP = `<div id="${this.serverContext.globals.id}"></div>`
}
// Inject head meta
const m = renderContext.meta.inject()
let HEAD =
m.title.text() +
m.meta.text() +
m.link.text() +
m.style.text() +
m.script.text() +
m.noscript.text()
// Add <base href=""> meta if router base specified
if (this.options._routerBaseSpecified) {
HEAD += `<base href="${this.options.router.base}">`
}
// Inject resource hints
if (this.options.render.resourceHints) {
HEAD += this.renderResourceHints(renderContext)
}
// Inject styles
HEAD += renderContext.renderStyles()
// Serialize state
const serializedSession = `window.${this.serverContext.globals.context}=${devalue(renderContext.nuxt)};`
APP += `<script>${serializedSession}</script>`
// Calculate CSP hashes
const { csp } = this.options.render
const cspScriptSrcHashes = []
if (csp) {
// Only add the hash if 'unsafe-inline' rule isn't present to avoid conflicts (#5387)
const containsUnsafeInlineScriptSrc = csp.policies && csp.policies['script-src'] && csp.policies['script-src'].includes(`'unsafe-inline'`)
if (!containsUnsafeInlineScriptSrc) {
const hash = crypto.createHash(csp.hashAlgorithm)
hash.update(serializedSession)
cspScriptSrcHashes.push(`'${csp.hashAlgorithm}-${hash.digest('base64')}'`)
}
// Call ssr:csp hook
await this.serverContext.nuxt.callHook('vue-renderer:ssr:csp', cspScriptSrcHashes)
// Add csp meta tags
if (csp.addMeta) {
HEAD += `<meta http-equiv="Content-Security-Policy" content="script-src ${cspScriptSrcHashes.join()}">`
}
}
// Prepend scripts
APP += this.renderScripts(renderContext)
APP += m.script.text({ body: true })
APP += m.noscript.text({ body: true })
// Template params
const templateParams = {
HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
HEAD_ATTRS: m.headAttrs.text(),
BODY_ATTRS: m.bodyAttrs.text(),
HEAD,
APP,
ENV: this.options.env
}
// Call ssr:templateParams hook
await this.serverContext.nuxt.callHook('vue-renderer:ssr:templateParams', templateParams)
// Render with SSR template
const html = this.renderTemplate(this.serverContext.resources.ssrTemplate, templateParams)
let preloadFiles
if (this.options.render.http2.push) {
preloadFiles = this.getPreloadFiles(renderContext)
}
return {
html,
cspScriptSrcHashes,
preloadFiles,
error: renderContext.nuxt.error,
redirected: renderContext.redirected
}
}
}