import path from 'path' import crypto from 'crypto' import devalue from '@nuxtjs/devalue' import template from 'lodash/template' import fs from 'fs-extra' import { createBundleRenderer } from 'vue-server-renderer' import consola from 'consola' import { waitFor } from '@nuxt/common' import SPAMetaRenderer from './spa-meta' export default class VueRenderer { constructor(context) { this.context = context // Will be set by createRenderer this.bundleRenderer = null this.spaMetaRenderer = null // Renderer runtime resources Object.assign(this.context.resources, { clientManifest: null, serverBundle: null, ssrTemplate: null, spaTemplate: null, errorTemplate: this.constructor.parseTemplate('Nuxt.js Internal Server Error') }) } async ready() { // Production: Load SSR resources from fs if (!this.context.options.dev) { await this.loadResources() } } async loadResources(_fs = fs) { const distPath = path.resolve(this.context.options.buildDir, 'dist', 'server') const updated = [] this.constructor.resourceMap.forEach(({ key, fileName, transform }) => { const rawKey = '$$' + key const _path = path.join(distPath, fileName) if (!_fs.existsSync(_path)) { return // Resource not exists } const rawData = _fs.readFileSync(_path, 'utf8') if (!rawData || rawData === this.context.resources[rawKey]) { return // No changes } this.context.resources[rawKey] = rawData const data = transform(rawData) /* istanbul ignore if */ if (!data) { return // Invalid data ? } this.context.resources[key] = data updated.push(key) }) // Reload error template const errorTemplatePath = path.resolve(this.context.options.buildDir, 'views/error.html') if (fs.existsSync(errorTemplatePath)) { this.context.resources.errorTemplate = this.constructor.parseTemplate( fs.readFileSync(errorTemplatePath, 'utf8') ) } // Load loading template const loadingHTMLPath = path.resolve(this.context.options.buildDir, 'loading.html') if (fs.existsSync(loadingHTMLPath)) { this.context.resources.loadingHTML = fs.readFileSync(loadingHTMLPath, 'utf8') this.context.resources.loadingHTML = this.context.resources.loadingHTML .replace(/\r|\n|[\t\s]{3,}/g, '') } else { this.context.resources.loadingHTML = '' } // Call resourcesLoaded plugin await this.context.nuxt.callHook('render:resourcesLoaded', this.context.resources) if (updated.length > 0) { this.createRenderer() } } get noSSR() { return this.context.options.render.ssr === false } get isReady() { if (this.noSSR) { return Boolean(this.context.resources.spaTemplate) } return Boolean(this.bundleRenderer && this.context.resources.ssrTemplate) } get isResourcesAvailable() { // Required for both /* istanbul ignore if */ if (!this.context.resources.clientManifest) { return false } // Required for SPA rendering if (this.noSSR) { return Boolean(this.context.resources.spaTemplate) } // Required for bundle renderer return Boolean(this.context.resources.ssrTemplate && this.context.resources.serverBundle) } createRenderer() { // Ensure resources are available if (!this.isResourcesAvailable) { return } // Create Meta Renderer this.spaMetaRenderer = new SPAMetaRenderer(this) // Skip following steps if noSSR mode if (this.noSSR) { return } const hasModules = fs.existsSync(path.resolve(this.context.options.rootDir, 'node_modules')) // Create bundle renderer for SSR this.bundleRenderer = createBundleRenderer( this.context.resources.serverBundle, Object.assign( { clientManifest: this.context.resources.clientManifest, runInNewContext: false, // for globally installed nuxt command, search dependencies in global dir basedir: hasModules ? this.context.options.rootDir : __dirname }, this.context.options.render.bundleRenderer ) ) } renderTemplate(ssr, opts) { // Fix problem with HTMLPlugin's minify option (#3392) opts.html_attrs = opts.HTML_ATTRS opts.body_attrs = opts.BODY_ATTRS const fn = ssr ? this.context.resources.ssrTemplate : this.context.resources.spaTemplate return fn(opts) } async renderRoute(url, context = {}) { /* istanbul ignore if */ if (!this.isReady) { await waitFor(1000) return this.renderRoute(url, context) } // Log rendered url consola.debug(`Rendering url ${url}`) // Add url and isSever to the context context.url = url // Basic response if SSR is disabled or spa data provided const spa = context.spa || (context.res && context.res.spa) const ENV = this.context.options.env if (this.noSSR || spa) { const { HTML_ATTRS, BODY_ATTRS, HEAD, BODY_SCRIPTS, getPreloadFiles } = await this.spaMetaRenderer.render(context) const APP = `