diff --git a/lib/builder/generator.js b/lib/builder/generator.js index 7a8ef0b45d..d94d751048 100644 --- a/lib/builder/generator.js +++ b/lib/builder/generator.js @@ -10,6 +10,7 @@ const _ = require('lodash') const { resolve, join, dirname, sep } = require('path') const { minify } = require('html-minifier') const Chalk = require('chalk') +const { printWarn } = require('../common/utils') const { isUrl, @@ -141,15 +142,22 @@ module.exports = class Generator { } async afterGenerate() { - const indexPath = join(this.distPath, 'index.html') - if (existsSync(indexPath)) { - // Copy /index.html to /200.html for surge SPA - // https://surge.sh/help/adding-a-200-page-for-client-side-routing - const _200Path = join(this.distPath, '200.html') - if (!existsSync(_200Path)) { - await copy(indexPath, _200Path) - } + let { fallback } = this.options.generate + + // Disable SPA fallback if value isn't true or a string + if (fallback !== true && typeof fallback !== 'string') return + + const fallbackPath = join(this.distPath, fallback) + + // Prevent conflicts + if (existsSync(fallbackPath)) { + printWarn(`SPA fallback was configured, but the configured path (${fallbackPath}) already exists.`) + return } + + // Render and write the SPA template to the fallback path + const { html } = await this.nuxt.renderRoute('/', { spa: true }) + await writeFile(fallbackPath, html, 'utf8') } async initDist() { diff --git a/lib/common/options.js b/lib/common/options.js index e2d06d56cf..3a3a262564 100755 --- a/lib/common/options.js +++ b/lib/common/options.js @@ -138,6 +138,11 @@ Options.from = function (_options) { options.transition.appear = true } + // We assume the SPA fallback path is 404.html (for GitHub Pages, Surge, etc.) + if (options.generate.fallback === true) { + options.generate.fallback = '404.html' + } + return options } @@ -223,6 +228,7 @@ Options.defaults = { concurrency: 500, interval: 0, subFolders: true, + fallback: '200.html', minify: { collapseBooleanAttributes: true, collapseWhitespace: false, diff --git a/test/fallback.generate.test.js b/test/fallback.generate.test.js new file mode 100644 index 0000000000..f041616d69 --- /dev/null +++ b/test/fallback.generate.test.js @@ -0,0 +1,113 @@ +import test from 'ava' +import { resolve } from 'path' +import { existsSync } from 'fs' +import http from 'http' +import serveStatic from 'serve-static' +import finalhandler from 'finalhandler' +import rp from 'request-promise-native' +import { intercept, interceptLog } from './helpers/console' +import { Nuxt, Builder, Generator, Options } from '..' + +const port = 4015 +const url = route => 'http://localhost:' + port + route +const rootDir = resolve(__dirname, 'fixtures/basic') + +let nuxt = null +let server = null +let generator = null + +// Init nuxt.js and create server listening on localhost:4015 +test.serial('Init Nuxt.js', async t => { + let config = require(resolve(rootDir, 'nuxt.config.js')) + config.rootDir = rootDir + config.buildDir = '.nuxt-spa-fallback' + config.dev = false + config.build.stats = false + + const logSpy = await interceptLog(async () => { + nuxt = new Nuxt(config) + const builder = new Builder(nuxt) + generator = new Generator(nuxt, builder) + + await generator.generate() + }) + t.true(logSpy.calledWithMatch('DONE')) + + const serve = serveStatic(resolve(__dirname, 'fixtures/basic/dist')) + server = http.createServer((req, res) => { + serve(req, res, finalhandler(req, res)) + }) + server.listen(port) +}) + +test.serial('default creates /200.html as fallback', async t => { + const html = await rp(url('/200.html')) + t.false(html.includes('

Index page

')) + t.false(html.includes('data-server-rendered')) + t.true(existsSync(resolve(__dirname, 'fixtures/basic/dist', '200.html'))) + t.false(existsSync(resolve(__dirname, 'fixtures/basic/dist', '404.html'))) +}) + +test.serial('nuxt re-generating with generate.fallback = false', async t => { + const logSpy = await interceptLog(async () => { + nuxt.options.generate.fallback = false + await generator.generate() + }) + t.true(logSpy.calledWithMatch('DONE')) +}) + +test.serial('false creates no fallback', async t => { + const error = await t.throws(rp(url('/200.html'))) + t.true(error.statusCode === 404) + t.true(error.response.body.includes('Cannot GET /200.html')) + t.false(existsSync(resolve(__dirname, 'fixtures/basic/dist', '200.html'))) + t.false(existsSync(resolve(__dirname, 'fixtures/basic/dist', '404.html'))) +}) + +test.serial('generate.fallback = true is transformed to /404.html', async t => { + nuxt.options.generate.fallback = true + const options = Options.from(nuxt.options) + t.is(options.generate.fallback, '404.html') +}) + +test.serial('nuxt re-generating with generate.fallback = "spa-fallback.html"', async t => { + const logSpy = await interceptLog(async () => { + nuxt.options.generate.fallback = 'spa-fallback.html' + await generator.generate() + }) + t.true(logSpy.calledWithMatch('DONE')) +}) + +test.serial('"spa-fallback.html" creates /spa-fallback.html as fallback', async t => { + const html = await rp(url('/spa-fallback.html')) + t.false(html.includes('

Index page

')) + t.false(html.includes('data-server-rendered')) + t.true( + existsSync(resolve(__dirname, 'fixtures/basic/dist', 'spa-fallback.html')) + ) + t.false(existsSync(resolve(__dirname, 'fixtures/basic/dist', '404.html'))) + t.false(existsSync(resolve(__dirname, 'fixtures/basic/dist', '200.html'))) +}) + +test.serial('nuxt re-generating with generate.fallback = "index.html"', async t => { + const {log: logSpy, warn: warnSpy} = await intercept({warn: true, log: true}, async () => { + nuxt.options.generate.fallback = 'index.html' + await generator.generate() + }) + t.true(warnSpy.calledWithMatch('WARN')) // Must emit warnning + t.true(logSpy.calledWithMatch('DONE')) +}) + +test.serial('"index.html" creates /index.html as fallback', async t => { + const html = await rp(url('/index.html')) + t.true(html.includes('

Index page

')) + t.true(html.includes('data-server-rendered')) + t.true(existsSync(resolve(__dirname, 'fixtures/basic/dist', 'index.html'))) + t.false(existsSync(resolve(__dirname, 'fixtures/basic/dist', '404.html'))) + t.false(existsSync(resolve(__dirname, 'fixtures/basic/dist', '200.html'))) +}) + +// Close server and ask nuxt to stop listening to file changes +test.after.always('Closing server', async t => { + await server.close() +})