/*!
|
* @nuxt/vue-renderer v2.15.8 (c) 2016-2021
|
* Released under the MIT License
|
* Repository: https://github.com/nuxt/nuxt.js
|
* Website: https://nuxtjs.org
|
*/
|
'use strict';
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
const path = require('path');
|
const fs = require('fs-extra');
|
const consola = require('consola');
|
const lodash = require('lodash');
|
const utils = require('@nuxt/utils');
|
const ufo = require('ufo');
|
const defu = require('defu');
|
const VueMeta = require('vue-meta');
|
const vueServerRenderer = require('vue-server-renderer');
|
const LRU = require('lru-cache');
|
const devalue = require('@nuxt/devalue');
|
const crypto = require('crypto');
|
const util = require('util');
|
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
const path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
const fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
|
const consola__default = /*#__PURE__*/_interopDefaultLegacy(consola);
|
const defu__default = /*#__PURE__*/_interopDefaultLegacy(defu);
|
const VueMeta__default = /*#__PURE__*/_interopDefaultLegacy(VueMeta);
|
const LRU__default = /*#__PURE__*/_interopDefaultLegacy(LRU);
|
const devalue__default = /*#__PURE__*/_interopDefaultLegacy(devalue);
|
const crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto);
|
|
class BaseRenderer {
|
constructor (serverContext) {
|
this.serverContext = serverContext;
|
this.options = serverContext.options;
|
|
this.vueRenderer = this.createRenderer();
|
}
|
|
createRenderer () {
|
throw new Error('`createRenderer()` needs to be implemented')
|
}
|
|
renderTemplate (templateFn, opts) {
|
// Fix problem with HTMLPlugin's minify option (#3392)
|
opts.html_attrs = opts.HTML_ATTRS;
|
opts.head_attrs = opts.HEAD_ATTRS;
|
opts.body_attrs = opts.BODY_ATTRS;
|
|
return templateFn(opts)
|
}
|
|
render () {
|
throw new Error('`render()` needs to be implemented')
|
}
|
}
|
|
class SPARenderer extends BaseRenderer {
|
constructor (serverContext) {
|
super(serverContext);
|
|
this.cache = new LRU__default['default']();
|
|
this.vueMetaConfig = {
|
ssrAppId: '1',
|
...this.options.vueMeta,
|
keyName: 'head',
|
attribute: 'data-n-head',
|
ssrAttribute: 'data-n-head-ssr',
|
tagIDKeyName: 'hid'
|
};
|
}
|
|
createRenderer () {
|
return vueServerRenderer.createRenderer()
|
}
|
|
async render (renderContext) {
|
const { url = '/', req = {} } = renderContext;
|
const modernMode = this.options.modern;
|
const modern = (modernMode && this.options.target === utils.TARGETS.static) || utils.isModernRequest(req, modernMode);
|
const cacheKey = `${modern ? 'modern:' : 'legacy:'}${url}`;
|
let meta = this.cache.get(cacheKey);
|
|
if (meta) {
|
// Return a copy of the content, so that future
|
// modifications do not effect the data in cache
|
return lodash.cloneDeep(meta)
|
}
|
|
meta = {
|
HTML_ATTRS: '',
|
HEAD_ATTRS: '',
|
BODY_ATTRS: '',
|
HEAD: '',
|
BODY_SCRIPTS_PREPEND: '',
|
BODY_SCRIPTS: ''
|
};
|
|
if (this.options.features.meta) {
|
// Get vue-meta context
|
renderContext.head = typeof this.options.head === 'function'
|
? this.options.head()
|
: lodash.cloneDeep(this.options.head);
|
}
|
|
// Allow overriding renderContext
|
await this.serverContext.nuxt.callHook('vue-renderer:spa:prepareContext', renderContext);
|
|
if (this.options.features.meta) {
|
const m = VueMeta__default['default'].generate(renderContext.head || {}, this.vueMetaConfig);
|
|
// HTML_ATTRS
|
meta.HTML_ATTRS = m.htmlAttrs.text();
|
|
// HEAD_ATTRS
|
meta.HEAD_ATTRS = m.headAttrs.text();
|
|
// BODY_ATTRS
|
meta.BODY_ATTRS = m.bodyAttrs.text();
|
|
// HEAD tags
|
meta.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) {
|
meta.HEAD += `<base href="${this.options.router.base}">`;
|
}
|
|
// BODY_SCRIPTS (PREPEND)
|
meta.BODY_SCRIPTS_PREPEND =
|
m.meta.text({ pbody: true }) +
|
m.link.text({ pbody: true }) +
|
m.style.text({ pbody: true }) +
|
m.script.text({ pbody: true }) +
|
m.noscript.text({ pbody: true });
|
|
// BODY_SCRIPTS (APPEND)
|
meta.BODY_SCRIPTS =
|
m.meta.text({ body: true }) +
|
m.link.text({ body: true }) +
|
m.style.text({ body: true }) +
|
m.script.text({ body: true }) +
|
m.noscript.text({ body: true });
|
}
|
|
// Resources Hints
|
meta.resourceHints = '';
|
|
const { resources: { modernManifest, clientManifest } } = this.serverContext;
|
const manifest = modern ? modernManifest : clientManifest;
|
|
const { shouldPreload, shouldPrefetch } = this.options.render.bundleRenderer;
|
|
if (this.options.render.resourceHints && manifest) {
|
const publicPath = manifest.publicPath || '/_nuxt/';
|
|
// Preload initial resources
|
if (Array.isArray(manifest.initial)) {
|
const { crossorigin } = this.options.render;
|
const cors = `${crossorigin ? ` crossorigin="${crossorigin}"` : ''}`;
|
|
meta.preloadFiles = manifest.initial
|
.map(SPARenderer.normalizeFile)
|
.filter(({ fileWithoutQuery, asType }) => shouldPreload(fileWithoutQuery, asType))
|
.map(file => ({ ...file, modern }));
|
|
meta.resourceHints += meta.preloadFiles
|
.map(({ file, extension, fileWithoutQuery, asType, modern }) => {
|
let extra = '';
|
if (asType === 'font') {
|
extra = ` type="font/${extension}"${cors ? '' : ' crossorigin'}`;
|
}
|
const rel = modern && asType === 'script' ? 'modulepreload' : 'preload';
|
return `<link rel="${rel}"${cors} href="${publicPath}${file}"${
|
asType !== '' ? ` as="${asType}"` : ''}${extra}>`
|
})
|
.join('');
|
}
|
|
// Prefetch async resources
|
if (Array.isArray(manifest.async)) {
|
meta.resourceHints += manifest.async
|
.map(SPARenderer.normalizeFile)
|
.filter(({ fileWithoutQuery, asType }) => shouldPrefetch(fileWithoutQuery, asType))
|
.map(({ file }) => `<link rel="prefetch" href="${publicPath}${file}">`)
|
.join('');
|
}
|
|
// Add them to HEAD
|
if (meta.resourceHints) {
|
meta.HEAD += meta.resourceHints;
|
}
|
}
|
|
// Serialize state (runtime config)
|
let APP = `${meta.BODY_SCRIPTS_PREPEND}<div id="${this.serverContext.globals.id}">${this.serverContext.resources.loadingHTML}</div>${meta.BODY_SCRIPTS}`;
|
|
const payload = {
|
config: renderContext.runtimeConfig.public
|
};
|
if (renderContext.staticAssetsBase) {
|
payload.staticAssetsBase = renderContext.staticAssetsBase;
|
}
|
APP += `<script>window.${this.serverContext.globals.context}=${devalue__default['default'](payload)}</script>`;
|
|
// Prepare template params
|
const templateParams = {
|
...meta,
|
APP,
|
ENV: this.options.env
|
};
|
|
// Call spa:templateParams hook
|
await this.serverContext.nuxt.callHook('vue-renderer:spa:templateParams', templateParams);
|
|
// Render with SPA template
|
const html = this.renderTemplate(this.serverContext.resources.spaTemplate, templateParams);
|
const content = {
|
html,
|
preloadFiles: meta.preloadFiles || []
|
};
|
|
// Set meta tags inside cache
|
this.cache.set(cacheKey, content);
|
|
// Return a copy of the content, so that future
|
// modifications do not effect the data in cache
|
return lodash.cloneDeep(content)
|
}
|
|
static normalizeFile (file) {
|
const withoutQuery = file.replace(/\?.*/, '');
|
const extension = path.extname(withoutQuery).slice(1);
|
return {
|
file,
|
extension,
|
fileWithoutQuery: withoutQuery,
|
asType: SPARenderer.getPreloadType(extension)
|
}
|
}
|
|
static getPreloadType (ext) {
|
if (ext === 'js') {
|
return 'script'
|
} else if (ext === 'css') {
|
return 'style'
|
} else if (/jpe?g|png|svg|gif|webp|ico|avif/.test(ext)) {
|
return 'image'
|
} else if (/woff2?|ttf|otf|eot/.test(ext)) {
|
return 'font'
|
} else {
|
return ''
|
}
|
}
|
}
|
|
class SSRRenderer extends BaseRenderer {
|
get rendererOptions () {
|
const hasModules = fs__default['default'].existsSync(path__default['default'].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
|
}
|
}
|
|
addAttrs (tags, referenceTag, referenceAttr) {
|
const reference = referenceTag ? `<${referenceTag}` : referenceAttr;
|
if (!reference) {
|
return tags
|
}
|
|
const { render: { crossorigin } } = this.options;
|
if (crossorigin) {
|
tags = tags.replace(
|
new RegExp(reference, 'g'),
|
`${reference} crossorigin="${crossorigin}"`
|
);
|
}
|
|
return tags
|
}
|
|
renderResourceHints (renderContext) {
|
return this.addAttrs(renderContext.renderResourceHints(), null, 'rel="preload"')
|
}
|
|
renderScripts (renderContext) {
|
let renderedScripts = this.addAttrs(renderContext.renderScripts(), 'script');
|
if (this.options.render.asyncScripts) {
|
renderedScripts = renderedScripts.replace(/defer>/g, 'defer async>');
|
}
|
return renderedScripts
|
}
|
|
renderStyles (renderContext) {
|
return this.addAttrs(renderContext.renderStyles(), 'link')
|
}
|
|
getPreloadFiles (renderContext) {
|
return renderContext.getPreloadFiles()
|
}
|
|
createRenderer () {
|
// Create bundle renderer for SSR
|
return vueServerRenderer.createBundleRenderer(
|
this.serverContext.resources.serverManifest,
|
this.rendererOptions
|
)
|
}
|
|
useSSRLog () {
|
if (!this.options.render.ssrLog) {
|
return
|
}
|
const logs = [];
|
const devReporter = {
|
log (logObj) {
|
logs.push({
|
...logObj,
|
args: logObj.args.map(arg => util.format(arg))
|
});
|
}
|
};
|
consola__default['default'].addReporter(devReporter);
|
|
return () => {
|
consola__default['default'].removeReporter(devReporter);
|
return logs
|
}
|
}
|
|
async render (renderContext) {
|
// Call ssr:context hook to extend context from modules
|
await this.serverContext.nuxt.callHook('vue-renderer:ssr:prepareContext', renderContext);
|
|
const getSSRLog = this.useSSRLog();
|
|
// Call Vue renderer renderToString
|
let APP = await this.vueRenderer.renderToString(renderContext);
|
|
if (typeof getSSRLog === 'function') {
|
renderContext.nuxt.logs = getSSRLog();
|
}
|
|
// Call ssr:context hook
|
await this.serverContext.nuxt.callHook('vue-renderer:ssr:context', renderContext);
|
|
// TODO: Remove in next major release (#4722)
|
await this.serverContext.nuxt.callHook('_render:context', renderContext.nuxt);
|
|
// Fallback to empty response
|
if (!renderContext.nuxt.serverRendered) {
|
APP = `<div id="${this.serverContext.globals.id}"></div>`;
|
}
|
|
// Perf: early returns if server target and redirected
|
if (renderContext.redirected && renderContext.target === utils.TARGETS.server) {
|
return {
|
html: APP,
|
error: renderContext.nuxt.error,
|
redirected: renderContext.redirected
|
}
|
}
|
|
let HEAD = '';
|
|
// Inject head meta
|
// (this is unset when features.meta is false in server template)
|
const meta = renderContext.meta && renderContext.meta.inject({
|
isSSR: renderContext.nuxt.serverRendered,
|
ln: this.options.dev
|
});
|
|
if (meta) {
|
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() +
|
meta.style.text() +
|
meta.script.text() +
|
meta.noscript.text();
|
}
|
|
// Check if we need to inject scripts and state
|
const shouldInjectScripts = this.options.render.injectScripts !== false;
|
|
// Inject resource hints
|
if (this.options.render.resourceHints && shouldInjectScripts) {
|
HEAD += this.renderResourceHints(renderContext);
|
}
|
|
// Inject styles
|
HEAD += this.renderStyles(renderContext);
|
|
if (meta) {
|
const prependInjectorOptions = { pbody: true };
|
|
const BODY_PREPEND =
|
meta.meta.text(prependInjectorOptions) +
|
meta.link.text(prependInjectorOptions) +
|
meta.style.text(prependInjectorOptions) +
|
meta.script.text(prependInjectorOptions) +
|
meta.noscript.text(prependInjectorOptions);
|
|
if (BODY_PREPEND) {
|
APP = `${BODY_PREPEND}${APP}`;
|
}
|
}
|
|
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);
|
const inlineScripts = [];
|
|
if (shouldInjectScripts && renderContext.staticAssetsBase) {
|
const preloadScripts = [];
|
renderContext.staticAssets = [];
|
const { staticAssetsBase, url, nuxt, staticAssets } = renderContext;
|
const { data, fetch, mutations, ...state } = nuxt;
|
|
// Initial state
|
const stateScript = `window.${this.serverContext.globals.context}=${devalue__default['default']({
|
staticAssetsBase,
|
...state
|
})};`;
|
|
// Make chunk for initial state > 10 KB
|
const stateScriptKb = (stateScript.length * 4 /* utf8 */) / 100;
|
if (stateScriptKb > 10) {
|
const statePath = utils.urlJoin(url, 'state.js');
|
const stateUrl = utils.urlJoin(staticAssetsBase, statePath);
|
staticAssets.push({ path: statePath, src: stateScript });
|
if (this.options.render.asyncScripts) {
|
APP += `<script defer async src="${stateUrl}"></script>`;
|
} else {
|
APP += `<script defer src="${stateUrl}"></script>`;
|
}
|
preloadScripts.push(stateUrl);
|
} else {
|
APP += `<script>${stateScript}</script>`;
|
}
|
|
// Save payload only if no error or redirection were made
|
if (!renderContext.nuxt.error && !renderContext.redirected) {
|
// Page level payload.js (async loaded for CSR)
|
const payloadPath = utils.urlJoin(url, 'payload.js');
|
const payloadUrl = utils.urlJoin(staticAssetsBase, payloadPath);
|
const routePath = ufo.withoutTrailingSlash(ufo.parsePath(url).pathname);
|
const payloadScript = `__NUXT_JSONP__("${routePath}", ${devalue__default['default']({ data, fetch, mutations })});`;
|
staticAssets.push({ path: payloadPath, src: payloadScript });
|
preloadScripts.push(payloadUrl);
|
// Add manifest preload
|
if (this.options.generate.manifest) {
|
const manifestUrl = utils.urlJoin(staticAssetsBase, 'manifest.js');
|
preloadScripts.push(manifestUrl);
|
}
|
}
|
|
// 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__default['default'](renderContext.nuxt)};`;
|
inlineScripts.push(serializedSession);
|
}
|
|
if (shouldInjectScripts) {
|
APP += `<script>${serializedSession}</script>`;
|
}
|
}
|
|
// Calculate CSP hashes
|
const cspScriptSrcHashes = [];
|
if (csp) {
|
if (shouldHashCspScriptSrc) {
|
for (const script of inlineScripts) {
|
const hash = crypto__default['default'].createHash(csp.hashAlgorithm);
|
hash.update(script);
|
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
|
if (shouldInjectScripts) {
|
APP += this.renderScripts(renderContext);
|
}
|
|
if (meta) {
|
const appendInjectorOptions = { body: true };
|
|
// Append body scripts
|
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);
|
}
|
|
// Template params
|
const templateParams = {
|
HTML_ATTRS: meta ? meta.htmlAttrs.text(renderContext.nuxt.serverRendered /* addSrrAttribute */) : '',
|
HEAD_ATTRS: meta ? meta.headAttrs.text() : '',
|
BODY_ATTRS: meta ? meta.bodyAttrs.text() : '',
|
HEAD,
|
APP,
|
ENV: this.options.env
|
};
|
|
// Call ssr:templateParams hook
|
await this.serverContext.nuxt.callHook('vue-renderer:ssr:templateParams', templateParams, renderContext);
|
|
// 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
|
}
|
}
|
}
|
|
class ModernRenderer extends SSRRenderer {
|
constructor (serverContext) {
|
super(serverContext);
|
|
const { build: { publicPath }, router: { base } } = this.options;
|
this.publicPath = utils.isUrl(publicPath) || ufo.isRelative(publicPath) ? publicPath : utils.urlJoin(base, publicPath);
|
}
|
|
get assetsMapping () {
|
if (this._assetsMapping) {
|
return this._assetsMapping
|
}
|
|
const { clientManifest, modernManifest } = this.serverContext.resources;
|
const legacyAssets = clientManifest.assetsMapping;
|
const modernAssets = modernManifest.assetsMapping;
|
const mapping = {};
|
|
Object.keys(legacyAssets).forEach((componentHash) => {
|
const modernComponentAssets = modernAssets[componentHash] || [];
|
legacyAssets[componentHash].forEach((legacyAssetName, index) => {
|
mapping[legacyAssetName] = modernComponentAssets[index];
|
});
|
});
|
delete clientManifest.assetsMapping;
|
delete modernManifest.assetsMapping;
|
this._assetsMapping = mapping;
|
|
return mapping
|
}
|
|
get isServerMode () {
|
return this.options.modern === 'server'
|
}
|
|
get rendererOptions () {
|
const rendererOptions = super.rendererOptions;
|
if (this.isServerMode) {
|
rendererOptions.clientManifest = this.serverContext.resources.modernManifest;
|
}
|
return rendererOptions
|
}
|
|
renderScripts (renderContext) {
|
const scripts = super.renderScripts(renderContext);
|
|
if (this.isServerMode) {
|
return scripts
|
}
|
|
const scriptPattern = /<script[^>]*?src="([^"]*?)" defer( async)?><\/script>/g;
|
|
const modernScripts = scripts.replace(scriptPattern, (scriptTag, jsFile) => {
|
const legacyJsFile = jsFile.replace(this.publicPath, '');
|
const modernJsFile = this.assetsMapping[legacyJsFile];
|
if (!modernJsFile) {
|
return scriptTag.replace('<script', `<script nomodule`)
|
}
|
const moduleTag = scriptTag
|
.replace('<script', `<script type="module"`)
|
.replace(legacyJsFile, modernJsFile);
|
const noModuleTag = scriptTag.replace('<script', `<script nomodule`);
|
|
return noModuleTag + moduleTag
|
});
|
|
const safariNoModuleFixScript = `<script>${utils.safariNoModuleFix}</script>`;
|
|
return safariNoModuleFixScript + modernScripts
|
}
|
|
getModernFiles (legacyFiles = []) {
|
const modernFiles = [];
|
|
for (const legacyJsFile of legacyFiles) {
|
const modernFile = { ...legacyJsFile, modern: true };
|
if (modernFile.asType === 'script') {
|
const file = this.assetsMapping[legacyJsFile.file];
|
modernFile.file = file;
|
modernFile.fileWithoutQuery = file.replace(/\?.*/, '');
|
}
|
modernFiles.push(modernFile);
|
}
|
|
return modernFiles
|
}
|
|
getPreloadFiles (renderContext) {
|
const preloadFiles = super.getPreloadFiles(renderContext);
|
// In eligible server modern mode, preloadFiles are modern bundles from modern renderer
|
return this.isServerMode ? preloadFiles : this.getModernFiles(preloadFiles)
|
}
|
|
renderResourceHints (renderContext) {
|
const resourceHints = super.renderResourceHints(renderContext);
|
if (this.isServerMode) {
|
return resourceHints
|
}
|
|
const linkPattern = /<link[^>]*?href="([^"]*?)"[^>]*?as="script"[^>]*?>/g;
|
|
return resourceHints.replace(linkPattern, (linkTag, jsFile) => {
|
const legacyJsFile = jsFile.replace(this.publicPath, '');
|
const modernJsFile = this.assetsMapping[legacyJsFile];
|
if (!modernJsFile) {
|
return ''
|
}
|
return linkTag
|
.replace('rel="preload"', `rel="modulepreload"`)
|
.replace(legacyJsFile, modernJsFile)
|
})
|
}
|
|
render (renderContext) {
|
if (this.isServerMode) {
|
renderContext.res.setHeader('Vary', 'User-Agent');
|
}
|
return super.render(renderContext)
|
}
|
}
|
|
class VueRenderer {
|
constructor (context) {
|
this.serverContext = context;
|
this.options = this.serverContext.options;
|
|
// Will be set by createRenderer
|
this.renderer = {
|
ssr: undefined,
|
modern: undefined,
|
spa: undefined
|
};
|
|
// Renderer runtime resources
|
Object.assign(this.serverContext.resources, {
|
clientManifest: undefined,
|
modernManifest: undefined,
|
serverManifest: undefined,
|
ssrTemplate: undefined,
|
spaTemplate: undefined,
|
errorTemplate: this.parseTemplate('Nuxt Internal Server Error')
|
});
|
|
// Default status
|
this._state = 'created';
|
this._error = null;
|
}
|
|
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
|
}
|
|
async _ready () {
|
// Resolve dist path
|
this.distPath = path__default['default'].resolve(this.options.buildDir, 'dist', 'server');
|
|
// -- Development mode --
|
if (this.options.dev) {
|
this.serverContext.nuxt.hook('build:resources', mfs => this.loadResources(mfs));
|
return
|
}
|
|
// -- Production mode --
|
|
// Try once to load SSR resources from fs
|
await this.loadResources(fs__default['default']);
|
|
// Without using `nuxt start` (programmatic, tests and generate)
|
if (!this.options._start) {
|
this.serverContext.nuxt.hook('build:resources', () => this.loadResources(fs__default['default']));
|
return
|
}
|
|
// Verify resources
|
if (this.options.modern && !this.isModernReady) {
|
throw new Error(
|
`No modern build files found in ${this.distPath}.\nUse either \`nuxt build --modern\` or \`modern\` option to build modern files.`
|
)
|
} else if (!this.isReady) {
|
throw new Error(
|
`No build files found in ${this.distPath}.\nUse either \`nuxt build\` or \`builder.build()\` or start nuxt in development mode.`
|
)
|
}
|
}
|
|
async loadResources (_fs) {
|
const updated = [];
|
|
const readResource = async (fileName, encoding) => {
|
try {
|
const fullPath = path__default['default'].resolve(this.distPath, fileName);
|
|
if (!await _fs.exists(fullPath)) {
|
return
|
}
|
const contents = await _fs.readFile(fullPath, encoding);
|
|
return contents
|
} catch (err) {
|
consola__default['default'].error('Unable to load resource:', fileName, err);
|
}
|
};
|
|
for (const resourceName in this.resourceMap) {
|
const { fileName, transform, encoding } = this.resourceMap[resourceName];
|
|
// Load resource
|
let resource = await readResource(fileName, encoding);
|
|
// Skip unavailable resources
|
if (!resource) {
|
continue
|
}
|
|
// Apply transforms
|
if (typeof transform === 'function') {
|
resource = await transform(resource, { readResource });
|
}
|
|
// Update resource
|
this.serverContext.resources[resourceName] = resource;
|
updated.push(resourceName);
|
}
|
|
// Load templates
|
await this.loadTemplates();
|
|
await this.serverContext.nuxt.callHook('render:resourcesLoaded', this.serverContext.resources);
|
|
// Detect if any resource updated
|
if (updated.length > 0) {
|
// Create new renderer
|
this.createRenderer();
|
}
|
}
|
|
async loadTemplates () {
|
// Reload error template
|
const errorTemplatePath = path__default['default'].resolve(this.options.buildDir, 'views/error.html');
|
|
if (await fs__default['default'].exists(errorTemplatePath)) {
|
const errorTemplate = await fs__default['default'].readFile(errorTemplatePath, 'utf8');
|
this.serverContext.resources.errorTemplate = this.parseTemplate(errorTemplate);
|
}
|
|
// Reload loading template
|
const loadingHTMLPath = path__default['default'].resolve(this.options.buildDir, 'loading.html');
|
|
if (await fs__default['default'].exists(loadingHTMLPath)) {
|
this.serverContext.resources.loadingHTML = await fs__default['default'].readFile(loadingHTMLPath, 'utf8');
|
this.serverContext.resources.loadingHTML = this.serverContext.resources.loadingHTML.replace(/\r|\n|[\t\s]{3,}/g, '');
|
} else {
|
this.serverContext.resources.loadingHTML = '';
|
}
|
}
|
|
// TODO: Remove in Nuxt 3
|
get noSSR () { /* Backward compatibility */
|
return this.options.render.ssr === false
|
}
|
|
get SSR () {
|
return this.options.render.ssr === true
|
}
|
|
get isReady () {
|
// SPA
|
if (!this.serverContext.resources.spaTemplate || !this.renderer.spa) {
|
return false
|
}
|
|
// SSR
|
if (this.SSR && (!this.serverContext.resources.ssrTemplate || !this.renderer.ssr)) {
|
return false
|
}
|
|
return true
|
}
|
|
get isModernReady () {
|
return this.isReady && this.serverContext.resources.modernManifest
|
}
|
|
// TODO: Remove in Nuxt 3
|
get isResourcesAvailable () { /* Backward compatibility */
|
return this.isReady
|
}
|
|
detectModernBuild () {
|
const { options, resources } = this.serverContext;
|
if ([false, 'client', 'server'].includes(options.modern)) {
|
return
|
}
|
|
const isExplicitStaticModern = options.target === utils.TARGETS.static && options.modern;
|
if (!resources.modernManifest && !isExplicitStaticModern) {
|
options.modern = false;
|
return
|
}
|
|
options.modern = options.render.ssr ? 'server' : 'client';
|
consola__default['default'].info(`Modern bundles are detected. Modern mode (\`${options.modern}\`) is enabled now.`);
|
}
|
|
createRenderer () {
|
// Resource clientManifest is always required
|
if (!this.serverContext.resources.clientManifest) {
|
return
|
}
|
|
this.detectModernBuild();
|
|
// Create SPA renderer
|
if (this.serverContext.resources.spaTemplate) {
|
this.renderer.spa = new SPARenderer(this.serverContext);
|
}
|
|
// Skip the rest if SSR resources are not available
|
if (this.serverContext.resources.ssrTemplate && this.serverContext.resources.serverManifest) {
|
// Create bundle renderer for SSR
|
this.renderer.ssr = new SSRRenderer(this.serverContext);
|
|
if (this.options.modern !== false) {
|
this.renderer.modern = new ModernRenderer(this.serverContext);
|
}
|
}
|
}
|
|
renderSPA (renderContext) {
|
return this.renderer.spa.render(renderContext)
|
}
|
|
renderSSR (renderContext) {
|
// Call renderToString from the bundleRenderer and generate the HTML (will update the renderContext as well)
|
const renderer = renderContext.modern ? this.renderer.modern : this.renderer.ssr;
|
return renderer.render(renderContext)
|
}
|
|
async renderRoute (url, renderContext = {}, _retried = 0) {
|
/* istanbul ignore if */
|
if (!this.isReady) {
|
// Fall-back to loading-screen if enabled
|
if (this.options.build.loadingScreen) {
|
// Tell nuxt middleware to use `server:nuxt:renderLoading hook
|
return false
|
}
|
|
// Retry
|
const retryLimit = this.options.dev ? 60 : 3;
|
if (_retried < retryLimit && this._state !== 'error') {
|
await this.ready().then(() => utils.waitFor(1000));
|
return this.renderRoute(url, renderContext, _retried + 1)
|
}
|
|
// Throw Error
|
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 resources are not loaded! Please check possible console errors and ensure dist (${this.distPath}) exists.`)
|
default:
|
throw new Error('Renderer is in unknown state!')
|
}
|
}
|
|
// Log rendered url
|
consola__default['default'].debug(`Rendering url ${url}`);
|
|
// Add url to the renderContext
|
renderContext.url = ufo.normalizeURL(url);
|
|
// Add target to the renderContext
|
renderContext.target = this.options.target;
|
|
const { req = {}, res = {} } = renderContext;
|
|
// renderContext.spa
|
if (renderContext.spa === undefined) {
|
// TODO: Remove reading from renderContext.res in Nuxt3
|
renderContext.spa = !this.SSR || req.spa || res.spa;
|
}
|
|
// renderContext.modern
|
if (renderContext.modern === undefined) {
|
const modernMode = this.options.modern;
|
renderContext.modern = modernMode === 'client' || utils.isModernRequest(req, modernMode);
|
}
|
|
// Set runtime config on renderContext
|
renderContext.runtimeConfig = {
|
private: renderContext.spa ? {} : defu__default['default'](this.options.privateRuntimeConfig, this.options.publicRuntimeConfig),
|
public: { ...this.options.publicRuntimeConfig }
|
};
|
|
// Call renderContext hook
|
await this.serverContext.nuxt.callHook('vue-renderer:context', renderContext);
|
|
// Render SPA or SSR
|
return renderContext.spa
|
? this.renderSPA(renderContext)
|
: this.renderSSR(renderContext)
|
}
|
|
get resourceMap () {
|
const publicPath = utils.urlJoin(this.options.app.cdnURL, this.options.app.assetsPath);
|
return {
|
clientManifest: {
|
fileName: 'client.manifest.json',
|
transform: src => Object.assign(JSON.parse(src), { publicPath })
|
},
|
modernManifest: {
|
fileName: 'modern.manifest.json',
|
transform: src => Object.assign(JSON.parse(src), { publicPath })
|
},
|
serverManifest: {
|
fileName: 'server.manifest.json',
|
// BundleRenderer needs resolved contents
|
transform: async (src, { readResource }) => {
|
const serverManifest = JSON.parse(src);
|
|
const readResources = async (obj) => {
|
const _obj = {};
|
await Promise.all(Object.keys(obj).map(async (key) => {
|
_obj[key] = await readResource(obj[key]);
|
}));
|
return _obj
|
};
|
|
const [files, maps] = await Promise.all([
|
readResources(serverManifest.files),
|
readResources(serverManifest.maps)
|
]);
|
|
// 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: '' };
|
}
|
}
|
|
return {
|
...serverManifest,
|
files,
|
maps
|
}
|
}
|
},
|
ssrTemplate: {
|
fileName: 'index.ssr.html',
|
transform: src => this.parseTemplate(src)
|
},
|
spaTemplate: {
|
fileName: 'index.spa.html',
|
transform: src => this.parseTemplate(src)
|
}
|
}
|
}
|
|
parseTemplate (templateStr) {
|
return lodash.template(templateStr, {
|
interpolate: /{{([\s\S]+?)}}/g,
|
evaluate: /{%([\s\S]+?)%}/g
|
})
|
}
|
|
close () {
|
if (this.__closed) {
|
return
|
}
|
this.__closed = true;
|
|
for (const key in this.renderer) {
|
delete this.renderer[key];
|
}
|
}
|
}
|
|
exports.VueRenderer = VueRenderer;
|