Repository: nuxt/nuxt.js
Pull request #7712 (2020-07-13 15:32:52)

feat: static target DX improvements
<!--- Provide a general summary of your changes in the title above -->

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (a non-breaking change which fixes an issue)
- [x] New feature (a non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)


## Description

- With 2.13 we were making errors to force using `nuxt export` and `nuxt serve` for static target. Now instead users can keep using `nuxt generate` and `nuxt start`
- `export:*` hooks deprecated to avoid using different names (generate will kept for nuxt3 too)
- `exports` option is deprecated
- Move smart build from [nuxt/static](https://github.com/nuxt/static) to the core for `nuxt generate` for static target only (non breaking change)

## Checklist:
<!--- Put an `x` in all the boxes that apply. -->
<!--- If your change requires a documentation PR, please link it appropriately -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly. (PR: #)
- [ ] I have added tests to cover my changes (if not applicable, please state why)
- [x] All new and existing tests are passing.



codecov-commenter: # [Codecov](https://codecov.io/gh/nuxt/nuxt.js/pull/7712?src=pr&el=h1) Report
> Merging [#7712](https://codecov.io/gh/nuxt/nuxt.js/pull/7712?src=pr&el=desc) into [dev](https://codecov.io/gh/nuxt/nuxt.js/commit/e537e8c92ab3f39553ca7b0bb1db75b92fcf28f2&el=desc) will **decrease** coverage by `1.34%`.
> The diff coverage is `28.57%`.

[![Impacted file tree graph](https://codecov.io/gh/nuxt/nuxt.js/pull/7712/graphs/tree.svg?width=650&height=150&src=pr&token=nGD1PtMB3M)](https://codecov.io/gh/nuxt/nuxt.js/pull/7712?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##              dev    #7712      +/-   ##
==========================================
- Coverage   70.13%   68.79%   -1.35%     
==========================================
  Files          88       90       +2     
  Lines        3764     3820      +56     
  Branches     1025     1033       +8     
==========================================
- Hits         2640     2628      -12     
- Misses        913      969      +56     
- Partials      211      223      +12     
```

| Flag | Coverage Δ | |
|---|---|---|
| #unittests | `68.79% <28.57%> (-1.35%)` | :arrow_down: |

| [Impacted Files](https://codecov.io/gh/nuxt/nuxt.js/pull/7712?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/cli/src/utils/generate.js](https://codecov.io/gh/nuxt/nuxt.js/pull/7712/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2xpL3NyYy91dGlscy9nZW5lcmF0ZS5qcw==) | `0.00% <0.00%> (ø)` | |
| [packages/config/src/config/generate.js](https://codecov.io/gh/nuxt/nuxt.js/pull/7712/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29uZmlnL3NyYy9jb25maWcvZ2VuZXJhdGUuanM=) | `100.00% <ø> (ø)` | |
| [packages/core/src/nuxt.js](https://codecov.io/gh/nuxt/nuxt.js/pull/7712/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29yZS9zcmMvbnV4dC5qcw==) | `93.18% <ø> (ø)` | |
| [packages/vue-app/src/index.js](https://codecov.io/gh/nuxt/nuxt.js/pull/7712/diff?src=pr&el=tree#diff-cGFja2FnZXMvdnVlLWFwcC9zcmMvaW5kZXguanM=) | `100.00% <ø> (ø)` | |
| [packages/cli/src/commands/generate.js](https://codecov.io/gh/nuxt/nuxt.js/pull/7712/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2xpL3NyYy9jb21tYW5kcy9nZW5lcmF0ZS5qcw==) | `88.23% <50.00%> (-5.52%)` | :arrow_down: |
| [packages/generator/src/generator.js](https://codecov.io/gh/nuxt/nuxt.js/pull/7712/diff?src=pr&el=tree#diff-cGFja2FnZXMvZ2VuZXJhdG9yL3NyYy9nZW5lcmF0b3IuanM=) | `85.78% <50.00%> (+0.49%)` | :arrow_up: |
| [packages/cli/src/utils/serve.js](https://codecov.io/gh/nuxt/nuxt.js/pull/7712/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2xpL3NyYy91dGlscy9zZXJ2ZS5qcw==) | `70.37% <70.37%> (ø)` | |
| [packages/cli/src/commands/build.js](https://codecov.io/gh/nuxt/nuxt.js/pull/7712/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2xpL3NyYy9jb21tYW5kcy9idWlsZC5qcw==) | `89.65% <100.00%> (ø)` | |
| [packages/cli/src/commands/export.js](https://codecov.io/gh/nuxt/nuxt.js/pull/7712/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2xpL3NyYy9jb21tYW5kcy9leHBvcnQuanM=) | `100.00% <100.00%> (ø)` | |
| [packages/cli/src/commands/serve.js](https://codecov.io/gh/nuxt/nuxt.js/pull/7712/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2xpL3NyYy9jb21tYW5kcy9zZXJ2ZS5qcw==) | `100.00% <100.00%> (+27.58%)` | :arrow_up: |
| ... and [3 more](https://codecov.io/gh/nuxt/nuxt.js/pull/7712/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/nuxt/nuxt.js/pull/7712?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/nuxt/nuxt.js/pull/7712?src=pr&el=footer). Last update [e537e8c...86b3a53](https://codecov.io/gh/nuxt/nuxt.js/pull/7712?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

Atinux: I need to update articles & docs now :)

Patch:
diff --git a/jest.config.js b/jest.config.js
index 68e437d91..abda43df2 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -19,7 +19,8 @@ module.exports = {
   // But its performance overhead is pretty bad (30+%).
   // detectOpenHandles: true
 
-  setupFilesAfterEnv: ['./test/utils/setup'],
+  setupFilesAfterEnv: ['./test/utils/setup-env'],
+  setupFiles: ['./test/utils/setup'],
 
   coverageDirectory: './coverage',
 
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 608ee6559..1b6c34375 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -20,10 +20,13 @@
     "compression": "^1.7.4",
     "connect": "^3.7.0",
     "consola": "^2.14.0",
+    "crc": "^3.8.0",
+    "destr": "^1.0.0",
     "esm": "^3.2.25",
     "execa": "^3.4.0",
     "exit": "^0.1.2",
     "fs-extra": "^8.1.0",
+    "globby": "^11.0.1",
     "hable": "^3.0.0",
     "minimist": "^1.2.5",
     "opener": "1.5.1",
diff --git a/packages/cli/src/commands/build.js b/packages/cli/src/commands/build.js
index 060c028a2..5224654c7 100644
--- a/packages/cli/src/commands/build.js
+++ b/packages/cli/src/commands/build.js
@@ -85,7 +85,7 @@ export default {
       const builder = await cmd.getBuilder(nuxt)
       await builder.build()
 
-      const nextCommand = nuxt.options.target === TARGETS.static ? 'nuxt export' : 'nuxt start'
+      const nextCommand = nuxt.options.target === TARGETS.static ? 'nuxt generate' : 'nuxt start'
       consola.info('Ready to run `' + (nextCommand) + '`')
     }
   }
diff --git a/packages/cli/src/commands/export.js b/packages/cli/src/commands/export.js
index a77ebb604..c1fd3656d 100644
--- a/packages/cli/src/commands/export.js
+++ b/packages/cli/src/commands/export.js
@@ -1,50 +1,10 @@
-import path from 'path'
 import consola from 'consola'
-import { TARGETS } from '@nuxt/utils'
-import { common, locking } from '../options'
-import { createLock } from '../utils'
+import generate from './generate'
 
 export default {
-  name: 'export',
-  description: 'Export a static generated web application',
-  usage: 'export <dir>',
-  options: {
-    ...common,
-    ...locking,
-    'fail-on-error': {
-      type: 'boolean',
-      default: false,
-      description: 'Exit with non-zero status code if there are errors when exporting pages'
-    }
-  },
-  async run (cmd) {
-    const config = await cmd.getNuxtConfig({
-      dev: false,
-      target: TARGETS.static,
-      _export: true
-    })
-    const nuxt = await cmd.getNuxt(config)
-
-    if (cmd.argv.lock) {
-      await cmd.setLock(await createLock({
-        id: 'export',
-        dir: nuxt.options.generate.dir,
-        root: config.rootDir
-      }))
-    }
-
-    const generator = await cmd.getGenerator(nuxt)
-    await nuxt.server.listen(0)
-
-    const { errors } = await generator.generate({
-      init: true,
-      build: false
-    })
-
-    await nuxt.close()
-    if (cmd.argv['fail-on-error'] && errors.length > 0) {
-      throw new Error('Error exporting pages, exiting with non-zero code')
-    }
-    consola.info('Ready to run `nuxt serve` or deploy `' + path.basename(nuxt.options.generate.dir) + '/` directory')
+  ...generate,
+  run (...args) {
+    consola.warn('`nuxt export` has been deprecated! Please use `nuxt generate`.')
+    return generate.run.call(this, ...args)
   }
 }
diff --git a/packages/cli/src/commands/generate.js b/packages/cli/src/commands/generate.js
index b77a8d09e..68f248c1d 100644
--- a/packages/cli/src/commands/generate.js
+++ b/packages/cli/src/commands/generate.js
@@ -1,6 +1,7 @@
 import { TARGETS } from '@nuxt/utils'
 import { common, locking } from '../options'
 import { normalizeArg, createLock } from '../utils'
+import { ensureBuild, generate } from '../utils/generate'
 
 export default {
   name: 'generate',
@@ -54,23 +55,22 @@ export default {
     }
   },
   async run (cmd) {
-    const config = await cmd.getNuxtConfig({
-      dev: false,
-      _build: cmd.argv.build,
-      _generate: true
-    })
+    const config = await cmd.getNuxtConfig({ dev: false })
+
+    // Disable analyze if set by the nuxt config
+    config.build = config.build || {}
+    config.build.analyze = false
 
+    // Full static
     if (config.target === TARGETS.static) {
-      throw new Error("Please use `nuxt export` when using `target: 'static'`")
+      await ensureBuild(cmd)
+      await generate(cmd)
+      return
     }
 
     // Forcing static target anyway
     config.target = TARGETS.static
 
-    // Disable analyze if set by the nuxt config
-    config.build = config.build || {}
-    config.build.analyze = false
-
     // Set flag to keep the prerendering behaviour
     config._legacyGenerate = true
 
diff --git a/packages/cli/src/commands/serve.js b/packages/cli/src/commands/serve.js
index 6867898ae..864cfd54c 100644
--- a/packages/cli/src/commands/serve.js
+++ b/packages/cli/src/commands/serve.js
@@ -1,84 +1,10 @@
-import { promises as fs } from 'fs'
-import { join, extname, basename } from 'path'
-import connect from 'connect'
-import serveStatic from 'serve-static'
-import compression from 'compression'
-import { getNuxtConfig } from '@nuxt/config'
-import { TARGETS } from '@nuxt/utils'
-import { common, server } from '../options'
-import { showBanner } from '../utils/banner'
-import * as imports from '../imports'
+import consola from 'consola'
+import start from './start'
 
 export default {
-  name: 'serve',
-  description: 'Serve the exported static application (should be compiled with `nuxt build` and `nuxt export` first)',
-  usage: 'serve <dir>',
-  options: {
-    'config-file': common['config-file'],
-    version: common.version,
-    help: common.help,
-    ...server
-  },
-  async run (cmd) {
-    let options = await cmd.getNuxtConfig({ dev: false })
-    // add default options
-    options = getNuxtConfig(options)
-    try {
-      // overwrites with build config
-      const buildConfig = require(join(options.buildDir, 'nuxt/config.json'))
-      options.target = buildConfig.target
-    } catch (err) {}
-
-    if (options.target === TARGETS.server) {
-      throw new Error('You cannot use `nuxt serve` with ' + TARGETS.server + ' target, please use `nuxt start`')
-    }
-    const distStat = await fs.stat(options.generate.dir).catch(err => null) // eslint-disable-line handle-callback-err
-    if (!distStat || !distStat.isDirectory()) {
-      throw new Error('Output directory `' + basename(options.generate.dir) + '/` does not exists, please run `nuxt export` before `nuxt serve`.')
-    }
-    const app = connect()
-    app.use(compression({ threshold: 0 }))
-    app.use(
-      options.router.base,
-      serveStatic(options.generate.dir, {
-        extensions: ['html']
-      })
-    )
-    if (options.generate.fallback) {
-      const fallbackFile = await fs.readFile(join(options.generate.dir, options.generate.fallback), 'utf-8')
-      app.use((req, res, next) => {
-        const ext = extname(req.url) || '.html'
-
-        if (ext !== '.html') {
-          return next()
-        }
-        res.writeHeader(200, {
-          'Content-Type': 'text/html'
-        })
-        res.write(fallbackFile)
-        res.end()
-      })
-    }
-
-    const { port, host, socket, https } = options.server
-    const { Listener } = await imports.server()
-    const listener = new Listener({
-      port,
-      host,
-      socket,
-      https,
-      app,
-      dev: true, // try another port if taken
-      baseURL: options.router.base
-    })
-    await listener.listen()
-    const { Nuxt } = await imports.core()
-    showBanner({
-      constructor: Nuxt,
-      options,
-      server: {
-        listeners: [listener]
-      }
-    }, false)
+  ...start,
+  run (...args) {
+    consola.warn('`nuxt serve` has been deprecated! Please use `nuxt start`.')
+    return start.run.call(this, ...args)
   }
 }
diff --git a/packages/cli/src/commands/start.js b/packages/cli/src/commands/start.js
index e462b6207..a120a405b 100644
--- a/packages/cli/src/commands/start.js
+++ b/packages/cli/src/commands/start.js
@@ -1,6 +1,8 @@
 import { TARGETS } from '@nuxt/utils'
+import consola from 'consola'
 import { common, server } from '../options'
 import { showBanner } from '../utils/banner'
+import { serve } from '../utils/serve'
 
 export default {
   name: 'start',
@@ -12,9 +14,12 @@ export default {
   },
   async run (cmd) {
     const config = await cmd.getNuxtConfig({ dev: false, _start: true })
+
     if (config.target === TARGETS.static) {
-      throw new Error('You cannot use `nuxt start` with ' + TARGETS.static + ' target, please use `nuxt export` and `nuxt serve`')
+      consola.info('Serving static dist')
+      return serve(cmd)
     }
+
     const nuxt = await cmd.getNuxt(config)
 
     // Listen and show ready banner
diff --git a/packages/cli/src/utils/generate.js b/packages/cli/src/utils/generate.js
new file mode 100644
index 000000000..830cee85d
--- /dev/null
+++ b/packages/cli/src/utils/generate.js
@@ -0,0 +1,161 @@
+import fs from 'fs'
+import path, { relative } from 'path'
+import crc32 from 'crc/lib/crc32'
+import consola from 'consola'
+import globby from 'globby'
+import destr from 'destr'
+import { TARGETS } from '@nuxt/utils'
+
+export async function generate (cmd) {
+  const nuxt = await getNuxt({ server: true }, cmd)
+  const generator = await cmd.getGenerator(nuxt)
+
+  await nuxt.server.listen(0)
+  await generator.generate({ build: false })
+  await nuxt.close()
+}
+
+export async function ensureBuild (cmd) {
+  const nuxt = await getNuxt({ _build: true, server: false }, cmd)
+  const { options } = nuxt
+
+  if (options.generate.cache === false || process.env.NUXT_BUILD) {
+    const builder = await cmd.getBuilder(nuxt)
+    await builder.build()
+    await nuxt.close()
+  }
+
+  // Default build ignore files
+  const ignore = [
+    options.buildDir,
+    options.dir.static,
+    options.generate.dir,
+    'node_modules',
+    '.**/*',
+    '.*',
+    'README.md'
+  ]
+
+  // Extend ignore
+  const { generate } = options
+  if (generate.cache.ignore === 'function') {
+    generate.cache.ignore = generate.cache.ignore(ignore)
+  } else if (Array.isArray(generate.cache.ignore)) {
+    generate.cache.ignore = generate.cache.ignore.concat(ignore)
+  }
+  await nuxt.callHook('generate:cache:ignore', generate.cache.ignore)
+
+  // Take a snapshot of current project
+  const snapshotOptions = {
+    rootDir: nuxt.options.rootDir,
+    ignore: nuxt.options.generate.cache.ignore,
+    globbyOptions: nuxt.options.generate.cache.globbyOptions
+  }
+
+  const currentBuildSnapshot = await snapshot(snapshotOptions)
+
+  // Current build meta
+  const currentBuild = {
+    // @ts-ignore
+    nuxtVersion: nuxt.constructor.version,
+    ssr: nuxt.options.ssr,
+    target: nuxt.options.target,
+    snapshot: currentBuildSnapshot
+  }
+
+  // Check if build can be skipped
+  const nuxtBuildFile = path.resolve(nuxt.options.buildDir, 'build.json')
+  if (fs.existsSync(nuxtBuildFile)) {
+    const previousBuild = destr(fs.readFileSync(nuxtBuildFile, 'utf-8')) || {}
+
+    // Quick diff
+    const needBuild = false
+    for (const field of ['nuxtVersion', 'ssr', 'target']) {
+      if (previousBuild[field] !== currentBuild[field]) {
+        consola.info(`Doing webpack rebuild because ${field} changed`)
+        break
+      }
+    }
+
+    // Full snapshot diff
+    if (!needBuild) {
+      const changed = compareSnapshots(previousBuild.snapshot, currentBuild.snapshot)
+      if (!changed) {
+        consola.success('Skipping webpack build as no changes detected')
+        return
+      } else {
+        consola.info(`Doing webpack rebuild because ${changed} modified`)
+      }
+    }
+  }
+
+  // Webpack build
+  const builder = await cmd.getBuilder(nuxt)
+  await builder.build()
+
+  // Write build.json
+  fs.writeFileSync(nuxtBuildFile, JSON.stringify(currentBuild, null, 2), 'utf-8')
+
+  await nuxt.close()
+}
+
+async function getNuxt (args, cmd) {
+  const config = await cmd.getNuxtConfig({ dev: false, ...args })
+
+  if (config.target === TARGETS.static) {
+    config._export = true
+  } else {
+    config._legacyGenerate = true
+  }
+  config.buildDir = (config.static && config.static.cacheDir) || path.resolve(config.rootDir, 'node_modules/.cache/nuxt')
+  config.build = config.build || {}
+  config.build.transpile = config.build.transpile || []
+  config.build.transpile.push(config.buildDir)
+
+  const nuxt = await cmd.getNuxt(config)
+
+  return nuxt
+}
+
+export function compareSnapshots (from, to) {
+  const allKeys = Array.from(new Set([
+    ...Object.keys(from).sort(),
+    ...Object.keys(to).sort()
+  ]))
+
+  for (const key of allKeys) {
+    if (JSON.stringify(from[key]) !== JSON.stringify(to[key])) {
+      return key
+    }
+  }
+
+  return false
+}
+
+export async function snapshot ({ globbyOptions, ignore, rootDir }) {
+  const snapshot = {}
+
+  const files = await globby('**/*.*', {
+    ...globbyOptions,
+    ignore,
+    cwd: rootDir,
+    absolute: true
+  })
+
+  await Promise.all(files.map(async (p) => {
+    const key = relative(rootDir, p)
+    try {
+      const fileContent = await fs.readFile(p)
+      snapshot[key] = {
+        checksum: await crc32(fileContent).toString(16)
+      }
+    } catch (e) {
+      // TODO: Check for other errors like permission denied
+      snapshot[key] = {
+        exists: false
+      }
+    }
+  }))
+
+  return snapshot
+}
diff --git a/packages/cli/src/utils/serve.js b/packages/cli/src/utils/serve.js
new file mode 100644
index 000000000..c272d7f4a
--- /dev/null
+++ b/packages/cli/src/utils/serve.js
@@ -0,0 +1,73 @@
+import { promises as fs } from 'fs'
+import { join, extname, basename } from 'path'
+import connect from 'connect'
+import serveStatic from 'serve-static'
+import compression from 'compression'
+import { getNuxtConfig } from '@nuxt/config'
+import { showBanner } from '../utils/banner'
+import * as imports from '../imports'
+
+export async function serve (cmd) {
+  const _config = await cmd.getNuxtConfig({ dev: false })
+
+  // add default options
+  const options = getNuxtConfig(_config)
+
+  try {
+    // overwrites with build config
+    const buildConfig = require(join(options.buildDir, 'nuxt/config.json'))
+    options.target = buildConfig.target
+  } catch (err) { }
+
+  const distStat = await fs.stat(options.generate.dir).catch(err => null) // eslint-disable-line handle-callback-err
+  if (!distStat || !distStat.isDirectory()) {
+    throw new Error('Output directory `' + basename(options.generate.dir) + '/` does not exists, please use `nuxt generate` before `nuxt start` for static target.')
+  }
+  const app = connect()
+  app.use(compression({ threshold: 0 }))
+  app.use(
+    options.router.base,
+    serveStatic(options.generate.dir, {
+      extensions: ['html']
+    })
+  )
+  if (options.generate.fallback) {
+    const fallbackFile = await fs.readFile(join(options.generate.dir, options.generate.fallback), 'utf-8')
+    app.use((req, res, next) => {
+      const ext = extname(req.url) || '.html'
+
+      if (ext !== '.html') {
+        return next()
+      }
+      res.writeHeader(200, {
+        'Content-Type': 'text/html'
+      })
+      res.write(fallbackFile)
+      res.end()
+    })
+  }
+
+  const { port, host, socket, https } = options.server
+  const { Listener } = await imports.server()
+  const listener = new Listener({
+    port,
+    host,
+    socket,
+    https,
+    app,
+    dev: true, // try another port if taken
+    baseURL: options.router.base
+  })
+
+  await listener.listen()
+
+  const { Nuxt } = await imports.core()
+
+  showBanner({
+    constructor: Nuxt,
+    options,
+    server: {
+      listeners: [listener]
+    }
+  }, false)
+}
diff --git a/packages/config/src/config/generate.js b/packages/config/src/config/generate.js
index 5ac7fe11c..8177238e3 100644
--- a/packages/config/src/config/generate.js
+++ b/packages/config/src/config/generate.js
@@ -1,4 +1,3 @@
-
 export default () => ({
   dir: 'dist',
   routes: [],
@@ -8,6 +7,12 @@ export default () => ({
   subFolders: true,
   fallback: '200.html',
   crawler: true,
+  cache: {
+    ignore: [],
+    globbyOptions: {
+      gitignore: true
+    }
+  },
   staticAssets: {
     base: undefined, // Default: "/_nuxt/static:
     versionBase: undefined, // Default: "_nuxt/static/{version}""
diff --git a/packages/config/src/options.js b/packages/config/src/options.js
index 0697e1613..16953da53 100644
--- a/packages/config/src/options.js
+++ b/packages/config/src/options.js
@@ -115,9 +115,9 @@ export function getNuxtConfig (_options) {
     options.router.base += '/'
   }
 
-  // Alias export to generate
-  // TODO: switch to export by default for nuxt3
+  // Legacy support for export
   if (options.export) {
+    consola.warn('export option is deprecated and will be removed in a future version! Please switch to generate')
     options.generate = defu(options.export, options.generate)
   }
   exports.export = options.generate
diff --git a/packages/core/src/nuxt.js b/packages/core/src/nuxt.js
index 1d7c791d4..6e55a6d99 100644
--- a/packages/core/src/nuxt.js
+++ b/packages/core/src/nuxt.js
@@ -35,7 +35,17 @@ export default class Nuxt extends Hookable {
         to: '_render:context',
         message: '`render:routeContext(nuxt)` is deprecated, Please use `vue-renderer:ssr:context(context)`'
       },
-      showReady: 'webpack:done'
+      showReady: 'webpack:done',
+      // Introduced in 2.13
+      'export:done': 'generate:done',
+      'export:before': 'generate:before',
+      'export:extendRoutes': 'generate:extendRoutes',
+      'export:distRemoved': 'generate:distRemoved',
+      'export:distCopied': 'generate:distCopied',
+      'export:route': 'generate:route',
+      'export:routeFailed': 'generate:routeFailed',
+      'export:page': 'generate:page',
+      'export:routeCreated': 'generate:routeCreated'
     })
 
     // Add Legacy aliases
diff --git a/packages/generator/src/generator.js b/packages/generator/src/generator.js
index 52f96e694..194f54f37 100644
--- a/packages/generator/src/generator.js
+++ b/packages/generator/src/generator.js
@@ -6,16 +6,16 @@ import defu from 'defu'
 import htmlMinifier from 'html-minifier'
 import { parse } from 'node-html-parser'
 
-import { isFullStatic, flatRoutes, isString, isUrl, promisifyRoute, waitFor, TARGETS } from '@nuxt/utils'
+import { isFullStatic, flatRoutes, isString, isUrl, promisifyRoute, waitFor } from '@nuxt/utils'
 
 export default class Generator {
   constructor (nuxt, builder) {
     this.nuxt = nuxt
     this.options = nuxt.options
     this.builder = builder
-    this.isFullStatic = false
 
     // Set variables
+    this.isFullStatic = isFullStatic(this.options)
     this.staticRoutes = path.resolve(this.options.srcDir, this.options.dir.static)
     this.srcBuiltPath = path.resolve(this.options.buildDir, 'dist', 'client')
     this.distPath = this.options.generate.dir
@@ -23,6 +23,12 @@ export default class Generator {
       this.distPath,
       isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath
     )
+    // Payloads for full static
+    if (this.isFullStatic) {
+      const { staticAssets } = this.options.generate
+      this.staticAssetsDir = path.resolve(this.distNuxtPath, staticAssets.dir, staticAssets.version)
+      this.staticAssetsBase = this.options.generate.staticAssets.versionBase
+    }
 
     // Shared payload
     this._payload = null
@@ -35,18 +41,10 @@ export default class Generator {
     consola.debug('Initializing generator...')
     await this.initiate({ build, init })
 
-    // Payloads for full static
-    if (this.isFullStatic) {
-      consola.info('Full static mode activated')
-      const { staticAssets } = this.options.generate
-      this.staticAssetsDir = path.resolve(this.distNuxtPath, staticAssets.dir, staticAssets.version)
-      this.staticAssetsBase = this.options.generate.staticAssets.versionBase
-    }
-
     consola.debug('Preparing routes for generate...')
     const routes = await this.initRoutes()
 
-    consola.info('Generating pages')
+    consola.info('Generating pages' + (this.isFullStatic ? ' with full static mode' : ''))
     const errors = await this.generateRoutes(routes)
 
     await this.afterGenerate()
@@ -72,23 +70,13 @@ export default class Generator {
 
       // Start build process
       await this.builder.build()
-      this.isFullStatic = isFullStatic(this.options)
     } else {
       const hasBuilt = await fsExtra.exists(path.resolve(this.options.buildDir, 'dist', 'server', 'client.manifest.json'))
       if (!hasBuilt) {
-        const fullStaticArgs = isFullStatic(this.options) ? ' --target static' : ''
         throw new Error(
-          `No build files found in ${this.srcBuiltPath}.\nPlease run \`nuxt build${fullStaticArgs}\` before calling \`nuxt export\``
+          `No build files found in ${this.srcBuiltPath}.\nPlease run \`nuxt build\``
         )
       }
-      const config = this.getBuildConfig()
-      if (!config || (config.target !== TARGETS.static && !this.options._legacyGenerate)) {
-        throw new Error(
-          `In order to use \`nuxt export\`, you need to run \`nuxt build --target static\``
-        )
-      }
-      this.isFullStatic = config.isFullStatic
-      this.options.render.ssr = config.ssr
     }
 
     // Initialize dist directory
@@ -366,9 +354,15 @@ export default class Generator {
     }
 
     // Call hook to let user update the path & html
-    const page = { route, path: fileName, html, exclude: false }
+    const page = {
+      route,
+      path: fileName,
+      html,
+      exclude: false,
+      errors: pageErrors
+    }
+    page.page = page // Backward compatibility for export:page hook
     await this.nuxt.callHook('generate:page', page)
-    await this.nuxt.callHook('export:page', { page, errors: pageErrors })
 
     if (page.exclude) {
       return false
diff --git a/packages/types/config/generate.d.ts b/packages/types/config/generate.d.ts
index f0fdfebb8..632165eb7 100644
--- a/packages/types/config/generate.d.ts
+++ b/packages/types/config/generate.d.ts
@@ -1,3 +1,5 @@
+import { GlobbyOptions } from 'globby'
+
 /**
  * NuxtOptionsGenerate
  * Documentation: https://nuxtjs.org/api/configuration-generate
@@ -18,4 +20,8 @@ export interface NuxtOptionsGenerate {
   interval?: number
   routes?: NuxtOptionsGenerateRoute[] | NuxtOptionsGenerateRoutesFunction | NuxtOptionsGenerateRoutesFunctionWithCallback
   subFolders?: boolean
+  cache?: false | {
+    ignore?: string[] | function,
+    globbyOptions?: GlobbyOptions
+  }
 }
diff --git a/packages/vue-app/src/index.js b/packages/vue-app/src/index.js
index 5cf5d1d85..11e0d65a7 100644
--- a/packages/vue-app/src/index.js
+++ b/packages/vue-app/src/index.js
@@ -5,7 +5,6 @@ export const template = {
   dependencies,
   dir: path.join(__dirname, '..', 'template'),
   files: [
-    'nuxt/config.json',
     'App.js',
     'client.js',
     'index.js',
diff --git a/packages/vue-app/template/client.js b/packages/vue-app/template/client.js
index 1d3ba1fd3..b3f37b9d9 100644
--- a/packages/vue-app/template/client.js
+++ b/packages/vue-app/template/client.js
@@ -525,10 +525,12 @@ async function render (to, from, next) {
       <% } %>
 
       <% if (isFullStatic && store) { %>
-      // Replay store mutations, catching to avoid error page on SPA fallback
-      promises.push(this.fetchPayload(to.path).then(payload => {
-        payload.mutations.forEach(m => { this.$store.commit(m[0], m[1]) })
-      }).catch(err => null))
+      if (!this.isPreview && !spaFallback) {
+        // Replay store mutations, catching to avoid error page on SPA fallback
+        promises.push(this.fetchPayload(to.path).then(payload => {
+          payload.mutations.forEach(m => { this.$store.commit(m[0], m[1]) })
+        }).catch(err => null))
+      }
       <% } %>
 
       // Check disabled page loading
diff --git a/packages/vue-app/template/nuxt/config.json b/packages/vue-app/template/nuxt/config.json
deleted file mode 100644
index 4d88401d1..000000000
--- a/packages/vue-app/template/nuxt/config.json
+++ /dev/null
@@ -1,5 +0,0 @@
-<%= JSON.stringify({
-  isFullStatic: isFullStatic,
-  ssr: nuxtOptions.render.ssr,
-  target: nuxtOptions.target
-}, null, 2) %>


Test patch:
diff --git a/packages/cli/test/unit/export.test.js b/packages/cli/test/unit/export.test.js
index 5651237b9..c75b116c7 100644
--- a/packages/cli/test/unit/export.test.js
+++ b/packages/cli/test/unit/export.test.js
@@ -29,29 +29,29 @@ describe('export', () => {
 
     expect(generator).toHaveBeenCalled()
     expect(generator.mock.calls[0][0].init).toBe(true)
-    expect(generator.mock.calls[0][0].build).toBe(false)
+    // expect(generator.mock.calls[0][0].build).toBe(false)
   })
 
   test('force-exits by default', async () => {
     mockGetNuxt({ generate: { dir: 'dist' } })
     mockGetGenerator()
 
-    const cmd = NuxtCommand.from(exportCommand, ['export', '.'])
+    const cmd = NuxtCommand.from(exportCommand, ['generate', '.'])
     await cmd.run()
 
     expect(utils.forceExit).toHaveBeenCalledTimes(1)
-    expect(utils.forceExit).toHaveBeenCalledWith('export', 5)
+    expect(utils.forceExit).toHaveBeenCalledWith('generate', 5)
   })
 
   test('can set force exit explicitly', async () => {
     mockGetNuxt({ generate: { dir: 'dist' } })
     mockGetGenerator()
 
-    const cmd = NuxtCommand.from(exportCommand, ['export', '.', '--force-exit'])
+    const cmd = NuxtCommand.from(exportCommand, ['generate', '.', '--force-exit'])
     await cmd.run()
 
     expect(utils.forceExit).toHaveBeenCalledTimes(1)
-    expect(utils.forceExit).toHaveBeenCalledWith('export', false)
+    expect(utils.forceExit).toHaveBeenCalledWith('generate', false)
   })
 
   test('can disable force exit explicitly', async () => {
@@ -97,7 +97,7 @@ describe('export', () => {
     mockGetGenerator(() => ({ errors: [{ type: 'dummy' }] }))
 
     const cmd = NuxtCommand.from(exportCommand, ['export', '.', '--fail-on-error'])
-    await expect(cmd.run()).rejects.toThrow('Error exporting pages, exiting with non-zero code')
+    await expect(cmd.run()).rejects.toThrow('Error generating pages, exiting with non-zero code')
   })
 
   test('do not throw an error when fail-on-error disabled and page errors', async () => {
diff --git a/packages/cli/test/unit/serve.test.js b/packages/cli/test/unit/serve.test.js
index 21f88f682..c22911316 100644
--- a/packages/cli/test/unit/serve.test.js
+++ b/packages/cli/test/unit/serve.test.js
@@ -19,16 +19,10 @@ describe('serve', () => {
     expect(typeof serve.run).toBe('function')
   })
 
-  test('error if starts with server target', () => {
-    mockGetNuxtConfig({ target: TARGETS.server })
-    const cmd = NuxtCommand.from(serve)
-    expect(cmd.run()).rejects.toThrow(new Error('You cannot use `nuxt serve` with server target, please use `nuxt start`'))
-  })
-
   test('error if dist/ does not exists', () => {
     mockGetNuxtConfig({ target: TARGETS.static })
     const cmd = NuxtCommand.from(serve)
-    expect(cmd.run()).rejects.toThrow(new Error('Output directory `dist/` does not exists, please run `nuxt export` before `nuxt serve`.'))
+    expect(cmd.run()).rejects.toThrow(new Error('Output directory `dist/` does not exists, please use `nuxt generate` before `nuxt start` for static target.'))
   })
 
   test('no error if dist/ dir exists', async () => {
diff --git a/packages/cli/test/unit/start.test.js b/packages/cli/test/unit/start.test.js
index f3781d921..ce09f3b58 100644
--- a/packages/cli/test/unit/start.test.js
+++ b/packages/cli/test/unit/start.test.js
@@ -1,5 +1,4 @@
 import fs from 'fs-extra'
-import { TARGETS } from '@nuxt/utils'
 import * as utils from '../../src/utils/'
 import { consola, mockGetNuxtStart, mockGetNuxtConfig, NuxtCommand } from '../utils'
 
@@ -36,13 +35,6 @@ describe('start', () => {
     expect(consola.fatal).not.toHaveBeenCalled()
   })
 
-  test('error if starts with static target', () => {
-    mockGetNuxtStart()
-    mockGetNuxtConfig({ target: TARGETS.static })
-    const cmd = NuxtCommand.from(start)
-    expect(cmd.run()).rejects.toThrow(new Error('You cannot use `nuxt start` with static target, please use `nuxt export` and `nuxt serve`'))
-  })
-
   test('start doesnt force-exit by default', async () => {
     mockGetNuxtStart()
     mockGetNuxtConfig()
diff --git a/packages/config/test/__snapshots__/options.test.js.snap b/packages/config/test/__snapshots__/options.test.js.snap
index 1df80864a..5872aa6c7 100644
--- a/packages/config/test/__snapshots__/options.test.js.snap
+++ b/packages/config/test/__snapshots__/options.test.js.snap
@@ -196,6 +196,12 @@ Object {
     "server": true,
   },
   "generate": Object {
+    "cache": Object {
+      "globbyOptions": Object {
+        "gitignore": true,
+      },
+      "ignore": Array [],
+    },
     "concurrency": 500,
     "crawler": true,
     "dir": "/var/nuxt/test/dist",
diff --git a/packages/config/test/config/__snapshots__/index.test.js.snap b/packages/config/test/config/__snapshots__/index.test.js.snap
index 4b231057b..8c12ed613 100644
--- a/packages/config/test/config/__snapshots__/index.test.js.snap
+++ b/packages/config/test/config/__snapshots__/index.test.js.snap
@@ -175,6 +175,12 @@ Object {
     "server": true,
   },
   "generate": Object {
+    "cache": Object {
+      "globbyOptions": Object {
+        "gitignore": true,
+      },
+      "ignore": Array [],
+    },
     "concurrency": 500,
     "crawler": true,
     "dir": "dist",
@@ -548,6 +554,12 @@ Object {
     "server": true,
   },
   "generate": Object {
+    "cache": Object {
+      "globbyOptions": Object {
+        "gitignore": true,
+      },
+      "ignore": Array [],
+    },
     "concurrency": 500,
     "crawler": true,
     "dir": "dist",
diff --git a/test/utils/setup-env.js b/test/utils/setup-env.js
new file mode 100644
index 000000000..95dc33e23
--- /dev/null
+++ b/test/utils/setup-env.js
@@ -0,0 +1,23 @@
+import consola from 'consola'
+import env from 'std-env'
+import exit from 'exit'
+
+const isWin = env.windows
+
+describe.win = isWin ? describe : describe.skip
+test.win = isWin ? test : test.skip
+
+describe.posix = !isWin ? describe : describe.skip
+test.posix = !isWin ? test : test.skip
+
+jest.setTimeout(60000)
+
+consola.mockTypes(() => jest.fn())
+
+function errorTrap (error) {
+  process.stderr.write('\n' + error.stack + '\n')
+  exit(1)
+}
+
+process.on('unhandledRejection', errorTrap)
+process.on('uncaughtException', errorTrap)
diff --git a/test/utils/setup.js b/test/utils/setup.js
index 04ffa5033..72ba9147c 100644
--- a/test/utils/setup.js
+++ b/test/utils/setup.js
@@ -1,26 +1,6 @@
-import consola from 'consola'
-import chalk from 'chalk'
-import env from 'std-env'
-import exit from 'exit'
-
-const isWin = env.windows
-
-describe.win = isWin ? describe : describe.skip
-test.win = isWin ? test : test.skip
-
-describe.posix = !isWin ? describe : describe.skip
-test.posix = !isWin ? test : test.skip
+process.env.FORCE_COLOR = 0
 
+const chalk = require('chalk')
 chalk.level = 0
-
-jest.setTimeout(60000)
-
-consola.mockTypes(() => jest.fn())
-
-function errorTrap (error) {
-  process.stderr.write('\n' + error.stack + '\n')
-  exit(1)
-}
-
-process.on('unhandledRejection', errorTrap)
-process.on('uncaughtException', errorTrap)
+chalk.supportsColor = false
+process.env.FORCE_COLOR = 0