2018-10-30 20:42:53 +00:00
|
|
|
import path from 'path'
|
|
|
|
import crypto from 'crypto'
|
2019-03-08 12:20:03 +00:00
|
|
|
import fs from 'fs-extra'
|
2018-10-30 20:42:53 +00:00
|
|
|
import consola from 'consola'
|
2018-12-20 11:15:12 +00:00
|
|
|
import devalue from '@nuxt/devalue'
|
2018-11-07 23:37:06 +00:00
|
|
|
import invert from 'lodash/invert'
|
2018-10-31 15:52:35 +00:00
|
|
|
import template from 'lodash/template'
|
2019-03-20 09:17:53 +00:00
|
|
|
import { isUrl, urlJoin } from '@nuxt/utils'
|
2018-10-31 15:52:35 +00:00
|
|
|
import { createBundleRenderer } from 'vue-server-renderer'
|
2018-10-30 20:42:53 +00:00
|
|
|
|
|
|
|
import SPAMetaRenderer from './spa-meta'
|
|
|
|
|
|
|
|
export default class VueRenderer {
|
|
|
|
constructor(context) {
|
|
|
|
this.context = context
|
|
|
|
|
2019-03-12 15:52:15 +00:00
|
|
|
const { build: { publicPath }, router: { base } } = this.context.options
|
|
|
|
this.publicPath = isUrl(publicPath) ? publicPath : urlJoin(base, publicPath)
|
|
|
|
|
2018-10-30 20:42:53 +00:00
|
|
|
// Will be set by createRenderer
|
2018-10-31 15:52:35 +00:00
|
|
|
this.renderer = {
|
2018-12-01 10:13:28 +00:00
|
|
|
ssr: undefined,
|
|
|
|
modern: undefined,
|
|
|
|
spa: undefined
|
2018-10-31 15:52:35 +00:00
|
|
|
}
|
|
|
|
|
2018-10-30 20:42:53 +00:00
|
|
|
// Renderer runtime resources
|
|
|
|
Object.assign(this.context.resources, {
|
2018-12-01 10:13:28 +00:00
|
|
|
clientManifest: undefined,
|
|
|
|
modernManifest: undefined,
|
|
|
|
serverManifest: undefined,
|
|
|
|
ssrTemplate: undefined,
|
|
|
|
spaTemplate: undefined,
|
|
|
|
errorTemplate: this.parseTemplate('Nuxt.js Internal Server Error')
|
2018-10-30 20:42:53 +00:00
|
|
|
})
|
2019-03-23 07:03:08 +00:00
|
|
|
|
|
|
|
// Default status
|
|
|
|
this._state = 'created'
|
|
|
|
this._error = null
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
|
|
|
|
2018-11-07 23:37:06 +00:00
|
|
|
get assetsMapping() {
|
2018-12-09 22:00:48 +00:00
|
|
|
if (this._assetsMapping) {
|
|
|
|
return this._assetsMapping
|
|
|
|
}
|
2018-11-07 23:37:06 +00:00
|
|
|
|
|
|
|
const legacyAssets = this.context.resources.clientManifest.assetsMapping
|
|
|
|
const modernAssets = invert(this.context.resources.modernManifest.assetsMapping)
|
|
|
|
const mapping = {}
|
2019-03-20 09:17:53 +00:00
|
|
|
|
2018-11-07 23:37:06 +00:00
|
|
|
for (const legacyJsFile in legacyAssets) {
|
|
|
|
const chunkNamesHash = legacyAssets[legacyJsFile]
|
|
|
|
mapping[legacyJsFile] = modernAssets[chunkNamesHash]
|
|
|
|
}
|
|
|
|
delete this.context.resources.clientManifest.assetsMapping
|
|
|
|
delete this.context.resources.modernManifest.assetsMapping
|
|
|
|
this._assetsMapping = mapping
|
2019-03-20 09:17:53 +00:00
|
|
|
|
2018-11-07 23:37:06 +00:00
|
|
|
return mapping
|
|
|
|
}
|
|
|
|
|
|
|
|
renderScripts(context) {
|
|
|
|
if (this.context.options.modern === 'client') {
|
2018-11-26 12:09:30 +00:00
|
|
|
const scriptPattern = /<script[^>]*?src="([^"]*?)"[^>]*?>[^<]*?<\/script>/g
|
2019-03-20 09:17:53 +00:00
|
|
|
|
2018-11-07 23:37:06 +00:00
|
|
|
return context.renderScripts().replace(scriptPattern, (scriptTag, jsFile) => {
|
2019-03-12 15:52:15 +00:00
|
|
|
const legacyJsFile = jsFile.replace(this.publicPath, '')
|
2018-11-07 23:37:06 +00:00
|
|
|
const modernJsFile = this.assetsMapping[legacyJsFile]
|
2019-03-12 15:52:15 +00:00
|
|
|
const { build: { crossorigin } } = this.context.options
|
2018-12-05 16:21:58 +00:00
|
|
|
const cors = `${crossorigin ? ` crossorigin="${crossorigin}"` : ''}`
|
2019-01-10 11:17:12 +00:00
|
|
|
const moduleTag = modernJsFile
|
|
|
|
? scriptTag
|
|
|
|
.replace('<script', `<script type="module"${cors}`)
|
|
|
|
.replace(legacyJsFile, modernJsFile)
|
|
|
|
: ''
|
2018-12-05 16:21:58 +00:00
|
|
|
const noModuleTag = scriptTag.replace('<script', `<script nomodule${cors}`)
|
2019-03-20 09:17:53 +00:00
|
|
|
|
2018-11-07 23:37:06 +00:00
|
|
|
return noModuleTag + moduleTag
|
|
|
|
})
|
|
|
|
}
|
2019-03-20 09:17:53 +00:00
|
|
|
|
2018-11-07 23:37:06 +00:00
|
|
|
return context.renderScripts()
|
|
|
|
}
|
|
|
|
|
2018-11-26 12:09:30 +00:00
|
|
|
getModernFiles(legacyFiles = []) {
|
|
|
|
const modernFiles = []
|
2019-03-20 09:17:53 +00:00
|
|
|
|
2018-11-26 12:09:30 +00:00
|
|
|
for (const legacyJsFile of legacyFiles) {
|
2018-12-05 16:21:58 +00:00
|
|
|
const modernFile = { ...legacyJsFile, modern: true }
|
2018-11-26 12:09:30 +00:00
|
|
|
if (modernFile.asType === 'script') {
|
|
|
|
const file = this.assetsMapping[legacyJsFile.file]
|
|
|
|
modernFile.file = file
|
|
|
|
modernFile.fileWithoutQuery = file.replace(/\?.*/, '')
|
|
|
|
}
|
|
|
|
modernFiles.push(modernFile)
|
|
|
|
}
|
2019-03-20 09:17:53 +00:00
|
|
|
|
2018-11-26 12:09:30 +00:00
|
|
|
return modernFiles
|
|
|
|
}
|
|
|
|
|
2019-03-03 07:52:59 +00:00
|
|
|
getSsrPreloadFiles(context) {
|
2018-11-26 12:09:30 +00:00
|
|
|
const preloadFiles = context.getPreloadFiles()
|
2019-03-20 09:17:53 +00:00
|
|
|
|
2018-11-26 12:09:30 +00:00
|
|
|
// In eligible server modern mode, preloadFiles are modern bundles from modern renderer
|
2019-03-03 07:52:59 +00:00
|
|
|
return this.context.options.modern === 'client' ? this.getModernFiles(preloadFiles) : preloadFiles
|
2018-11-26 12:09:30 +00:00
|
|
|
}
|
|
|
|
|
2019-03-03 07:52:59 +00:00
|
|
|
renderSsrResourceHints(context) {
|
2018-11-07 23:37:06 +00:00
|
|
|
if (this.context.options.modern === 'client') {
|
2018-11-26 12:09:30 +00:00
|
|
|
const linkPattern = /<link[^>]*?href="([^"]*?)"[^>]*?as="script"[^>]*?>/g
|
2019-03-20 09:17:53 +00:00
|
|
|
|
2018-11-26 12:09:30 +00:00
|
|
|
return context.renderResourceHints().replace(linkPattern, (linkTag, jsFile) => {
|
2019-03-12 15:52:15 +00:00
|
|
|
const legacyJsFile = jsFile.replace(this.publicPath, '')
|
2018-11-26 12:09:30 +00:00
|
|
|
const modernJsFile = this.assetsMapping[legacyJsFile]
|
2019-01-10 11:17:12 +00:00
|
|
|
if (!modernJsFile) {
|
|
|
|
return ''
|
|
|
|
}
|
2019-03-12 15:52:15 +00:00
|
|
|
const { crossorigin } = this.context.options.build
|
2018-12-05 16:21:58 +00:00
|
|
|
const cors = `${crossorigin ? ` crossorigin="${crossorigin}"` : ''}`
|
|
|
|
return linkTag.replace('rel="preload"', `rel="modulepreload"${cors}`).replace(legacyJsFile, modernJsFile)
|
2018-11-26 12:09:30 +00:00
|
|
|
})
|
2018-11-07 23:37:06 +00:00
|
|
|
}
|
2019-03-20 09:17:53 +00:00
|
|
|
|
2018-11-07 23:37:06 +00:00
|
|
|
return context.renderResourceHints()
|
|
|
|
}
|
|
|
|
|
2019-03-23 07:03:08 +00:00
|
|
|
ready() {
|
|
|
|
if (!this._readyPromise) {
|
|
|
|
this._state = 'loading'
|
|
|
|
this._readyPromise = this._ready()
|
|
|
|
.then(() => {
|
|
|
|
this._state = 'ready'
|
|
|
|
return this
|
|
|
|
})
|
|
|
|
.catch((error) => {
|
|
|
|
this._state = 'error'
|
|
|
|
this._error = error
|
|
|
|
throw error
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return this._readyPromise
|
|
|
|
}
|
2019-03-08 20:43:23 +00:00
|
|
|
|
2019-03-23 07:03:08 +00:00
|
|
|
async _ready() {
|
2019-03-20 09:17:53 +00:00
|
|
|
// Resolve dist path
|
|
|
|
this.distPath = path.resolve(this.context.options.buildDir, 'dist', 'server')
|
|
|
|
|
2018-12-10 12:16:05 +00:00
|
|
|
// -- Development mode --
|
|
|
|
if (this.context.options.dev) {
|
2019-03-08 12:20:03 +00:00
|
|
|
this.context.nuxt.hook('build:resources', mfs => this.loadResources(mfs))
|
2018-12-10 12:16:05 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// -- Production mode --
|
|
|
|
|
|
|
|
// Try once to load SSR resources from fs
|
|
|
|
await this.loadResources(fs)
|
|
|
|
|
2019-03-23 07:02:55 +00:00
|
|
|
// Without using `nuxt start` (programmatic, tests and generate)
|
2018-12-10 12:16:05 +00:00
|
|
|
if (!this.context.options._start) {
|
|
|
|
this.context.nuxt.hook('build:resources', () => this.loadResources(fs))
|
2019-02-08 16:25:11 +00:00
|
|
|
return
|
2018-12-10 12:16:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Verify resources
|
2019-03-20 09:17:53 +00:00
|
|
|
if (this.context.options.modern && !this.isModernReady) {
|
2019-02-08 16:25:11 +00:00
|
|
|
throw new Error(
|
2019-03-20 09:17:53 +00:00
|
|
|
`No modern build files found in ${this.distPath}.\nUse either \`nuxt build --modern\` or \`modern\` option to build modern files.`
|
2019-02-08 16:25:11 +00:00
|
|
|
)
|
2019-03-20 09:17:53 +00:00
|
|
|
} else if (!this.isReady) {
|
2019-02-08 16:25:11 +00:00
|
|
|
throw new Error(
|
2019-03-20 09:17:53 +00:00
|
|
|
`No build files found in ${this.distPath}.\nUse either \`nuxt build\` or \`builder.build()\` or start nuxt in development mode.`
|
2019-02-08 16:25:11 +00:00
|
|
|
)
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-08 12:20:03 +00:00
|
|
|
async loadResources(_fs) {
|
2018-10-30 20:42:53 +00:00
|
|
|
const updated = []
|
|
|
|
|
2019-03-08 12:20:03 +00:00
|
|
|
const readResource = async (fileName, encoding) => {
|
2018-12-01 10:13:28 +00:00
|
|
|
try {
|
2019-03-20 09:17:53 +00:00
|
|
|
const fullPath = path.resolve(this.distPath, fileName)
|
|
|
|
|
2019-03-08 12:20:03 +00:00
|
|
|
if (!await _fs.exists(fullPath)) {
|
2018-12-01 10:13:28 +00:00
|
|
|
return
|
|
|
|
}
|
2019-03-08 12:20:03 +00:00
|
|
|
const contents = await _fs.readFile(fullPath, encoding)
|
2019-03-20 09:17:53 +00:00
|
|
|
|
2018-12-01 10:13:28 +00:00
|
|
|
return contents
|
|
|
|
} catch (err) {
|
|
|
|
consola.error('Unable to load resource:', fileName, err)
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
2018-12-01 10:13:28 +00:00
|
|
|
}
|
2018-11-08 09:15:56 +00:00
|
|
|
|
2019-03-08 12:20:03 +00:00
|
|
|
for (const resourceName in this.resourceMap) {
|
|
|
|
const { fileName, transform, encoding } = this.resourceMap[resourceName]
|
2018-12-01 10:13:28 +00:00
|
|
|
|
|
|
|
// Load resource
|
2019-03-08 12:20:03 +00:00
|
|
|
let resource = await readResource(fileName, encoding)
|
2018-12-01 10:13:28 +00:00
|
|
|
|
|
|
|
// Skip unavailable resources
|
|
|
|
if (!resource) {
|
|
|
|
continue
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
2018-12-01 10:13:28 +00:00
|
|
|
|
|
|
|
// Apply transforms
|
|
|
|
if (typeof transform === 'function') {
|
2019-03-08 12:20:03 +00:00
|
|
|
resource = await transform(resource, { readResource })
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
2018-12-01 10:13:28 +00:00
|
|
|
|
|
|
|
// Update resource
|
|
|
|
this.context.resources[resourceName] = resource
|
|
|
|
updated.push(resourceName)
|
|
|
|
}
|
2018-10-30 20:42:53 +00:00
|
|
|
|
2019-03-08 12:20:03 +00:00
|
|
|
// Load templates
|
|
|
|
await this.loadTemplates()
|
|
|
|
|
|
|
|
// Detect if any resource updated
|
|
|
|
if (updated.length > 0) {
|
|
|
|
// Invalidate assetsMapping cache
|
|
|
|
delete this._assetsMapping
|
|
|
|
|
|
|
|
// Create new renderer
|
|
|
|
this.createRenderer()
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.context.nuxt.callHook('render:resourcesLoaded', this.context.resources)
|
|
|
|
}
|
|
|
|
|
|
|
|
async loadTemplates() {
|
2018-10-30 20:42:53 +00:00
|
|
|
// Reload error template
|
|
|
|
const errorTemplatePath = path.resolve(this.context.options.buildDir, 'views/error.html')
|
2019-03-20 09:17:53 +00:00
|
|
|
|
2019-03-08 12:20:03 +00:00
|
|
|
if (await fs.exists(errorTemplatePath)) {
|
|
|
|
const errorTemplate = await fs.readFile(errorTemplatePath, 'utf8')
|
|
|
|
this.context.resources.errorTemplate = this.parseTemplate(errorTemplate)
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
|
|
|
|
2018-12-01 10:13:28 +00:00
|
|
|
// Reload loading template
|
2018-10-30 20:42:53 +00:00
|
|
|
const loadingHTMLPath = path.resolve(this.context.options.buildDir, 'loading.html')
|
2019-03-20 09:17:53 +00:00
|
|
|
|
2019-03-08 12:20:03 +00:00
|
|
|
if (await fs.exists(loadingHTMLPath)) {
|
|
|
|
this.context.resources.loadingHTML = await fs.readFile(loadingHTMLPath, 'utf8')
|
|
|
|
this.context.resources.loadingHTML = this.context.resources.loadingHTML.replace(/\r|\n|[\t\s]{3,}/g, '')
|
2018-10-30 20:42:53 +00:00
|
|
|
} else {
|
|
|
|
this.context.resources.loadingHTML = ''
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-08 16:25:11 +00:00
|
|
|
// TODO: Remove in Nuxt 3
|
2018-12-01 10:13:28 +00:00
|
|
|
get noSSR() { /* Backward compatibility */
|
2018-10-30 20:42:53 +00:00
|
|
|
return this.context.options.render.ssr === false
|
|
|
|
}
|
|
|
|
|
2018-12-01 10:13:28 +00:00
|
|
|
get SSR() {
|
|
|
|
return this.context.options.render.ssr === true
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
|
|
|
|
2018-12-01 10:13:28 +00:00
|
|
|
get isReady() {
|
|
|
|
// SPA
|
|
|
|
if (!this.context.resources.spaTemplate || !this.renderer.spa) {
|
2018-10-30 20:42:53 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-12-01 10:13:28 +00:00
|
|
|
// SSR
|
|
|
|
if (this.SSR && (!this.context.resources.ssrTemplate || !this.renderer.ssr)) {
|
|
|
|
return false
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
|
|
|
|
2018-12-01 10:13:28 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2019-03-20 09:17:53 +00:00
|
|
|
get isModernReady() {
|
|
|
|
return this.isReady && this.context.resources.modernManifest
|
|
|
|
}
|
|
|
|
|
2019-02-08 16:25:11 +00:00
|
|
|
// TODO: Remove in Nuxt 3
|
2018-12-01 10:13:28 +00:00
|
|
|
get isResourcesAvailable() { /* Backward compatibility */
|
|
|
|
return this.isReady
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
createRenderer() {
|
2018-12-01 10:13:28 +00:00
|
|
|
// Resource clientManifest is always required
|
|
|
|
if (!this.context.resources.clientManifest) {
|
2018-10-30 20:42:53 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-12-01 10:13:28 +00:00
|
|
|
// Create SPA renderer
|
|
|
|
if (this.context.resources.spaTemplate) {
|
|
|
|
this.renderer.spa = new SPAMetaRenderer(this)
|
|
|
|
}
|
2018-10-30 20:42:53 +00:00
|
|
|
|
2018-12-01 10:13:28 +00:00
|
|
|
// Skip the rest if SSR resources are not available
|
|
|
|
if (!this.context.resources.ssrTemplate || !this.context.resources.serverManifest) {
|
2018-10-30 20:42:53 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const hasModules = fs.existsSync(path.resolve(this.context.options.rootDir, 'node_modules'))
|
2018-12-01 10:13:28 +00:00
|
|
|
|
2018-10-31 15:52:35 +00:00
|
|
|
const rendererOptions = {
|
|
|
|
clientManifest: this.context.resources.clientManifest,
|
|
|
|
// for globally installed nuxt command, search dependencies in global dir
|
|
|
|
basedir: hasModules ? this.context.options.rootDir : __dirname,
|
|
|
|
...this.context.options.render.bundleRenderer
|
|
|
|
}
|
|
|
|
|
2018-10-30 20:42:53 +00:00
|
|
|
// Create bundle renderer for SSR
|
2018-10-31 15:52:35 +00:00
|
|
|
this.renderer.ssr = createBundleRenderer(
|
2018-12-01 10:13:28 +00:00
|
|
|
this.context.resources.serverManifest,
|
2018-10-31 15:52:35 +00:00
|
|
|
rendererOptions
|
|
|
|
)
|
|
|
|
|
2018-11-26 22:49:47 +00:00
|
|
|
if (this.context.resources.modernManifest &&
|
|
|
|
!['client', false].includes(this.context.options.modern)) {
|
2018-10-31 15:52:35 +00:00
|
|
|
this.renderer.modern = createBundleRenderer(
|
2018-12-01 10:13:28 +00:00
|
|
|
this.context.resources.serverManifest,
|
2018-10-30 20:42:53 +00:00
|
|
|
{
|
2018-10-31 15:52:35 +00:00
|
|
|
...rendererOptions,
|
|
|
|
clientManifest: this.context.resources.modernManifest
|
|
|
|
}
|
2018-10-30 20:42:53 +00:00
|
|
|
)
|
2018-10-31 15:52:35 +00:00
|
|
|
}
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
renderTemplate(ssr, opts) {
|
|
|
|
// Fix problem with HTMLPlugin's minify option (#3392)
|
|
|
|
opts.html_attrs = opts.HTML_ATTRS
|
2018-12-14 14:06:27 +00:00
|
|
|
opts.head_attrs = opts.HEAD_ATTRS
|
2018-10-30 20:42:53 +00:00
|
|
|
opts.body_attrs = opts.BODY_ATTRS
|
|
|
|
|
2019-02-08 16:25:11 +00:00
|
|
|
const templateFn = ssr ? this.context.resources.ssrTemplate : this.context.resources.spaTemplate
|
2018-10-30 20:42:53 +00:00
|
|
|
|
2019-02-08 16:25:11 +00:00
|
|
|
return templateFn(opts)
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
|
|
|
|
2019-02-08 10:05:01 +00:00
|
|
|
async renderSPA(context) {
|
|
|
|
const content = await this.renderer.spa.render(context)
|
|
|
|
|
2019-02-08 16:25:11 +00:00
|
|
|
const APP = `<div id="${this.context.globals.id}">${this.context.resources.loadingHTML}</div>${content.BODY_SCRIPTS}`
|
2019-02-08 10:05:01 +00:00
|
|
|
|
|
|
|
// Prepare template params
|
|
|
|
const templateParams = {
|
|
|
|
...content,
|
|
|
|
APP,
|
|
|
|
ENV: this.context.options.env
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
|
|
|
|
2019-02-08 10:05:01 +00:00
|
|
|
// Call spa:templateParams hook
|
|
|
|
this.context.nuxt.callHook('vue-renderer:spa:templateParams', templateParams)
|
2018-10-30 20:42:53 +00:00
|
|
|
|
2019-02-08 10:05:01 +00:00
|
|
|
// Render with SPA template
|
|
|
|
const html = this.renderTemplate(false, templateParams)
|
2018-10-30 20:42:53 +00:00
|
|
|
|
2019-02-08 10:05:01 +00:00
|
|
|
return {
|
|
|
|
html,
|
2019-03-03 07:52:59 +00:00
|
|
|
getPreloadFiles: content.getPreloadFiles
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
2019-02-08 10:05:01 +00:00
|
|
|
}
|
2018-10-30 20:42:53 +00:00
|
|
|
|
2019-02-08 10:05:01 +00:00
|
|
|
async renderSSR(context) {
|
2018-10-30 20:42:53 +00:00
|
|
|
// Call renderToString from the bundleRenderer and generate the HTML (will update the context as well)
|
2019-02-08 10:05:01 +00:00
|
|
|
const renderer = context.modern ? this.renderer.modern : this.renderer.ssr
|
|
|
|
|
|
|
|
// Call ssr:context hook to extend context from modules
|
|
|
|
await this.context.nuxt.callHook('vue-renderer:ssr:prepareContext', context)
|
2018-10-30 20:42:53 +00:00
|
|
|
|
2019-02-08 10:05:01 +00:00
|
|
|
// Call Vue renderer renderToString
|
|
|
|
let APP = await renderer.renderToString(context)
|
|
|
|
|
|
|
|
// Call ssr:context hook
|
|
|
|
await this.context.nuxt.callHook('vue-renderer:ssr:context', context)
|
|
|
|
// TODO: Remove in next major release
|
|
|
|
await this.context.nuxt.callHook('render:routeContext', context.nuxt)
|
|
|
|
|
|
|
|
// Fallback to empty response
|
2018-10-30 20:42:53 +00:00
|
|
|
if (!context.nuxt.serverRendered) {
|
|
|
|
APP = `<div id="${this.context.globals.id}"></div>`
|
|
|
|
}
|
2019-02-08 10:05:01 +00:00
|
|
|
|
|
|
|
// Inject head meta
|
2018-10-30 20:42:53 +00:00
|
|
|
const m = context.meta.inject()
|
|
|
|
let HEAD =
|
|
|
|
m.title.text() +
|
|
|
|
m.meta.text() +
|
|
|
|
m.link.text() +
|
|
|
|
m.style.text() +
|
|
|
|
m.script.text() +
|
|
|
|
m.noscript.text()
|
2019-02-08 10:05:01 +00:00
|
|
|
|
|
|
|
// Add <base href=""> meta if router base specified
|
2018-10-30 20:42:53 +00:00
|
|
|
if (this.context.options._routerBaseSpecified) {
|
|
|
|
HEAD += `<base href="${this.context.options.router.base}">`
|
|
|
|
}
|
|
|
|
|
2019-02-08 10:05:01 +00:00
|
|
|
// Inject resource hints
|
2018-10-30 20:42:53 +00:00
|
|
|
if (this.context.options.render.resourceHints) {
|
2019-03-03 07:52:59 +00:00
|
|
|
HEAD += this.renderSsrResourceHints(context)
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
|
|
|
|
2019-02-08 10:05:01 +00:00
|
|
|
// Inject styles
|
|
|
|
HEAD += context.renderStyles()
|
2018-10-30 20:42:53 +00:00
|
|
|
|
2019-02-08 10:05:01 +00:00
|
|
|
// Serialize state
|
2018-10-30 20:42:53 +00:00
|
|
|
const serializedSession = `window.${this.context.globals.context}=${devalue(context.nuxt)};`
|
2019-02-08 10:05:01 +00:00
|
|
|
APP += `<script>${serializedSession}</script>`
|
2018-10-30 20:42:53 +00:00
|
|
|
|
2019-02-08 10:05:01 +00:00
|
|
|
// Calculate CSP hashes
|
2018-12-12 06:29:28 +00:00
|
|
|
const cspScriptSrcHashes = []
|
2019-03-29 16:09:53 +00:00
|
|
|
const csp = this.context.options.render.csp
|
|
|
|
const containsUnsafeInlineScriptSrc = csp && csp.policies && csp.policies['script-src'] && csp.policies['script-src'].includes(`'unsafe-inline'`)
|
|
|
|
|
|
|
|
// Only add the hash if 'unsafe-inline' rule isn't present to avoid conflicts (#5387)
|
|
|
|
if (csp && !containsUnsafeInlineScriptSrc) {
|
2018-10-30 20:42:53 +00:00
|
|
|
const { hashAlgorithm } = this.context.options.render.csp
|
|
|
|
const hash = crypto.createHash(hashAlgorithm)
|
|
|
|
hash.update(serializedSession)
|
2018-12-12 06:29:28 +00:00
|
|
|
cspScriptSrcHashes.push(`'${hashAlgorithm}-${hash.digest('base64')}'`)
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
|
|
|
|
2019-02-08 10:05:01 +00:00
|
|
|
// Call ssr:csp hook
|
|
|
|
await this.context.nuxt.callHook('vue-renderer:ssr:csp', cspScriptSrcHashes)
|
|
|
|
|
|
|
|
// Prepend scripts
|
2018-11-07 23:37:06 +00:00
|
|
|
APP += this.renderScripts(context)
|
2018-10-30 20:42:53 +00:00
|
|
|
APP += m.script.text({ body: true })
|
|
|
|
APP += m.noscript.text({ body: true })
|
|
|
|
|
2019-02-08 10:05:01 +00:00
|
|
|
// Template params
|
|
|
|
const templateParams = {
|
2018-10-30 20:42:53 +00:00
|
|
|
HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
|
2018-12-14 14:06:27 +00:00
|
|
|
HEAD_ATTRS: m.headAttrs.text(),
|
2018-10-30 20:42:53 +00:00
|
|
|
BODY_ATTRS: m.bodyAttrs.text(),
|
|
|
|
HEAD,
|
|
|
|
APP,
|
2019-02-08 10:05:01 +00:00
|
|
|
ENV: this.context.options.env
|
|
|
|
}
|
|
|
|
|
|
|
|
// Call ssr:templateParams hook
|
|
|
|
await this.context.nuxt.callHook('vue-renderer:ssr:templateParams', templateParams)
|
|
|
|
|
|
|
|
// Render with SSR template
|
|
|
|
const html = this.renderTemplate(true, templateParams)
|
2018-10-30 20:42:53 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
html,
|
2018-12-12 06:29:28 +00:00
|
|
|
cspScriptSrcHashes,
|
2019-03-03 07:52:59 +00:00
|
|
|
getPreloadFiles: this.getSsrPreloadFiles.bind(this, context),
|
2018-10-30 20:42:53 +00:00
|
|
|
error: context.nuxt.error,
|
|
|
|
redirected: context.redirected
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-20 09:17:53 +00:00
|
|
|
async renderRoute(url, context = {}) {
|
|
|
|
/* istanbul ignore if */
|
|
|
|
if (!this.isReady) {
|
2019-03-23 07:03:08 +00:00
|
|
|
// Production
|
2019-03-20 09:17:53 +00:00
|
|
|
if (!this.context.options.dev) {
|
2019-03-23 07:03:08 +00:00
|
|
|
switch (this._state) {
|
|
|
|
case 'created':
|
|
|
|
throw new Error('Renderer ready() is not called! Please ensure `nuxt.ready()` is called and awaited.')
|
|
|
|
case 'loading':
|
|
|
|
throw new Error(`Renderer is loading.`)
|
|
|
|
case 'error':
|
|
|
|
throw this._error
|
|
|
|
case 'ready':
|
|
|
|
throw new Error(`Renderer is loaded but not all resources are unavailable! Please check ${this.distPath} existence.`)
|
|
|
|
default:
|
|
|
|
throw new Error('Renderer is in unknown state!')
|
|
|
|
}
|
2019-02-08 10:05:01 +00:00
|
|
|
}
|
2019-03-20 09:17:53 +00:00
|
|
|
// Tell nuxt middleware to render UI
|
|
|
|
return false
|
2019-02-08 10:05:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Log rendered url
|
|
|
|
consola.debug(`Rendering url ${url}`)
|
|
|
|
|
|
|
|
// Add url to the context
|
|
|
|
context.url = url
|
|
|
|
|
2019-03-03 07:52:59 +00:00
|
|
|
const { req = {} } = context
|
|
|
|
|
2019-02-08 10:05:01 +00:00
|
|
|
// context.spa
|
|
|
|
if (context.spa === undefined) {
|
|
|
|
// TODO: Remove reading from context.res in Nuxt3
|
2019-03-05 17:07:05 +00:00
|
|
|
context.spa = !this.SSR || req.spa || (context.res && context.res.spa)
|
2019-02-08 10:05:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// context.modern
|
|
|
|
if (context.modern === undefined) {
|
2019-03-03 07:52:59 +00:00
|
|
|
context.modern = req.modernMode && this.context.options.modern === 'server'
|
2019-02-08 10:05:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Call context hook
|
|
|
|
await this.context.nuxt.callHook('vue-renderer:context', context)
|
|
|
|
|
|
|
|
// Render SPA or SSR
|
|
|
|
return context.spa
|
|
|
|
? this.renderSPA(context)
|
|
|
|
: this.renderSSR(context)
|
|
|
|
}
|
|
|
|
|
2018-12-01 10:13:28 +00:00
|
|
|
get resourceMap() {
|
|
|
|
return {
|
|
|
|
clientManifest: {
|
|
|
|
fileName: 'client.manifest.json',
|
|
|
|
transform: src => JSON.parse(src)
|
2018-10-30 20:42:53 +00:00
|
|
|
},
|
2018-12-01 10:13:28 +00:00
|
|
|
modernManifest: {
|
|
|
|
fileName: 'modern.manifest.json',
|
|
|
|
transform: src => JSON.parse(src)
|
2018-10-31 15:52:35 +00:00
|
|
|
},
|
2018-12-01 10:13:28 +00:00
|
|
|
serverManifest: {
|
|
|
|
fileName: 'server.manifest.json',
|
|
|
|
// BundleRenderer needs resolved contents
|
2019-03-08 12:20:03 +00:00
|
|
|
transform: async (src, { readResource }) => {
|
2018-12-01 10:13:28 +00:00
|
|
|
const serverManifest = JSON.parse(src)
|
|
|
|
|
2019-03-08 12:20:03 +00:00
|
|
|
const readResources = async (obj) => {
|
|
|
|
const _obj = {}
|
|
|
|
await Promise.all(Object.keys(obj).map(async (key) => {
|
|
|
|
_obj[key] = await readResource(obj[key])
|
|
|
|
}))
|
|
|
|
return _obj
|
2018-12-01 10:13:28 +00:00
|
|
|
}
|
|
|
|
|
2019-03-08 12:20:03 +00:00
|
|
|
const [files, maps] = await Promise.all([
|
|
|
|
readResources(serverManifest.files),
|
|
|
|
readResources(serverManifest.maps)
|
|
|
|
])
|
2018-12-01 10:51:10 +00:00
|
|
|
|
|
|
|
// Try to parse sourcemaps
|
|
|
|
for (const map in maps) {
|
|
|
|
if (maps[map] && maps[map].version) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
maps[map] = JSON.parse(maps[map])
|
|
|
|
} catch (e) {
|
|
|
|
maps[map] = { version: 3, sources: [], mappings: '' }
|
|
|
|
}
|
|
|
|
}
|
2018-12-01 10:13:28 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
...serverManifest,
|
|
|
|
files,
|
|
|
|
maps
|
|
|
|
}
|
|
|
|
}
|
2018-10-30 20:42:53 +00:00
|
|
|
},
|
2018-12-01 10:13:28 +00:00
|
|
|
ssrTemplate: {
|
2018-10-30 20:42:53 +00:00
|
|
|
fileName: 'index.ssr.html',
|
2018-12-01 10:13:28 +00:00
|
|
|
transform: src => this.parseTemplate(src)
|
2018-10-30 20:42:53 +00:00
|
|
|
},
|
2018-12-01 10:13:28 +00:00
|
|
|
spaTemplate: {
|
2018-10-30 20:42:53 +00:00
|
|
|
fileName: 'index.spa.html',
|
2018-12-01 10:13:28 +00:00
|
|
|
transform: src => this.parseTemplate(src)
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
2018-12-01 10:13:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parseTemplate(templateStr) {
|
|
|
|
return template(templateStr, {
|
|
|
|
interpolate: /{{([\s\S]+?)}}/g
|
|
|
|
})
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|
2018-12-09 10:42:22 +00:00
|
|
|
|
|
|
|
close() {
|
2018-12-09 22:00:48 +00:00
|
|
|
if (this.__closed) {
|
|
|
|
return
|
|
|
|
}
|
2018-12-09 10:42:22 +00:00
|
|
|
this.__closed = true
|
|
|
|
|
|
|
|
for (const key in this.renderer) {
|
|
|
|
delete this.renderer[key]
|
|
|
|
}
|
|
|
|
}
|
2018-10-30 20:42:53 +00:00
|
|
|
}
|