From 917adc06184efd55a48123269b659adb288a3341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Thu, 7 May 2020 21:08:01 +0200 Subject: [PATCH] feat: options.target and full-static export (#6159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add options.target * fix(lint): lint * fix(test): update snapshots * fix(builder): default value for target * fix(test): fix test * fix(test): test fixing * fix: use this.options.target * fix: final test * Update packages/vue-renderer/src/renderer.js Co-Authored-By: Alexander Lichter * feat: Add target option and update banner * fix(lint): fix * feat: Add warning when using serverMiddleware in static target * chore(utils): add TARGETS and MODES as constants * hotfix: lint * chore(module): add filename as alias of fileName * feat: introducing nuxt export and router/routes.json * hotfix: Fix the linting lord * chore(core): add comment for filename vs fileName * fix: use targets constant * chore: remove warning * fix: unit testing * wip: refactor and use TARGETS * fix: lint * feat: add target as alias for first arg value * fix: generate only for SPA * chore: explain to use nuxt static X * fix: render SPA fallback on redirect for static target * fix: lint issue * fix: only target is useful for now * wip * wip: nuxt static export is looking good * Update packages/generator/src/generator.js Co-Authored-By: Devon Rueckner * Update packages/cli/src/options/common.js Co-Authored-By: Alexander Lichter * feat: add options.target * fix(lint): lint * fix(test): update snapshots * fix(builder): default value for target * fix(test): fix test * fix(test): test fixing * fix: use this.options.target * fix: final test * Update packages/vue-renderer/src/renderer.js Co-Authored-By: Alexander Lichter * feat: Add target option and update banner * fix(lint): fix * feat: Add warning when using serverMiddleware in static target * chore(utils): add TARGETS and MODES as constants * hotfix: lint * chore(module): add filename as alias of fileName * feat: introducing nuxt export and router/routes.json * hotfix: Fix the linting lord * chore(core): add comment for filename vs fileName * fix: use targets constant * chore: remove warning * fix: unit testing * wip: refactor and use TARGETS * fix: lint * feat: add target as alias for first arg value * chore: explain to use nuxt static X * fix: render SPA fallback on redirect for static target * fix: lint issue * fix: only target is useful for now * wip * wip: nuxt static export is looking good * Update packages/generator/src/generator.js Co-Authored-By: Devon Rueckner * Update packages/cli/src/options/common.js Co-Authored-By: Alexander Lichter * fix: duplicate imports * chore: don't server render if an error happens on static target * test: update unit and add export * lint: fix * lint: fix * fix: e2e test * fix: fallback only for static target * fix: dev test * feat: add generate.crawler * fix: full static is when generate.static is given * chore: improvements * fix: Add isFullStatic in nuxt/config.json * feat: handle fetch for full static * feat: router.prefetchPayloads for full static * chore: use fetch in async-data example * fix: add target only if given * fix: use created to have access to props in fetchOnServer * chore: add console.error in dev for easy debugging * feat: payload smart pre-fetching * fix: remove alias for target * fix: increment payloadFetchIndex is static set to false * chore: lint * chore: add serve command * chore: rename universal to server-side * fix: handle payloadPath on SPA fallback * fix: lint * chore lint again * feat: handle spa fallback * feat: support string for exclude * fix: fallback only if no extension or html * chore: use JSON.stringify() for static target * chore: lint again, dammit * chore: fix tests and remove too early return * fix: early return only for server target * fix: update tests * fix: unit tests * chore: add ssr option * chore: add logic for ssr option * fix: #6682 * chore(dx): add next command to run * fix: lint * fix: tests * chore: keep old behaviour for nuxt build in spa * fix: test again, oh boy * fix: alright this is good now * chore: add comment for spa fallback * chore: move routes.json to dot nuxt dir * chore: simplify check for promise * chore: unique lock id * chore: refactor isFullStatic * fix: dont set default in build context * chore: add test for serve * chore: update tests * hotfix: lint tests * chore(dx): improve message for bundling * feat: js payload extraction with jsonp * fix: keep serialized session script for legacy generate * fix: call to setPagePayload from fetchPayload * use devalue for payload chunks * feat: add initial load state chunk * feat: preload payload and state scripts * fix(vue-app): don't re-render the app if trailing slash on SSG * hotfix: remove console.log * chore(dx): add deploy infos for nuxt export Co-authored-by: Pooya Parsa * chore: handle fetching payload.js for nuxt state * chore(dx): error when using nuxt generate and static * chore: remove static option for clarity * chore: remove serverless target * hotfix: lint * hotfix: unit tests * chore: update legacy js resource * chore: remove query params from url in static target * fix: use globalName and urlJoin * chore: typo * feat: previewMode 👀 * chore: rename to enablePreview * fix: wait next tick to avoid error on spa * chore: try 1 sec * hotfix: test only for linux, wtf azure * refactor: static assets - generalize logic for modules need emit export static assets - allow customization for version, dir and base - serialization logic is only in ssr now * feat: smart state chunk creates * fix(client): ignore payload load error * perf: avoide payload loading for spa initial * perf: avoid loading failed chunks again * chore(cli): add simple compression for nuxt serve * test: update snapshots * fix version snapshot * fix(generator): set staticAssetsBase on context only for full static * fix tests * fix: honor shouldHashCspScriptSrc * chore(dx): add log for client-side fallback creation Co-authored-by: Xin Du (Clark) Co-authored-by: Alexander Lichter Co-authored-by: Pooya Parsa Co-authored-by: Devon Rueckner Co-authored-by: Pooya Parsa --- examples/async-data/pages/posts/_id.vue | 7 +- examples/async-data/pages/posts/index.vue | 12 +- packages/builder/src/builder.js | 14 +- packages/builder/src/context/build.js | 2 +- packages/builder/src/context/template.js | 3 +- packages/builder/test/__utils__/index.js | 3 + packages/builder/test/builder.build.test.js | 4 +- .../__snapshots__/template.test.js.snap | 1 + packages/builder/test/context/build.test.js | 9 +- packages/cli/src/commands/build.js | 10 +- packages/cli/src/commands/export.js | 50 +++++++ packages/cli/src/commands/generate.js | 22 ++- packages/cli/src/commands/index.js | 2 + packages/cli/src/commands/serve.js | 83 ++++++++++++ packages/cli/src/commands/start.js | 4 + packages/cli/src/options/common.js | 12 +- packages/cli/src/utils/banner.js | 11 +- packages/cli/src/utils/config.js | 4 +- .../unit/__snapshots__/command.test.js.snap | 3 + packages/cli/test/unit/build.test.js | 14 +- packages/cli/test/unit/command.test.js | 2 +- packages/cli/test/unit/export.test.js | 118 ++++++++++++++++ packages/cli/test/unit/serve.test.js | 44 ++++++ packages/cli/test/unit/start.test.js | 11 ++ packages/cli/test/unit/utils.test.js | 42 +++++- packages/cli/test/utils/mocking.js | 7 +- packages/config/src/config/_common.js | 21 ++- packages/config/src/config/generate.js | 17 +++ packages/config/src/config/index.js | 4 +- packages/config/src/config/modes.js | 6 +- packages/config/src/config/router.js | 1 + packages/config/src/options.js | 45 +++++-- .../test/__snapshots__/options.test.js.snap | 10 ++ .../config/__snapshots__/index.test.js.snap | 20 +++ packages/config/test/options.test.js | 13 +- packages/core/src/module.js | 10 +- packages/generator/package.json | 3 +- packages/generator/src/generator.js | 127 ++++++++++++++---- packages/generator/test/__utils__/index.js | 4 +- .../generator/test/generator.init.test.js | 11 +- .../generator/test/generator.route.test.js | 4 +- packages/server/src/jsdom.js | 5 +- packages/server/src/middleware/nuxt.js | 4 +- packages/server/src/server.js | 2 - packages/utils/src/constants.js | 9 ++ packages/utils/src/context.js | 5 + packages/utils/src/index.js | 1 + packages/utils/test/index.test.js | 4 +- packages/vue-app/src/index.js | 3 + packages/vue-app/template/App.js | 57 ++++++-- packages/vue-app/template/client.js | 65 +++++++-- .../template/components/nuxt-error.vue | 2 +- .../template/components/nuxt-link.client.js | 19 ++- packages/vue-app/template/index.js | 13 ++ packages/vue-app/template/jsonp.js | 80 +++++++++++ .../vue-app/template/mixins/fetch.client.js | 32 ++++- .../vue-app/template/mixins/fetch.server.js | 5 +- packages/vue-app/template/nuxt/config.json | 5 + packages/vue-app/template/routes.json | 1 + packages/vue-app/template/server.js | 19 ++- packages/vue-app/template/utils.js | 12 +- packages/vue-renderer/src/renderer.js | 6 +- packages/vue-renderer/src/renderers/spa.js | 13 +- packages/vue-renderer/src/renderers/ssr.js | 66 +++++++-- packages/webpack/src/builder.js | 4 +- packages/webpack/src/config/base.js | 11 +- test/dev/basic.fail.generate.test.js | 8 +- test/dev/basic.generate.test.js | 29 +++- test/dev/generator.test.js | 5 + test/dev/renderer.test.js | 7 +- test/dev/unicode-base.size-limit.test.js | 2 +- yarn.lock | 7 + 72 files changed, 1105 insertions(+), 186 deletions(-) create mode 100644 packages/cli/src/commands/export.js create mode 100644 packages/cli/src/commands/serve.js create mode 100644 packages/cli/test/unit/export.test.js create mode 100644 packages/cli/test/unit/serve.test.js create mode 100644 packages/config/src/config/generate.js create mode 100644 packages/utils/src/constants.js create mode 100644 packages/vue-app/template/jsonp.js create mode 100644 packages/vue-app/template/nuxt/config.json create mode 100644 packages/vue-app/template/routes.json 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"