2019-04-20 12:02:51 +00:00
|
|
|
import path from 'path'
|
|
|
|
import crypto from 'crypto'
|
2019-05-19 18:49:24 +00:00
|
|
|
import { format } from 'util'
|
2019-04-20 12:02:51 +00:00
|
|
|
import fs from 'fs-extra'
|
2019-05-09 07:06:17 +00:00
|
|
|
import consola from 'consola'
|
2020-05-07 19:08:01 +00:00
|
|
|
import { TARGETS, urlJoin } from '@nuxt/utils'
|
2019-04-20 12:02:51 +00:00
|
|
|
import devalue from '@nuxt/devalue'
|
|
|
|
import { createBundleRenderer } from 'vue-server-renderer'
|
|
|
|
import BaseRenderer from './base'
|
|
|
|
|
|
|
|
export default class SSRRenderer extends BaseRenderer {
|
2019-07-10 10:45:49 +00:00
|
|
|
get rendererOptions () {
|
2019-04-20 12:02:51 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-10 10:45:49 +00:00
|
|
|
renderScripts (renderContext) {
|
2020-02-24 22:58:24 +00:00
|
|
|
const scripts = renderContext.renderScripts()
|
2020-04-07 09:38:49 +00:00
|
|
|
const { render: { crossorigin } } = this.options
|
2020-02-24 22:58:24 +00:00
|
|
|
if (!crossorigin) {
|
|
|
|
return scripts
|
|
|
|
}
|
|
|
|
return scripts.replace(
|
|
|
|
/<script/g,
|
|
|
|
`<script crossorigin="${crossorigin}"`
|
|
|
|
)
|
2019-04-20 12:02:51 +00:00
|
|
|
}
|
|
|
|
|
2019-07-10 10:45:49 +00:00
|
|
|
getPreloadFiles (renderContext) {
|
2019-04-20 12:02:51 +00:00
|
|
|
return renderContext.getPreloadFiles()
|
|
|
|
}
|
|
|
|
|
2019-07-10 10:45:49 +00:00
|
|
|
renderResourceHints (renderContext) {
|
2020-02-24 22:58:24 +00:00
|
|
|
const resourceHints = renderContext.renderResourceHints()
|
2020-04-07 09:38:49 +00:00
|
|
|
const { render: { crossorigin } } = this.options
|
2020-02-24 22:58:24 +00:00
|
|
|
if (!crossorigin) {
|
|
|
|
return resourceHints
|
|
|
|
}
|
|
|
|
return resourceHints.replace(
|
|
|
|
/rel="preload"/g,
|
|
|
|
`rel="preload" crossorigin="${crossorigin}"`
|
|
|
|
)
|
2019-04-20 12:02:51 +00:00
|
|
|
}
|
|
|
|
|
2019-07-10 10:45:49 +00:00
|
|
|
createRenderer () {
|
2019-04-20 12:02:51 +00:00
|
|
|
// Create bundle renderer for SSR
|
|
|
|
return createBundleRenderer(
|
|
|
|
this.serverContext.resources.serverManifest,
|
|
|
|
this.rendererOptions
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-07-10 10:45:49 +00:00
|
|
|
useSSRLog () {
|
2019-05-23 09:49:16 +00:00
|
|
|
if (!this.options.render.ssrLog) {
|
|
|
|
return
|
|
|
|
}
|
2019-05-09 07:06:17 +00:00
|
|
|
const logs = []
|
|
|
|
const devReporter = {
|
2019-07-10 10:45:49 +00:00
|
|
|
log (logObj) {
|
2019-05-19 18:49:24 +00:00
|
|
|
logs.push({
|
|
|
|
...logObj,
|
2019-05-20 16:05:17 +00:00
|
|
|
args: logObj.args.map(arg => format(arg))
|
2019-05-19 18:49:24 +00:00
|
|
|
})
|
2019-05-09 07:06:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
consola.addReporter(devReporter)
|
|
|
|
|
2019-05-23 09:49:16 +00:00
|
|
|
return () => {
|
|
|
|
consola.removeReporter(devReporter)
|
|
|
|
return logs
|
|
|
|
}
|
2019-05-09 07:06:17 +00:00
|
|
|
}
|
|
|
|
|
2019-07-10 10:45:49 +00:00
|
|
|
async render (renderContext) {
|
2019-04-20 12:02:51 +00:00
|
|
|
// Call ssr:context hook to extend context from modules
|
|
|
|
await this.serverContext.nuxt.callHook('vue-renderer:ssr:prepareContext', renderContext)
|
|
|
|
|
2019-05-23 09:49:16 +00:00
|
|
|
const getSSRLog = this.useSSRLog()
|
|
|
|
|
2019-04-20 12:02:51 +00:00
|
|
|
// Call Vue renderer renderToString
|
2019-05-23 09:49:16 +00:00
|
|
|
let APP = await this.vueRenderer.renderToString(renderContext)
|
|
|
|
|
|
|
|
if (typeof getSSRLog === 'function') {
|
|
|
|
renderContext.nuxt.logs = getSSRLog()
|
|
|
|
}
|
2019-04-20 12:02:51 +00:00
|
|
|
|
|
|
|
// Call ssr:context hook
|
|
|
|
await this.serverContext.nuxt.callHook('vue-renderer:ssr:context', renderContext)
|
2020-02-25 16:15:40 +00:00
|
|
|
|
|
|
|
// TODO: Remove in next major release (#4722)
|
|
|
|
await this.serverContext.nuxt.callHook('_render:context', renderContext.nuxt)
|
2019-04-20 12:02:51 +00:00
|
|
|
|
|
|
|
// Fallback to empty response
|
|
|
|
if (!renderContext.nuxt.serverRendered) {
|
|
|
|
APP = `<div id="${this.serverContext.globals.id}"></div>`
|
|
|
|
}
|
|
|
|
|
2020-05-07 19:08:01 +00:00
|
|
|
// Perf: early returns if server target and redirected
|
|
|
|
if (renderContext.redirected && renderContext.target === TARGETS.server) {
|
2020-02-16 13:20:08 +00:00
|
|
|
return {
|
|
|
|
html: APP,
|
|
|
|
error: renderContext.nuxt.error,
|
|
|
|
redirected: renderContext.redirected
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-05 15:15:27 +00:00
|
|
|
let HEAD = ''
|
|
|
|
|
2019-04-20 12:02:51 +00:00
|
|
|
// Inject head meta
|
2019-09-05 15:15:27 +00:00
|
|
|
// (this is unset when features.meta is false in server template)
|
2020-06-10 15:26:50 +00:00
|
|
|
const meta = renderContext.meta && renderContext.meta.inject({
|
|
|
|
isSSR: renderContext.nuxt.serverRendered,
|
|
|
|
ln: this.options.dev
|
|
|
|
})
|
|
|
|
|
2019-09-05 15:15:27 +00:00
|
|
|
if (meta) {
|
2020-05-16 16:03:24 +00:00
|
|
|
HEAD += meta.title.text() + meta.meta.text()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add <base href=""> meta if router base specified
|
|
|
|
if (this.options._routerBaseSpecified) {
|
|
|
|
HEAD += `<base href="${this.options.router.base}">`
|
|
|
|
}
|
|
|
|
|
|
|
|
if (meta) {
|
|
|
|
HEAD += meta.link.text() +
|
2019-09-05 15:15:27 +00:00
|
|
|
meta.style.text() +
|
|
|
|
meta.script.text() +
|
|
|
|
meta.noscript.text()
|
|
|
|
}
|
2019-04-20 12:02:51 +00:00
|
|
|
|
2019-05-25 18:19:10 +00:00
|
|
|
// Check if we need to inject scripts and state
|
|
|
|
const shouldInjectScripts = this.options.render.injectScripts !== false
|
|
|
|
|
2019-04-20 12:02:51 +00:00
|
|
|
// Inject resource hints
|
2019-05-25 18:19:10 +00:00
|
|
|
if (this.options.render.resourceHints && shouldInjectScripts) {
|
2019-04-20 12:02:51 +00:00
|
|
|
HEAD += this.renderResourceHints(renderContext)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Inject styles
|
|
|
|
HEAD += renderContext.renderStyles()
|
|
|
|
|
2019-09-05 15:15:27 +00:00
|
|
|
if (meta) {
|
2020-06-10 15:26:50 +00:00
|
|
|
const prependInjectorOptions = { pbody: true }
|
|
|
|
|
2019-09-05 15:15:27 +00:00
|
|
|
const BODY_PREPEND =
|
2020-06-10 15:26:50 +00:00
|
|
|
meta.meta.text(prependInjectorOptions) +
|
|
|
|
meta.link.text(prependInjectorOptions) +
|
|
|
|
meta.style.text(prependInjectorOptions) +
|
|
|
|
meta.script.text(prependInjectorOptions) +
|
|
|
|
meta.noscript.text(prependInjectorOptions)
|
2019-08-19 18:38:13 +00:00
|
|
|
|
2019-09-05 15:15:27 +00:00
|
|
|
if (BODY_PREPEND) {
|
|
|
|
APP = `${BODY_PREPEND}${APP}`
|
|
|
|
}
|
2019-08-19 18:38:13 +00:00
|
|
|
}
|
|
|
|
|
2020-01-10 20:43:50 +00:00
|
|
|
const { csp } = this.options.render
|
|
|
|
// 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\'')
|
|
|
|
const shouldHashCspScriptSrc = csp && (csp.unsafeInlineCompatibility || !containsUnsafeInlineScriptSrc)
|
2020-05-07 19:08:01 +00:00
|
|
|
const inlineScripts = []
|
|
|
|
|
|
|
|
if (renderContext.staticAssetsBase) {
|
|
|
|
const preloadScripts = []
|
|
|
|
renderContext.staticAssets = []
|
2020-06-10 07:51:29 +00:00
|
|
|
const routerBase = this.options.router.base
|
2020-05-07 19:08:01 +00:00
|
|
|
const { staticAssetsBase, url, nuxt, staticAssets } = renderContext
|
2020-05-12 11:05:24 +00:00
|
|
|
const { data, fetch, mutations, ...state } = nuxt
|
2020-05-07 19:08:01 +00:00
|
|
|
|
|
|
|
// Initial state
|
2020-05-20 17:31:31 +00:00
|
|
|
const stateScript = `window.${this.serverContext.globals.context}=${devalue({
|
|
|
|
staticAssetsBase,
|
|
|
|
...state
|
|
|
|
})};`
|
2020-05-07 19:08:01 +00:00
|
|
|
|
|
|
|
// Make chunk for initial state > 10 KB
|
|
|
|
const stateScriptKb = (stateScript.length * 4 /* utf8 */) / 100
|
|
|
|
if (stateScriptKb > 10) {
|
|
|
|
const statePath = urlJoin(url, 'state.js')
|
2020-06-10 07:51:29 +00:00
|
|
|
const stateUrl = urlJoin(routerBase, staticAssetsBase, statePath)
|
2020-05-07 19:08:01 +00:00
|
|
|
staticAssets.push({ path: statePath, src: stateScript })
|
2020-06-10 07:51:29 +00:00
|
|
|
APP += `<script defer src="${stateUrl}"></script>`
|
2020-05-07 19:08:01 +00:00
|
|
|
preloadScripts.push(stateUrl)
|
|
|
|
} else {
|
2020-05-20 17:31:31 +00:00
|
|
|
APP += `<script>${stateScript}</script>`
|
2020-05-07 19:08:01 +00:00
|
|
|
}
|
2020-01-10 20:43:50 +00:00
|
|
|
|
2020-05-07 19:08:01 +00:00
|
|
|
// Page level payload.js (async loaded for CSR)
|
|
|
|
const payloadPath = urlJoin(url, 'payload.js')
|
2020-06-10 07:51:29 +00:00
|
|
|
const payloadUrl = urlJoin(routerBase, staticAssetsBase, payloadPath)
|
2020-05-08 16:10:06 +00:00
|
|
|
const routePath = (url.replace(/\/+$/, '') || '/').split('?')[0] // remove trailing slah and query params
|
2020-05-12 11:05:24 +00:00
|
|
|
const payloadScript = `__NUXT_JSONP__("${routePath}", ${devalue({ data, fetch, mutations })});`
|
2020-05-07 19:08:01 +00:00
|
|
|
staticAssets.push({ path: payloadPath, src: payloadScript })
|
|
|
|
preloadScripts.push(payloadUrl)
|
2020-01-10 20:43:50 +00:00
|
|
|
|
2020-05-07 19:08:01 +00:00
|
|
|
// Preload links
|
|
|
|
for (const href of preloadScripts) {
|
|
|
|
HEAD += `<link rel="preload" href="${href}" as="script">`
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Serialize state
|
|
|
|
let serializedSession
|
|
|
|
if (shouldInjectScripts || shouldHashCspScriptSrc) {
|
|
|
|
// Only serialized session if need inject scripts or csp hash
|
|
|
|
serializedSession = `window.${this.serverContext.globals.context}=${devalue(renderContext.nuxt)};`
|
|
|
|
inlineScripts.push(serializedSession)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (shouldInjectScripts) {
|
|
|
|
APP += `<script>${serializedSession}</script>`
|
|
|
|
}
|
2019-05-25 18:19:10 +00:00
|
|
|
}
|
2019-04-20 12:02:51 +00:00
|
|
|
|
|
|
|
// Calculate CSP hashes
|
|
|
|
const cspScriptSrcHashes = []
|
|
|
|
if (csp) {
|
2020-01-10 20:43:50 +00:00
|
|
|
if (shouldHashCspScriptSrc) {
|
2020-05-07 19:08:01 +00:00
|
|
|
for (const script of inlineScripts) {
|
|
|
|
const hash = crypto.createHash(csp.hashAlgorithm)
|
|
|
|
hash.update(script)
|
|
|
|
cspScriptSrcHashes.push(`'${csp.hashAlgorithm}-${hash.digest('base64')}'`)
|
|
|
|
}
|
2019-04-20 12:02:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2019-05-25 18:19:10 +00:00
|
|
|
if (shouldInjectScripts) {
|
|
|
|
APP += this.renderScripts(renderContext)
|
|
|
|
}
|
2019-08-19 18:38:13 +00:00
|
|
|
|
2019-09-05 15:15:27 +00:00
|
|
|
if (meta) {
|
2020-06-10 15:26:50 +00:00
|
|
|
const appendInjectorOptions = { body: true }
|
|
|
|
|
2019-09-05 15:15:27 +00:00
|
|
|
// Append body scripts
|
2020-06-10 15:26:50 +00:00
|
|
|
APP += meta.meta.text(appendInjectorOptions)
|
|
|
|
APP += meta.link.text(appendInjectorOptions)
|
|
|
|
APP += meta.style.text(appendInjectorOptions)
|
|
|
|
APP += meta.script.text(appendInjectorOptions)
|
|
|
|
APP += meta.noscript.text(appendInjectorOptions)
|
2019-09-05 15:15:27 +00:00
|
|
|
}
|
2019-04-20 12:02:51 +00:00
|
|
|
|
|
|
|
// Template params
|
|
|
|
const templateParams = {
|
2020-06-10 15:26:50 +00:00
|
|
|
HTML_ATTRS: meta ? meta.htmlAttrs.text(renderContext.nuxt.serverRendered /* addSrrAttribute */) : '',
|
2019-09-05 15:15:27 +00:00
|
|
|
HEAD_ATTRS: meta ? meta.headAttrs.text() : '',
|
|
|
|
BODY_ATTRS: meta ? meta.bodyAttrs.text() : '',
|
2019-04-20 12:02:51 +00:00
|
|
|
HEAD,
|
|
|
|
APP,
|
|
|
|
ENV: this.options.env
|
|
|
|
}
|
|
|
|
|
|
|
|
// Call ssr:templateParams hook
|
2020-04-02 09:28:57 +00:00
|
|
|
await this.serverContext.nuxt.callHook('vue-renderer:ssr:templateParams', templateParams, renderContext)
|
2019-04-20 12:02:51 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|