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('

User: 1

') + expect(existsSync(resolve(distDir, 'users/1.html'))).toBe(true) + expect( + existsSync(resolve(distDir, 'users/1/index.html')) + ).toBe(false) + }) + test('/-ignored', async () => { await expect(rp(url('/-ignored'))).rejects.toMatchObject({ response: { diff --git a/test/dev/generator.test.js b/test/dev/generator.test.js index 7baf48ea7b..eca8b44396 100644 --- a/test/dev/generator.test.js +++ b/test/dev/generator.test.js @@ -11,6 +11,7 @@ describe('generator', () => { const nuxt = new Nuxt(config) await nuxt.ready() const generator = new Generator(nuxt) + generator.getAppRoutes = jest.fn(() => []) const routes = await generator.initRoutes() expect(routes.length).toBe(array.length) @@ -31,6 +32,8 @@ describe('generator', () => { const nuxt = new Nuxt(config) await nuxt.ready() const generator = new Generator(nuxt) + generator.getAppRoutes = jest.fn(() => []) + const routes = await generator.initRoutes() expect(routes.length).toBe(array.length) @@ -50,6 +53,7 @@ describe('generator', () => { const nuxt = new Nuxt(config) await nuxt.ready() const generator = new Generator(nuxt) + generator.getAppRoutes = jest.fn(() => []) const array = ['/1', '/2', '/3', '/4'] const routes = await generator.initRoutes(array) @@ -70,6 +74,7 @@ describe('generator', () => { const nuxt = new Nuxt(config) await nuxt.ready() const generator = new Generator(nuxt) + generator.getAppRoutes = jest.fn(() => []) const array = ['/1', '/2', '/3', '/4'] const routes = await generator.initRoutes(...array) diff --git a/test/dev/renderer.test.js b/test/dev/renderer.test.js index b0331e3ba0..61b335d038 100644 --- a/test/dev/renderer.test.js +++ b/test/dev/renderer.test.js @@ -1,4 +1,5 @@ import consola from 'consola' +import { MODES } from '@nuxt/utils' import { Nuxt } from '../utils' const NO_BUILD_MSG = /Use either `nuxt build` or `builder\.build\(\)` or start nuxt in development mode/ @@ -12,7 +13,7 @@ describe('renderer', () => { test('detect no-build (Universal)', async () => { const nuxt = new Nuxt({ _start: true, - mode: 'universal', + mode: MODES.universal, dev: false, buildDir: '/path/to/404' }) @@ -25,7 +26,7 @@ describe('renderer', () => { test('detect no-build (SPA)', async () => { const nuxt = new Nuxt({ _start: true, - mode: 'spa', + mode: MODES.spa, dev: false, buildDir: '/path/to/404' }) @@ -37,7 +38,7 @@ describe('renderer', () => { test('detect no-modern-build', async () => { const nuxt = new Nuxt({ _start: true, - mode: 'universal', + mode: MODES.universal, modern: 'client', dev: false, buildDir: '/path/to/404' diff --git a/test/dev/unicode-base.size-limit.test.js b/test/dev/unicode-base.size-limit.test.js index 1f1f46c1fe..d00c9918e8 100644 --- a/test/dev/unicode-base.size-limit.test.js +++ b/test/dev/unicode-base.size-limit.test.js @@ -20,7 +20,7 @@ describe('nuxt minimal vue-app bundle size limit', () => { it('should stay within the size limit range', async () => { const filter = filename => filename === 'vue-app.nuxt.js' const legacyResourcesSize = await getResourcesSize(distDir, 'client', { filter }) - const LEGACY_JS_RESOURCES_KB_SIZE = 15.7 + const LEGACY_JS_RESOURCES_KB_SIZE = 16.2 expect(legacyResourcesSize.uncompressed).toBeWithinSize(LEGACY_JS_RESOURCES_KB_SIZE) }) }) diff --git a/yarn.lock b/yarn.lock index d09326a5c2..082473bd07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8690,6 +8690,13 @@ node-gyp@^5.0.2: tar "^4.4.12" which "^1.3.1" +node-html-parser@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.2.4.tgz#bff5b403da3c5061d189e922aafb193c8e1f6f92" + integrity sha512-qHwPdGyGr9pOZBoSgUOuNPG20QYZVN00lFcxKQgjPUODSxVH7obQeLVVawa3B4cfSNtLIeczSzoy/xYA8XG5WQ== + dependencies: + he "1.1.1" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"