vue-renderer.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065
  1. /*!
  2. * @nuxt/vue-renderer v2.15.8 (c) 2016-2021
  3. * Released under the MIT License
  4. * Repository: https://github.com/nuxt/nuxt.js
  5. * Website: https://nuxtjs.org
  6. */
  7. 'use strict';
  8. Object.defineProperty(exports, '__esModule', { value: true });
  9. const path = require('path');
  10. const fs = require('fs-extra');
  11. const consola = require('consola');
  12. const lodash = require('lodash');
  13. const utils = require('@nuxt/utils');
  14. const ufo = require('ufo');
  15. const defu = require('defu');
  16. const VueMeta = require('vue-meta');
  17. const vueServerRenderer = require('vue-server-renderer');
  18. const LRU = require('lru-cache');
  19. const devalue = require('@nuxt/devalue');
  20. const crypto = require('crypto');
  21. const util = require('util');
  22. function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
  23. const path__default = /*#__PURE__*/_interopDefaultLegacy(path);
  24. const fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
  25. const consola__default = /*#__PURE__*/_interopDefaultLegacy(consola);
  26. const defu__default = /*#__PURE__*/_interopDefaultLegacy(defu);
  27. const VueMeta__default = /*#__PURE__*/_interopDefaultLegacy(VueMeta);
  28. const LRU__default = /*#__PURE__*/_interopDefaultLegacy(LRU);
  29. const devalue__default = /*#__PURE__*/_interopDefaultLegacy(devalue);
  30. const crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto);
  31. class BaseRenderer {
  32. constructor (serverContext) {
  33. this.serverContext = serverContext;
  34. this.options = serverContext.options;
  35. this.vueRenderer = this.createRenderer();
  36. }
  37. createRenderer () {
  38. throw new Error('`createRenderer()` needs to be implemented')
  39. }
  40. renderTemplate (templateFn, opts) {
  41. // Fix problem with HTMLPlugin's minify option (#3392)
  42. opts.html_attrs = opts.HTML_ATTRS;
  43. opts.head_attrs = opts.HEAD_ATTRS;
  44. opts.body_attrs = opts.BODY_ATTRS;
  45. return templateFn(opts)
  46. }
  47. render () {
  48. throw new Error('`render()` needs to be implemented')
  49. }
  50. }
  51. class SPARenderer extends BaseRenderer {
  52. constructor (serverContext) {
  53. super(serverContext);
  54. this.cache = new LRU__default['default']();
  55. this.vueMetaConfig = {
  56. ssrAppId: '1',
  57. ...this.options.vueMeta,
  58. keyName: 'head',
  59. attribute: 'data-n-head',
  60. ssrAttribute: 'data-n-head-ssr',
  61. tagIDKeyName: 'hid'
  62. };
  63. }
  64. createRenderer () {
  65. return vueServerRenderer.createRenderer()
  66. }
  67. async render (renderContext) {
  68. const { url = '/', req = {} } = renderContext;
  69. const modernMode = this.options.modern;
  70. const modern = (modernMode && this.options.target === utils.TARGETS.static) || utils.isModernRequest(req, modernMode);
  71. const cacheKey = `${modern ? 'modern:' : 'legacy:'}${url}`;
  72. let meta = this.cache.get(cacheKey);
  73. if (meta) {
  74. // Return a copy of the content, so that future
  75. // modifications do not effect the data in cache
  76. return lodash.cloneDeep(meta)
  77. }
  78. meta = {
  79. HTML_ATTRS: '',
  80. HEAD_ATTRS: '',
  81. BODY_ATTRS: '',
  82. HEAD: '',
  83. BODY_SCRIPTS_PREPEND: '',
  84. BODY_SCRIPTS: ''
  85. };
  86. if (this.options.features.meta) {
  87. // Get vue-meta context
  88. renderContext.head = typeof this.options.head === 'function'
  89. ? this.options.head()
  90. : lodash.cloneDeep(this.options.head);
  91. }
  92. // Allow overriding renderContext
  93. await this.serverContext.nuxt.callHook('vue-renderer:spa:prepareContext', renderContext);
  94. if (this.options.features.meta) {
  95. const m = VueMeta__default['default'].generate(renderContext.head || {}, this.vueMetaConfig);
  96. // HTML_ATTRS
  97. meta.HTML_ATTRS = m.htmlAttrs.text();
  98. // HEAD_ATTRS
  99. meta.HEAD_ATTRS = m.headAttrs.text();
  100. // BODY_ATTRS
  101. meta.BODY_ATTRS = m.bodyAttrs.text();
  102. // HEAD tags
  103. meta.HEAD =
  104. m.title.text() +
  105. m.meta.text() +
  106. m.link.text() +
  107. m.style.text() +
  108. m.script.text() +
  109. m.noscript.text();
  110. // Add <base href=""> meta if router base specified
  111. if (this.options._routerBaseSpecified) {
  112. meta.HEAD += `<base href="${this.options.router.base}">`;
  113. }
  114. // BODY_SCRIPTS (PREPEND)
  115. meta.BODY_SCRIPTS_PREPEND =
  116. m.meta.text({ pbody: true }) +
  117. m.link.text({ pbody: true }) +
  118. m.style.text({ pbody: true }) +
  119. m.script.text({ pbody: true }) +
  120. m.noscript.text({ pbody: true });
  121. // BODY_SCRIPTS (APPEND)
  122. meta.BODY_SCRIPTS =
  123. m.meta.text({ body: true }) +
  124. m.link.text({ body: true }) +
  125. m.style.text({ body: true }) +
  126. m.script.text({ body: true }) +
  127. m.noscript.text({ body: true });
  128. }
  129. // Resources Hints
  130. meta.resourceHints = '';
  131. const { resources: { modernManifest, clientManifest } } = this.serverContext;
  132. const manifest = modern ? modernManifest : clientManifest;
  133. const { shouldPreload, shouldPrefetch } = this.options.render.bundleRenderer;
  134. if (this.options.render.resourceHints && manifest) {
  135. const publicPath = manifest.publicPath || '/_nuxt/';
  136. // Preload initial resources
  137. if (Array.isArray(manifest.initial)) {
  138. const { crossorigin } = this.options.render;
  139. const cors = `${crossorigin ? ` crossorigin="${crossorigin}"` : ''}`;
  140. meta.preloadFiles = manifest.initial
  141. .map(SPARenderer.normalizeFile)
  142. .filter(({ fileWithoutQuery, asType }) => shouldPreload(fileWithoutQuery, asType))
  143. .map(file => ({ ...file, modern }));
  144. meta.resourceHints += meta.preloadFiles
  145. .map(({ file, extension, fileWithoutQuery, asType, modern }) => {
  146. let extra = '';
  147. if (asType === 'font') {
  148. extra = ` type="font/${extension}"${cors ? '' : ' crossorigin'}`;
  149. }
  150. const rel = modern && asType === 'script' ? 'modulepreload' : 'preload';
  151. return `<link rel="${rel}"${cors} href="${publicPath}${file}"${
  152. asType !== '' ? ` as="${asType}"` : ''}${extra}>`
  153. })
  154. .join('');
  155. }
  156. // Prefetch async resources
  157. if (Array.isArray(manifest.async)) {
  158. meta.resourceHints += manifest.async
  159. .map(SPARenderer.normalizeFile)
  160. .filter(({ fileWithoutQuery, asType }) => shouldPrefetch(fileWithoutQuery, asType))
  161. .map(({ file }) => `<link rel="prefetch" href="${publicPath}${file}">`)
  162. .join('');
  163. }
  164. // Add them to HEAD
  165. if (meta.resourceHints) {
  166. meta.HEAD += meta.resourceHints;
  167. }
  168. }
  169. // Serialize state (runtime config)
  170. let APP = `${meta.BODY_SCRIPTS_PREPEND}<div id="${this.serverContext.globals.id}">${this.serverContext.resources.loadingHTML}</div>${meta.BODY_SCRIPTS}`;
  171. const payload = {
  172. config: renderContext.runtimeConfig.public
  173. };
  174. if (renderContext.staticAssetsBase) {
  175. payload.staticAssetsBase = renderContext.staticAssetsBase;
  176. }
  177. APP += `<script>window.${this.serverContext.globals.context}=${devalue__default['default'](payload)}</script>`;
  178. // Prepare template params
  179. const templateParams = {
  180. ...meta,
  181. APP,
  182. ENV: this.options.env
  183. };
  184. // Call spa:templateParams hook
  185. await this.serverContext.nuxt.callHook('vue-renderer:spa:templateParams', templateParams);
  186. // Render with SPA template
  187. const html = this.renderTemplate(this.serverContext.resources.spaTemplate, templateParams);
  188. const content = {
  189. html,
  190. preloadFiles: meta.preloadFiles || []
  191. };
  192. // Set meta tags inside cache
  193. this.cache.set(cacheKey, content);
  194. // Return a copy of the content, so that future
  195. // modifications do not effect the data in cache
  196. return lodash.cloneDeep(content)
  197. }
  198. static normalizeFile (file) {
  199. const withoutQuery = file.replace(/\?.*/, '');
  200. const extension = path.extname(withoutQuery).slice(1);
  201. return {
  202. file,
  203. extension,
  204. fileWithoutQuery: withoutQuery,
  205. asType: SPARenderer.getPreloadType(extension)
  206. }
  207. }
  208. static getPreloadType (ext) {
  209. if (ext === 'js') {
  210. return 'script'
  211. } else if (ext === 'css') {
  212. return 'style'
  213. } else if (/jpe?g|png|svg|gif|webp|ico|avif/.test(ext)) {
  214. return 'image'
  215. } else if (/woff2?|ttf|otf|eot/.test(ext)) {
  216. return 'font'
  217. } else {
  218. return ''
  219. }
  220. }
  221. }
  222. class SSRRenderer extends BaseRenderer {
  223. get rendererOptions () {
  224. const hasModules = fs__default['default'].existsSync(path__default['default'].resolve(this.options.rootDir, 'node_modules'));
  225. return {
  226. clientManifest: this.serverContext.resources.clientManifest,
  227. // for globally installed nuxt command, search dependencies in global dir
  228. basedir: hasModules ? this.options.rootDir : __dirname,
  229. ...this.options.render.bundleRenderer
  230. }
  231. }
  232. addAttrs (tags, referenceTag, referenceAttr) {
  233. const reference = referenceTag ? `<${referenceTag}` : referenceAttr;
  234. if (!reference) {
  235. return tags
  236. }
  237. const { render: { crossorigin } } = this.options;
  238. if (crossorigin) {
  239. tags = tags.replace(
  240. new RegExp(reference, 'g'),
  241. `${reference} crossorigin="${crossorigin}"`
  242. );
  243. }
  244. return tags
  245. }
  246. renderResourceHints (renderContext) {
  247. return this.addAttrs(renderContext.renderResourceHints(), null, 'rel="preload"')
  248. }
  249. renderScripts (renderContext) {
  250. let renderedScripts = this.addAttrs(renderContext.renderScripts(), 'script');
  251. if (this.options.render.asyncScripts) {
  252. renderedScripts = renderedScripts.replace(/defer>/g, 'defer async>');
  253. }
  254. return renderedScripts
  255. }
  256. renderStyles (renderContext) {
  257. return this.addAttrs(renderContext.renderStyles(), 'link')
  258. }
  259. getPreloadFiles (renderContext) {
  260. return renderContext.getPreloadFiles()
  261. }
  262. createRenderer () {
  263. // Create bundle renderer for SSR
  264. return vueServerRenderer.createBundleRenderer(
  265. this.serverContext.resources.serverManifest,
  266. this.rendererOptions
  267. )
  268. }
  269. useSSRLog () {
  270. if (!this.options.render.ssrLog) {
  271. return
  272. }
  273. const logs = [];
  274. const devReporter = {
  275. log (logObj) {
  276. logs.push({
  277. ...logObj,
  278. args: logObj.args.map(arg => util.format(arg))
  279. });
  280. }
  281. };
  282. consola__default['default'].addReporter(devReporter);
  283. return () => {
  284. consola__default['default'].removeReporter(devReporter);
  285. return logs
  286. }
  287. }
  288. async render (renderContext) {
  289. // Call ssr:context hook to extend context from modules
  290. await this.serverContext.nuxt.callHook('vue-renderer:ssr:prepareContext', renderContext);
  291. const getSSRLog = this.useSSRLog();
  292. // Call Vue renderer renderToString
  293. let APP = await this.vueRenderer.renderToString(renderContext);
  294. if (typeof getSSRLog === 'function') {
  295. renderContext.nuxt.logs = getSSRLog();
  296. }
  297. // Call ssr:context hook
  298. await this.serverContext.nuxt.callHook('vue-renderer:ssr:context', renderContext);
  299. // TODO: Remove in next major release (#4722)
  300. await this.serverContext.nuxt.callHook('_render:context', renderContext.nuxt);
  301. // Fallback to empty response
  302. if (!renderContext.nuxt.serverRendered) {
  303. APP = `<div id="${this.serverContext.globals.id}"></div>`;
  304. }
  305. // Perf: early returns if server target and redirected
  306. if (renderContext.redirected && renderContext.target === utils.TARGETS.server) {
  307. return {
  308. html: APP,
  309. error: renderContext.nuxt.error,
  310. redirected: renderContext.redirected
  311. }
  312. }
  313. let HEAD = '';
  314. // Inject head meta
  315. // (this is unset when features.meta is false in server template)
  316. const meta = renderContext.meta && renderContext.meta.inject({
  317. isSSR: renderContext.nuxt.serverRendered,
  318. ln: this.options.dev
  319. });
  320. if (meta) {
  321. HEAD += meta.title.text() + meta.meta.text();
  322. }
  323. // Add <base href=""> meta if router base specified
  324. if (this.options._routerBaseSpecified) {
  325. HEAD += `<base href="${this.options.router.base}">`;
  326. }
  327. if (meta) {
  328. HEAD += meta.link.text() +
  329. meta.style.text() +
  330. meta.script.text() +
  331. meta.noscript.text();
  332. }
  333. // Check if we need to inject scripts and state
  334. const shouldInjectScripts = this.options.render.injectScripts !== false;
  335. // Inject resource hints
  336. if (this.options.render.resourceHints && shouldInjectScripts) {
  337. HEAD += this.renderResourceHints(renderContext);
  338. }
  339. // Inject styles
  340. HEAD += this.renderStyles(renderContext);
  341. if (meta) {
  342. const prependInjectorOptions = { pbody: true };
  343. const BODY_PREPEND =
  344. meta.meta.text(prependInjectorOptions) +
  345. meta.link.text(prependInjectorOptions) +
  346. meta.style.text(prependInjectorOptions) +
  347. meta.script.text(prependInjectorOptions) +
  348. meta.noscript.text(prependInjectorOptions);
  349. if (BODY_PREPEND) {
  350. APP = `${BODY_PREPEND}${APP}`;
  351. }
  352. }
  353. const { csp } = this.options.render;
  354. // Only add the hash if 'unsafe-inline' rule isn't present to avoid conflicts (#5387)
  355. const containsUnsafeInlineScriptSrc = csp.policies && csp.policies['script-src'] && csp.policies['script-src'].includes('\'unsafe-inline\'');
  356. const shouldHashCspScriptSrc = csp && (csp.unsafeInlineCompatibility || !containsUnsafeInlineScriptSrc);
  357. const inlineScripts = [];
  358. if (shouldInjectScripts && renderContext.staticAssetsBase) {
  359. const preloadScripts = [];
  360. renderContext.staticAssets = [];
  361. const { staticAssetsBase, url, nuxt, staticAssets } = renderContext;
  362. const { data, fetch, mutations, ...state } = nuxt;
  363. // Initial state
  364. const stateScript = `window.${this.serverContext.globals.context}=${devalue__default['default']({
  365. staticAssetsBase,
  366. ...state
  367. })};`;
  368. // Make chunk for initial state > 10 KB
  369. const stateScriptKb = (stateScript.length * 4 /* utf8 */) / 100;
  370. if (stateScriptKb > 10) {
  371. const statePath = utils.urlJoin(url, 'state.js');
  372. const stateUrl = utils.urlJoin(staticAssetsBase, statePath);
  373. staticAssets.push({ path: statePath, src: stateScript });
  374. if (this.options.render.asyncScripts) {
  375. APP += `<script defer async src="${stateUrl}"></script>`;
  376. } else {
  377. APP += `<script defer src="${stateUrl}"></script>`;
  378. }
  379. preloadScripts.push(stateUrl);
  380. } else {
  381. APP += `<script>${stateScript}</script>`;
  382. }
  383. // Save payload only if no error or redirection were made
  384. if (!renderContext.nuxt.error && !renderContext.redirected) {
  385. // Page level payload.js (async loaded for CSR)
  386. const payloadPath = utils.urlJoin(url, 'payload.js');
  387. const payloadUrl = utils.urlJoin(staticAssetsBase, payloadPath);
  388. const routePath = ufo.withoutTrailingSlash(ufo.parsePath(url).pathname);
  389. const payloadScript = `__NUXT_JSONP__("${routePath}", ${devalue__default['default']({ data, fetch, mutations })});`;
  390. staticAssets.push({ path: payloadPath, src: payloadScript });
  391. preloadScripts.push(payloadUrl);
  392. // Add manifest preload
  393. if (this.options.generate.manifest) {
  394. const manifestUrl = utils.urlJoin(staticAssetsBase, 'manifest.js');
  395. preloadScripts.push(manifestUrl);
  396. }
  397. }
  398. // Preload links
  399. for (const href of preloadScripts) {
  400. HEAD += `<link rel="preload" href="${href}" as="script">`;
  401. }
  402. } else {
  403. // Serialize state
  404. let serializedSession;
  405. if (shouldInjectScripts || shouldHashCspScriptSrc) {
  406. // Only serialized session if need inject scripts or csp hash
  407. serializedSession = `window.${this.serverContext.globals.context}=${devalue__default['default'](renderContext.nuxt)};`;
  408. inlineScripts.push(serializedSession);
  409. }
  410. if (shouldInjectScripts) {
  411. APP += `<script>${serializedSession}</script>`;
  412. }
  413. }
  414. // Calculate CSP hashes
  415. const cspScriptSrcHashes = [];
  416. if (csp) {
  417. if (shouldHashCspScriptSrc) {
  418. for (const script of inlineScripts) {
  419. const hash = crypto__default['default'].createHash(csp.hashAlgorithm);
  420. hash.update(script);
  421. cspScriptSrcHashes.push(`'${csp.hashAlgorithm}-${hash.digest('base64')}'`);
  422. }
  423. }
  424. // Call ssr:csp hook
  425. await this.serverContext.nuxt.callHook('vue-renderer:ssr:csp', cspScriptSrcHashes);
  426. // Add csp meta tags
  427. if (csp.addMeta) {
  428. HEAD += `<meta http-equiv="Content-Security-Policy" content="script-src ${cspScriptSrcHashes.join()}">`;
  429. }
  430. }
  431. // Prepend scripts
  432. if (shouldInjectScripts) {
  433. APP += this.renderScripts(renderContext);
  434. }
  435. if (meta) {
  436. const appendInjectorOptions = { body: true };
  437. // Append body scripts
  438. APP += meta.meta.text(appendInjectorOptions);
  439. APP += meta.link.text(appendInjectorOptions);
  440. APP += meta.style.text(appendInjectorOptions);
  441. APP += meta.script.text(appendInjectorOptions);
  442. APP += meta.noscript.text(appendInjectorOptions);
  443. }
  444. // Template params
  445. const templateParams = {
  446. HTML_ATTRS: meta ? meta.htmlAttrs.text(renderContext.nuxt.serverRendered /* addSrrAttribute */) : '',
  447. HEAD_ATTRS: meta ? meta.headAttrs.text() : '',
  448. BODY_ATTRS: meta ? meta.bodyAttrs.text() : '',
  449. HEAD,
  450. APP,
  451. ENV: this.options.env
  452. };
  453. // Call ssr:templateParams hook
  454. await this.serverContext.nuxt.callHook('vue-renderer:ssr:templateParams', templateParams, renderContext);
  455. // Render with SSR template
  456. const html = this.renderTemplate(this.serverContext.resources.ssrTemplate, templateParams);
  457. let preloadFiles;
  458. if (this.options.render.http2.push) {
  459. preloadFiles = this.getPreloadFiles(renderContext);
  460. }
  461. return {
  462. html,
  463. cspScriptSrcHashes,
  464. preloadFiles,
  465. error: renderContext.nuxt.error,
  466. redirected: renderContext.redirected
  467. }
  468. }
  469. }
  470. class ModernRenderer extends SSRRenderer {
  471. constructor (serverContext) {
  472. super(serverContext);
  473. const { build: { publicPath }, router: { base } } = this.options;
  474. this.publicPath = utils.isUrl(publicPath) || ufo.isRelative(publicPath) ? publicPath : utils.urlJoin(base, publicPath);
  475. }
  476. get assetsMapping () {
  477. if (this._assetsMapping) {
  478. return this._assetsMapping
  479. }
  480. const { clientManifest, modernManifest } = this.serverContext.resources;
  481. const legacyAssets = clientManifest.assetsMapping;
  482. const modernAssets = modernManifest.assetsMapping;
  483. const mapping = {};
  484. Object.keys(legacyAssets).forEach((componentHash) => {
  485. const modernComponentAssets = modernAssets[componentHash] || [];
  486. legacyAssets[componentHash].forEach((legacyAssetName, index) => {
  487. mapping[legacyAssetName] = modernComponentAssets[index];
  488. });
  489. });
  490. delete clientManifest.assetsMapping;
  491. delete modernManifest.assetsMapping;
  492. this._assetsMapping = mapping;
  493. return mapping
  494. }
  495. get isServerMode () {
  496. return this.options.modern === 'server'
  497. }
  498. get rendererOptions () {
  499. const rendererOptions = super.rendererOptions;
  500. if (this.isServerMode) {
  501. rendererOptions.clientManifest = this.serverContext.resources.modernManifest;
  502. }
  503. return rendererOptions
  504. }
  505. renderScripts (renderContext) {
  506. const scripts = super.renderScripts(renderContext);
  507. if (this.isServerMode) {
  508. return scripts
  509. }
  510. const scriptPattern = /<script[^>]*?src="([^"]*?)" defer( async)?><\/script>/g;
  511. const modernScripts = scripts.replace(scriptPattern, (scriptTag, jsFile) => {
  512. const legacyJsFile = jsFile.replace(this.publicPath, '');
  513. const modernJsFile = this.assetsMapping[legacyJsFile];
  514. if (!modernJsFile) {
  515. return scriptTag.replace('<script', `<script nomodule`)
  516. }
  517. const moduleTag = scriptTag
  518. .replace('<script', `<script type="module"`)
  519. .replace(legacyJsFile, modernJsFile);
  520. const noModuleTag = scriptTag.replace('<script', `<script nomodule`);
  521. return noModuleTag + moduleTag
  522. });
  523. const safariNoModuleFixScript = `<script>${utils.safariNoModuleFix}</script>`;
  524. return safariNoModuleFixScript + modernScripts
  525. }
  526. getModernFiles (legacyFiles = []) {
  527. const modernFiles = [];
  528. for (const legacyJsFile of legacyFiles) {
  529. const modernFile = { ...legacyJsFile, modern: true };
  530. if (modernFile.asType === 'script') {
  531. const file = this.assetsMapping[legacyJsFile.file];
  532. modernFile.file = file;
  533. modernFile.fileWithoutQuery = file.replace(/\?.*/, '');
  534. }
  535. modernFiles.push(modernFile);
  536. }
  537. return modernFiles
  538. }
  539. getPreloadFiles (renderContext) {
  540. const preloadFiles = super.getPreloadFiles(renderContext);
  541. // In eligible server modern mode, preloadFiles are modern bundles from modern renderer
  542. return this.isServerMode ? preloadFiles : this.getModernFiles(preloadFiles)
  543. }
  544. renderResourceHints (renderContext) {
  545. const resourceHints = super.renderResourceHints(renderContext);
  546. if (this.isServerMode) {
  547. return resourceHints
  548. }
  549. const linkPattern = /<link[^>]*?href="([^"]*?)"[^>]*?as="script"[^>]*?>/g;
  550. return resourceHints.replace(linkPattern, (linkTag, jsFile) => {
  551. const legacyJsFile = jsFile.replace(this.publicPath, '');
  552. const modernJsFile = this.assetsMapping[legacyJsFile];
  553. if (!modernJsFile) {
  554. return ''
  555. }
  556. return linkTag
  557. .replace('rel="preload"', `rel="modulepreload"`)
  558. .replace(legacyJsFile, modernJsFile)
  559. })
  560. }
  561. render (renderContext) {
  562. if (this.isServerMode) {
  563. renderContext.res.setHeader('Vary', 'User-Agent');
  564. }
  565. return super.render(renderContext)
  566. }
  567. }
  568. class VueRenderer {
  569. constructor (context) {
  570. this.serverContext = context;
  571. this.options = this.serverContext.options;
  572. // Will be set by createRenderer
  573. this.renderer = {
  574. ssr: undefined,
  575. modern: undefined,
  576. spa: undefined
  577. };
  578. // Renderer runtime resources
  579. Object.assign(this.serverContext.resources, {
  580. clientManifest: undefined,
  581. modernManifest: undefined,
  582. serverManifest: undefined,
  583. ssrTemplate: undefined,
  584. spaTemplate: undefined,
  585. errorTemplate: this.parseTemplate('Nuxt Internal Server Error')
  586. });
  587. // Default status
  588. this._state = 'created';
  589. this._error = null;
  590. }
  591. ready () {
  592. if (!this._readyPromise) {
  593. this._state = 'loading';
  594. this._readyPromise = this._ready()
  595. .then(() => {
  596. this._state = 'ready';
  597. return this
  598. })
  599. .catch((error) => {
  600. this._state = 'error';
  601. this._error = error;
  602. throw error
  603. });
  604. }
  605. return this._readyPromise
  606. }
  607. async _ready () {
  608. // Resolve dist path
  609. this.distPath = path__default['default'].resolve(this.options.buildDir, 'dist', 'server');
  610. // -- Development mode --
  611. if (this.options.dev) {
  612. this.serverContext.nuxt.hook('build:resources', mfs => this.loadResources(mfs));
  613. return
  614. }
  615. // -- Production mode --
  616. // Try once to load SSR resources from fs
  617. await this.loadResources(fs__default['default']);
  618. // Without using `nuxt start` (programmatic, tests and generate)
  619. if (!this.options._start) {
  620. this.serverContext.nuxt.hook('build:resources', () => this.loadResources(fs__default['default']));
  621. return
  622. }
  623. // Verify resources
  624. if (this.options.modern && !this.isModernReady) {
  625. throw new Error(
  626. `No modern build files found in ${this.distPath}.\nUse either \`nuxt build --modern\` or \`modern\` option to build modern files.`
  627. )
  628. } else if (!this.isReady) {
  629. throw new Error(
  630. `No build files found in ${this.distPath}.\nUse either \`nuxt build\` or \`builder.build()\` or start nuxt in development mode.`
  631. )
  632. }
  633. }
  634. async loadResources (_fs) {
  635. const updated = [];
  636. const readResource = async (fileName, encoding) => {
  637. try {
  638. const fullPath = path__default['default'].resolve(this.distPath, fileName);
  639. if (!await _fs.exists(fullPath)) {
  640. return
  641. }
  642. const contents = await _fs.readFile(fullPath, encoding);
  643. return contents
  644. } catch (err) {
  645. consola__default['default'].error('Unable to load resource:', fileName, err);
  646. }
  647. };
  648. for (const resourceName in this.resourceMap) {
  649. const { fileName, transform, encoding } = this.resourceMap[resourceName];
  650. // Load resource
  651. let resource = await readResource(fileName, encoding);
  652. // Skip unavailable resources
  653. if (!resource) {
  654. continue
  655. }
  656. // Apply transforms
  657. if (typeof transform === 'function') {
  658. resource = await transform(resource, { readResource });
  659. }
  660. // Update resource
  661. this.serverContext.resources[resourceName] = resource;
  662. updated.push(resourceName);
  663. }
  664. // Load templates
  665. await this.loadTemplates();
  666. await this.serverContext.nuxt.callHook('render:resourcesLoaded', this.serverContext.resources);
  667. // Detect if any resource updated
  668. if (updated.length > 0) {
  669. // Create new renderer
  670. this.createRenderer();
  671. }
  672. }
  673. async loadTemplates () {
  674. // Reload error template
  675. const errorTemplatePath = path__default['default'].resolve(this.options.buildDir, 'views/error.html');
  676. if (await fs__default['default'].exists(errorTemplatePath)) {
  677. const errorTemplate = await fs__default['default'].readFile(errorTemplatePath, 'utf8');
  678. this.serverContext.resources.errorTemplate = this.parseTemplate(errorTemplate);
  679. }
  680. // Reload loading template
  681. const loadingHTMLPath = path__default['default'].resolve(this.options.buildDir, 'loading.html');
  682. if (await fs__default['default'].exists(loadingHTMLPath)) {
  683. this.serverContext.resources.loadingHTML = await fs__default['default'].readFile(loadingHTMLPath, 'utf8');
  684. this.serverContext.resources.loadingHTML = this.serverContext.resources.loadingHTML.replace(/\r|\n|[\t\s]{3,}/g, '');
  685. } else {
  686. this.serverContext.resources.loadingHTML = '';
  687. }
  688. }
  689. // TODO: Remove in Nuxt 3
  690. get noSSR () { /* Backward compatibility */
  691. return this.options.render.ssr === false
  692. }
  693. get SSR () {
  694. return this.options.render.ssr === true
  695. }
  696. get isReady () {
  697. // SPA
  698. if (!this.serverContext.resources.spaTemplate || !this.renderer.spa) {
  699. return false
  700. }
  701. // SSR
  702. if (this.SSR && (!this.serverContext.resources.ssrTemplate || !this.renderer.ssr)) {
  703. return false
  704. }
  705. return true
  706. }
  707. get isModernReady () {
  708. return this.isReady && this.serverContext.resources.modernManifest
  709. }
  710. // TODO: Remove in Nuxt 3
  711. get isResourcesAvailable () { /* Backward compatibility */
  712. return this.isReady
  713. }
  714. detectModernBuild () {
  715. const { options, resources } = this.serverContext;
  716. if ([false, 'client', 'server'].includes(options.modern)) {
  717. return
  718. }
  719. const isExplicitStaticModern = options.target === utils.TARGETS.static && options.modern;
  720. if (!resources.modernManifest && !isExplicitStaticModern) {
  721. options.modern = false;
  722. return
  723. }
  724. options.modern = options.render.ssr ? 'server' : 'client';
  725. consola__default['default'].info(`Modern bundles are detected. Modern mode (\`${options.modern}\`) is enabled now.`);
  726. }
  727. createRenderer () {
  728. // Resource clientManifest is always required
  729. if (!this.serverContext.resources.clientManifest) {
  730. return
  731. }
  732. this.detectModernBuild();
  733. // Create SPA renderer
  734. if (this.serverContext.resources.spaTemplate) {
  735. this.renderer.spa = new SPARenderer(this.serverContext);
  736. }
  737. // Skip the rest if SSR resources are not available
  738. if (this.serverContext.resources.ssrTemplate && this.serverContext.resources.serverManifest) {
  739. // Create bundle renderer for SSR
  740. this.renderer.ssr = new SSRRenderer(this.serverContext);
  741. if (this.options.modern !== false) {
  742. this.renderer.modern = new ModernRenderer(this.serverContext);
  743. }
  744. }
  745. }
  746. renderSPA (renderContext) {
  747. return this.renderer.spa.render(renderContext)
  748. }
  749. renderSSR (renderContext) {
  750. // Call renderToString from the bundleRenderer and generate the HTML (will update the renderContext as well)
  751. const renderer = renderContext.modern ? this.renderer.modern : this.renderer.ssr;
  752. return renderer.render(renderContext)
  753. }
  754. async renderRoute (url, renderContext = {}, _retried = 0) {
  755. /* istanbul ignore if */
  756. if (!this.isReady) {
  757. // Fall-back to loading-screen if enabled
  758. if (this.options.build.loadingScreen) {
  759. // Tell nuxt middleware to use `server:nuxt:renderLoading hook
  760. return false
  761. }
  762. // Retry
  763. const retryLimit = this.options.dev ? 60 : 3;
  764. if (_retried < retryLimit && this._state !== 'error') {
  765. await this.ready().then(() => utils.waitFor(1000));
  766. return this.renderRoute(url, renderContext, _retried + 1)
  767. }
  768. // Throw Error
  769. switch (this._state) {
  770. case 'created':
  771. throw new Error('Renderer ready() is not called! Please ensure `nuxt.ready()` is called and awaited.')
  772. case 'loading':
  773. throw new Error('Renderer is loading.')
  774. case 'error':
  775. throw this._error
  776. case 'ready':
  777. throw new Error(`Renderer resources are not loaded! Please check possible console errors and ensure dist (${this.distPath}) exists.`)
  778. default:
  779. throw new Error('Renderer is in unknown state!')
  780. }
  781. }
  782. // Log rendered url
  783. consola__default['default'].debug(`Rendering url ${url}`);
  784. // Add url to the renderContext
  785. renderContext.url = ufo.normalizeURL(url);
  786. // Add target to the renderContext
  787. renderContext.target = this.options.target;
  788. const { req = {}, res = {} } = renderContext;
  789. // renderContext.spa
  790. if (renderContext.spa === undefined) {
  791. // TODO: Remove reading from renderContext.res in Nuxt3
  792. renderContext.spa = !this.SSR || req.spa || res.spa;
  793. }
  794. // renderContext.modern
  795. if (renderContext.modern === undefined) {
  796. const modernMode = this.options.modern;
  797. renderContext.modern = modernMode === 'client' || utils.isModernRequest(req, modernMode);
  798. }
  799. // Set runtime config on renderContext
  800. renderContext.runtimeConfig = {
  801. private: renderContext.spa ? {} : defu__default['default'](this.options.privateRuntimeConfig, this.options.publicRuntimeConfig),
  802. public: { ...this.options.publicRuntimeConfig }
  803. };
  804. // Call renderContext hook
  805. await this.serverContext.nuxt.callHook('vue-renderer:context', renderContext);
  806. // Render SPA or SSR
  807. return renderContext.spa
  808. ? this.renderSPA(renderContext)
  809. : this.renderSSR(renderContext)
  810. }
  811. get resourceMap () {
  812. const publicPath = utils.urlJoin(this.options.app.cdnURL, this.options.app.assetsPath);
  813. return {
  814. clientManifest: {
  815. fileName: 'client.manifest.json',
  816. transform: src => Object.assign(JSON.parse(src), { publicPath })
  817. },
  818. modernManifest: {
  819. fileName: 'modern.manifest.json',
  820. transform: src => Object.assign(JSON.parse(src), { publicPath })
  821. },
  822. serverManifest: {
  823. fileName: 'server.manifest.json',
  824. // BundleRenderer needs resolved contents
  825. transform: async (src, { readResource }) => {
  826. const serverManifest = JSON.parse(src);
  827. const readResources = async (obj) => {
  828. const _obj = {};
  829. await Promise.all(Object.keys(obj).map(async (key) => {
  830. _obj[key] = await readResource(obj[key]);
  831. }));
  832. return _obj
  833. };
  834. const [files, maps] = await Promise.all([
  835. readResources(serverManifest.files),
  836. readResources(serverManifest.maps)
  837. ]);
  838. // Try to parse sourcemaps
  839. for (const map in maps) {
  840. if (maps[map] && maps[map].version) {
  841. continue
  842. }
  843. try {
  844. maps[map] = JSON.parse(maps[map]);
  845. } catch (e) {
  846. maps[map] = { version: 3, sources: [], mappings: '' };
  847. }
  848. }
  849. return {
  850. ...serverManifest,
  851. files,
  852. maps
  853. }
  854. }
  855. },
  856. ssrTemplate: {
  857. fileName: 'index.ssr.html',
  858. transform: src => this.parseTemplate(src)
  859. },
  860. spaTemplate: {
  861. fileName: 'index.spa.html',
  862. transform: src => this.parseTemplate(src)
  863. }
  864. }
  865. }
  866. parseTemplate (templateStr) {
  867. return lodash.template(templateStr, {
  868. interpolate: /{{([\s\S]+?)}}/g,
  869. evaluate: /{%([\s\S]+?)%}/g
  870. })
  871. }
  872. close () {
  873. if (this.__closed) {
  874. return
  875. }
  876. this.__closed = true;
  877. for (const key in this.renderer) {
  878. delete this.renderer[key];
  879. }
  880. }
  881. }
  882. exports.VueRenderer = VueRenderer;