diff --git a/examples/async-data/pages/posts/_id.vue b/examples/async-data/pages/posts/_id.vue index c391a073a9..708f02a4bd 100644 --- a/examples/async-data/pages/posts/_id.vue +++ b/examples/async-data/pages/posts/_id.vue @@ -12,13 +12,12 @@ ` + } // Prepare template params const templateParams = { diff --git a/packages/vue-renderer/src/renderers/ssr.js b/packages/vue-renderer/src/renderers/ssr.js index 5804c2a13d..65a1dac6c7 100644 --- a/packages/vue-renderer/src/renderers/ssr.js +++ b/packages/vue-renderer/src/renderers/ssr.js @@ -3,6 +3,7 @@ import crypto from 'crypto' import { format } from 'util' import fs from 'fs-extra' import consola from 'consola' +import { TARGETS, urlJoin } from '@nuxt/utils' import devalue from '@nuxt/devalue' import { createBundleRenderer } from 'vue-server-renderer' import BaseRenderer from './base' @@ -100,7 +101,8 @@ export default class SSRRenderer extends BaseRenderer { APP = `
` } - if (renderContext.redirected && !renderContext._generate) { + // Perf: early returns if server target and redirected + if (renderContext.redirected && renderContext.target === TARGETS.server) { return { html: APP, error: renderContext.nuxt.error, @@ -155,25 +157,65 @@ export default class SSRRenderer extends BaseRenderer { // 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) - let serializedSession = '' + const inlineScripts = [] - // Serialize state - if (shouldInjectScripts || shouldHashCspScriptSrc) { - // Only serialized session if need inject scripts or csp hash - serializedSession = `window.${this.serverContext.globals.context}=${devalue(renderContext.nuxt)};` - } + if (renderContext.staticAssetsBase) { + const preloadScripts = [] + renderContext.staticAssets = [] + const { staticAssetsBase, url, nuxt, staticAssets } = renderContext + const { data, fetch, ...state } = nuxt - if (shouldInjectScripts) { - APP += `` + // Initial state + const nuxtStaticScript = `window.__NUXT_STATIC__='${staticAssetsBase}';` + const stateScript = `window.${this.serverContext.globals.context}=${devalue(state)};` + + // Make chunk for initial state > 10 KB + const stateScriptKb = (stateScript.length * 4 /* utf8 */) / 100 + if (stateScriptKb > 10) { + const statePath = urlJoin(url, 'state.js') + const stateUrl = urlJoin(staticAssetsBase, statePath) + staticAssets.push({ path: statePath, src: stateScript }) + APP += `` + APP += `` + preloadScripts.push(stateUrl) + } else { + APP += `` + } + + // Page level payload.js (async loaded for CSR) + const payloadPath = urlJoin(url, 'payload.js') + const payloadUrl = urlJoin(staticAssetsBase, payloadPath) + const payloadScript = `__NUXT_JSONP__("${url}", ${devalue({ data, fetch })});` + staticAssets.push({ path: payloadPath, src: payloadScript }) + preloadScripts.push(payloadUrl) + + // Preload links + for (const href of preloadScripts) { + HEAD += `` + } + } else { + // Serialize state + let serializedSession + if (shouldInjectScripts || shouldHashCspScriptSrc) { + // Only serialized session if need inject scripts or csp hash + serializedSession = `window.${this.serverContext.globals.context}=${devalue(renderContext.nuxt)};` + inlineScripts.push(serializedSession) + } + + if (shouldInjectScripts) { + APP += `` + } } // Calculate CSP hashes const cspScriptSrcHashes = [] if (csp) { if (shouldHashCspScriptSrc) { - const hash = crypto.createHash(csp.hashAlgorithm) - hash.update(serializedSession) - cspScriptSrcHashes.push(`'${csp.hashAlgorithm}-${hash.digest('base64')}'`) + for (const script of inlineScripts) { + const hash = crypto.createHash(csp.hashAlgorithm) + hash.update(script) + cspScriptSrcHashes.push(`'${csp.hashAlgorithm}-${hash.digest('base64')}'`) + } } // Call ssr:csp hook diff --git a/packages/webpack/src/builder.js b/packages/webpack/src/builder.js index e345953989..3d9545710f 100644 --- a/packages/webpack/src/builder.js +++ b/packages/webpack/src/builder.js @@ -6,7 +6,7 @@ import webpackDevMiddleware from 'webpack-dev-middleware' import webpackHotMiddleware from 'webpack-hot-middleware' import consola from 'consola' -import { parallel, sequence, wrapArray, isModernRequest } from '@nuxt/utils' +import { TARGETS, parallel, sequence, wrapArray, isModernRequest } from '@nuxt/utils' import AsyncMFS from './utils/async-mfs' import * as WebpackConfigs from './config' @@ -244,6 +244,6 @@ export class WebpackBundler { } forGenerate () { - this.buildContext.isStatic = true + this.buildContext.target = TARGETS.static } } diff --git a/packages/webpack/src/config/base.js b/packages/webpack/src/config/base.js index 4c699a419d..8f86929771 100644 --- a/packages/webpack/src/config/base.js +++ b/packages/webpack/src/config/base.js @@ -11,12 +11,11 @@ import WebpackBar from 'webpackbar' import env from 'std-env' import semver from 'semver' -import { isUrl, urlJoin, getPKG } from '@nuxt/utils' +import { TARGETS, isUrl, urlJoin, getPKG } from '@nuxt/utils' import PerfLoader from '../utils/perf-loader' import StyleLoader from '../utils/style-loader' import WarningIgnorePlugin from '../plugins/warning-ignore' - import { reservedVueTags } from '../utils/reserved-tags' export default class WebpackBaseConfig { @@ -47,6 +46,10 @@ export default class WebpackBaseConfig { return this.dev ? 'development' : 'production' } + get target () { + return this.buildContext.target + } + get dev () { return this.buildContext.options.dev } @@ -139,7 +142,9 @@ export default class WebpackBaseConfig { const env = { 'process.env.NODE_ENV': JSON.stringify(this.mode), 'process.mode': JSON.stringify(this.mode), - 'process.static': this.buildContext.isStatic + 'process.dev': this.dev, + 'process.static': this.target === TARGETS.static, + 'process.target': JSON.stringify(this.target) } if (this.buildContext.buildOptions.aggressiveCodeRemoval) { env['typeof process'] = JSON.stringify(this.isServer ? 'object' : 'undefined') diff --git a/test/dev/basic.fail.generate.test.js b/test/dev/basic.fail.generate.test.js index 658e319de3..5bc409900a 100644 --- a/test/dev/basic.fail.generate.test.js +++ b/test/dev/basic.fail.generate.test.js @@ -1,4 +1,4 @@ -import { loadFixture, Nuxt, Generator } from '../utils' +import { loadFixture, Nuxt, Builder, Generator } from '../utils' describe('basic fail generate', () => { test('Fail with routes() which throw an error', async () => { @@ -12,9 +12,11 @@ describe('basic fail generate', () => { const nuxt = new Nuxt(options) await nuxt.ready() - const generator = new Generator(nuxt) + const builder = new Builder(nuxt) + builder.build = jest.fn() + const generator = new Generator(nuxt, builder) - await generator.generate({ build: false }).catch((e) => { + await generator.generate().catch((e) => { expect(e.message).toBe('Not today!') }) }) diff --git a/test/dev/basic.generate.test.js b/test/dev/basic.generate.test.js index 5244644b68..bef539fa82 100644 --- a/test/dev/basic.generate.test.js +++ b/test/dev/basic.generate.test.js @@ -4,6 +4,7 @@ import { resolve } from 'path' import { remove } from 'fs-extra' import serveStatic from 'serve-static' import finalhandler from 'finalhandler' +import { TARGETS } from '@nuxt/utils' import { Builder, Generator, getPort, loadFixture, Nuxt, rp, listPaths, equalOrStartsWith } from '../utils' let port @@ -19,7 +20,12 @@ let changedFileName describe('basic generate', () => { beforeAll(async () => { - const config = await loadFixture('basic', { generate: { dir: '.nuxt-generate' } }) + const config = await loadFixture('basic', { + generate: { + static: false, + dir: '.nuxt-generate' + } + }) const nuxt = new Nuxt(config) await nuxt.ready() @@ -47,7 +53,7 @@ describe('basic generate', () => { }) test('Check builder', () => { - expect(builder.bundleBuilder.buildContext.isStatic).toBe(true) + expect(builder.bundleBuilder.buildContext.target).toBe(TARGETS.static) expect(builder.build).toHaveBeenCalledTimes(1) }) @@ -167,10 +173,9 @@ describe('basic generate', () => { test('/validate should not be server-rendered', async () => { const { body: html } = await rp(url('/validate')) expect(html).toContain('') - expect(html).toContain('serverRendered:!1') }) - test('/validate -> should display a 404', async () => { + test.posix('/validate -> should display a 404', async () => { const window = await generator.nuxt.server.renderAndGetWindow(url('/validate')) const html = window.document.body.innerHTML expect(html).toContain('This page could not be found') @@ -185,7 +190,6 @@ describe('basic generate', () => { test('/redirect should not be server-rendered', async () => { const { body: html } = await rp(url('/redirect')) expect(html).toContain('') - expect(html).toContain('serverRendered:!1') }) test('/redirect -> check redirected source', async () => { @@ -204,6 +208,21 @@ describe('basic generate', () => { }) }) + test('nuxt re-generating with no subfolders', async () => { + generator.nuxt.options.generate.subFolders = false + generator.getAppRoutes = jest.fn(() => []) + await expect(generator.generate()).resolves.toBeTruthy() + }) + + test('/users/1.html', async () => { + const { body } = await rp(url('/users/1.html')) + expect(body).toContain('