mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-26 23:52:06 +00:00
feat: options.target and full-static export (#6159)
* 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 <manniL@gmx.net> * 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 <indirectlylit@users.noreply.github.com> * Update packages/cli/src/options/common.js Co-Authored-By: Alexander Lichter <manniL@gmx.net> * 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 <manniL@gmx.net> * 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 <indirectlylit@users.noreply.github.com> * Update packages/cli/src/options/common.js Co-Authored-By: Alexander Lichter <manniL@gmx.net> * 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 <pyapar@gmail.com> * 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) <clark.duxin@gmail.com> Co-authored-by: Alexander Lichter <manniL@gmx.net> Co-authored-by: Pooya Parsa <pooya@pi0.ir> Co-authored-by: Devon Rueckner <indirectlylit@users.noreply.github.com> Co-authored-by: Pooya Parsa <pyapar@gmail.com>
This commit is contained in:
parent
a0db3644f6
commit
917adc0618
@ -12,13 +12,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
async asyncData ({ params }) {
|
||||
// We can use async/await ES6 feature
|
||||
const { data } = await axios.get(`https://jsonplaceholder.typicode.com/posts/${params.id}`)
|
||||
return { post: data }
|
||||
const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`).then(res => res.json())
|
||||
|
||||
return { post }
|
||||
},
|
||||
head () {
|
||||
return {
|
||||
|
@ -7,6 +7,9 @@
|
||||
<NuxtLink :to="{ name: 'posts-id', params: { id: post.id } }">
|
||||
{{ post.title }}
|
||||
</NuxtLink>
|
||||
<NuxtLink :to="{ name: 'posts-id', params: { id: post.id } }">
|
||||
{{ post.title }}
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
@ -18,14 +21,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
asyncData ({ req, params }) {
|
||||
// We can return a Promise instead of calling the callback
|
||||
return axios.get('https://jsonplaceholder.typicode.com/posts')
|
||||
.then((res) => {
|
||||
return { posts: res.data.slice(0, 5) }
|
||||
return fetch('https://jsonplaceholder.typicode.com/posts')
|
||||
.then(res => res.json())
|
||||
.then((data) => {
|
||||
return { posts: data.slice(0, 5) }
|
||||
})
|
||||
},
|
||||
head: {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import path from 'path'
|
||||
import chalk from 'chalk'
|
||||
import chokidar from 'chokidar'
|
||||
import consola from 'consola'
|
||||
import fsExtra from 'fs-extra'
|
||||
@ -7,7 +8,6 @@ import hash from 'hash-sum'
|
||||
import pify from 'pify'
|
||||
import upath from 'upath'
|
||||
import semver from 'semver'
|
||||
import chalk from 'chalk'
|
||||
|
||||
import debounce from 'lodash/debounce'
|
||||
import omit from 'lodash/omit'
|
||||
@ -23,7 +23,9 @@ import {
|
||||
determineGlobals,
|
||||
stripWhitespace,
|
||||
isIndexFileAndFolder,
|
||||
scanRequireTree
|
||||
scanRequireTree,
|
||||
TARGETS,
|
||||
isFullStatic
|
||||
} from '@nuxt/utils'
|
||||
|
||||
import Ignore from './ignore'
|
||||
@ -102,6 +104,7 @@ export default class Builder {
|
||||
}
|
||||
|
||||
forGenerate () {
|
||||
this.options.target = TARGETS.static
|
||||
this.bundleBuilder.forGenerate()
|
||||
}
|
||||
|
||||
@ -122,6 +125,13 @@ export default class Builder {
|
||||
consola.info('Initial build may take a while')
|
||||
} else {
|
||||
consola.info('Production build')
|
||||
if (this.options.render.ssr) {
|
||||
consola.info(`Bundling for ${chalk.bold.yellow('server')} and ${chalk.bold.green('client')} side`)
|
||||
} else {
|
||||
consola.info(`Bundling only for ${chalk.bold.green('client')} side`)
|
||||
}
|
||||
const target = isFullStatic(this.options) ? 'full static' : this.options.target
|
||||
consola.info(`Target: ${chalk.bold.cyan(target)}`)
|
||||
}
|
||||
|
||||
// Wait for nuxt ready
|
||||
|
@ -3,7 +3,7 @@ export default class BuildContext {
|
||||
this._builder = builder
|
||||
this.nuxt = builder.nuxt
|
||||
this.options = builder.nuxt.options
|
||||
this.isStatic = false
|
||||
this.target = builder.nuxt.options.target
|
||||
}
|
||||
|
||||
get buildOptions () {
|
||||
|
@ -4,7 +4,7 @@ import uniqBy from 'lodash/uniqBy'
|
||||
import serialize from 'serialize-javascript'
|
||||
|
||||
import devalue from '@nuxt/devalue'
|
||||
import { r, wp, wChunk, serializeFunction } from '@nuxt/utils'
|
||||
import { r, wp, wChunk, serializeFunction, isFullStatic } from '@nuxt/utils'
|
||||
|
||||
export default class TemplateContext {
|
||||
constructor (builder, options) {
|
||||
@ -20,6 +20,7 @@ export default class TemplateContext {
|
||||
uniqBy,
|
||||
isDev: options.dev,
|
||||
isTest: options.test,
|
||||
isFullStatic: isFullStatic(options),
|
||||
debug: options.debug,
|
||||
buildIndicator: options.dev && options.build.indicator,
|
||||
vue: { config: options.vue.config },
|
||||
|
@ -5,6 +5,9 @@ export const createNuxt = () => ({
|
||||
build: {
|
||||
watch: []
|
||||
},
|
||||
render: {
|
||||
ssr: true
|
||||
},
|
||||
router: {},
|
||||
dir: {
|
||||
app: 'app'
|
||||
|
@ -35,6 +35,7 @@ describe('builder: builder build', () => {
|
||||
nuxt.options.dir = { pages: '/var/nuxt/src/pages' }
|
||||
nuxt.options.build.template = { dir: '/var/nuxt/src/template' }
|
||||
nuxt.options.build.createRoutes = jest.fn()
|
||||
nuxt.options.render = { ssr: true }
|
||||
|
||||
const bundleBuilder = { build: jest.fn() }
|
||||
const builder = new Builder(nuxt, bundleBuilder)
|
||||
@ -47,7 +48,7 @@ describe('builder: builder build', () => {
|
||||
|
||||
const buildReturn = await builder.build()
|
||||
|
||||
expect(consola.info).toBeCalledTimes(1)
|
||||
expect(consola.info).toBeCalledTimes(3)
|
||||
expect(consola.info).toBeCalledWith('Production build')
|
||||
expect(nuxt.ready).toBeCalledTimes(1)
|
||||
expect(nuxt.callHook).toBeCalledTimes(3)
|
||||
@ -117,6 +118,7 @@ describe('builder: builder build', () => {
|
||||
nuxt.options.buildDir = '/var/nuxt/build'
|
||||
nuxt.options.dir = { pages: '/var/nuxt/src/pages' }
|
||||
nuxt.options.build.createRoutes = jest.fn()
|
||||
nuxt.options.render = { ssr: true }
|
||||
|
||||
const bundleBuilder = { build: jest.fn() }
|
||||
const builder = new Builder(nuxt, bundleBuilder)
|
||||
|
@ -30,6 +30,7 @@ TemplateContext {
|
||||
],
|
||||
"head": "test_head",
|
||||
"isDev": "test_dev",
|
||||
"isFullStatic": false,
|
||||
"isTest": "test_test",
|
||||
"layoutTransition": Object {
|
||||
"name": "test_layout_trans",
|
||||
|
@ -1,15 +1,20 @@
|
||||
import { TARGETS } from '@nuxt/utils'
|
||||
import BuildContext from '../../src/context/build'
|
||||
|
||||
describe('builder: buildContext', () => {
|
||||
test('should construct context', () => {
|
||||
const builder = {
|
||||
nuxt: { options: {} }
|
||||
nuxt: {
|
||||
options: {
|
||||
target: TARGETS.server
|
||||
}
|
||||
}
|
||||
}
|
||||
const context = new BuildContext(builder)
|
||||
expect(context._builder).toEqual(builder)
|
||||
expect(context.nuxt).toEqual(builder.nuxt)
|
||||
expect(context.options).toEqual(builder.nuxt.options)
|
||||
expect(context.isStatic).toEqual(false)
|
||||
expect(context.target).toEqual('server')
|
||||
})
|
||||
|
||||
test('should return builder plugins context', () => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import consola from 'consola'
|
||||
import { MODES, TARGETS } from '@nuxt/utils'
|
||||
import { common, locking } from '../options'
|
||||
import { createLock } from '../utils'
|
||||
|
||||
@ -62,7 +64,7 @@ export default {
|
||||
},
|
||||
async run (cmd) {
|
||||
const config = await cmd.getNuxtConfig({ dev: false, server: false, _build: true })
|
||||
config.server = config.mode === 'spa' && cmd.argv.generate !== false
|
||||
config.server = (config.mode === MODES.spa || config.ssr === false) && cmd.argv.generate !== false
|
||||
const nuxt = await cmd.getNuxt(config)
|
||||
|
||||
if (cmd.argv.lock) {
|
||||
@ -73,7 +75,8 @@ export default {
|
||||
}))
|
||||
}
|
||||
|
||||
if (nuxt.options.mode === 'spa' && cmd.argv.generate !== false) {
|
||||
// TODO: remove if in Nuxt 3
|
||||
if (nuxt.options.mode === MODES.spa && nuxt.options.target === TARGETS.server && cmd.argv.generate !== false) {
|
||||
// Build + Generate for static deployment
|
||||
const generator = await cmd.getGenerator(nuxt)
|
||||
await generator.generate({ build: true })
|
||||
@ -81,6 +84,9 @@ export default {
|
||||
// Build only
|
||||
const builder = await cmd.getBuilder(nuxt)
|
||||
await builder.build()
|
||||
|
||||
const nextCommand = nuxt.options.target === TARGETS.static ? 'nuxt export' : 'nuxt start'
|
||||
consola.info('Ready to run `' + (nextCommand) + '`')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
50
packages/cli/src/commands/export.js
Normal file
50
packages/cli/src/commands/export.js
Normal file
@ -0,0 +1,50 @@
|
||||
import path from 'path'
|
||||
import consola from 'consola'
|
||||
import { TARGETS } from '@nuxt/utils'
|
||||
import { common, locking } from '../options'
|
||||
import { createLock } from '../utils'
|
||||
|
||||
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,
|
||||
_build: cmd.argv.build
|
||||
})
|
||||
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()
|
||||
|
||||
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')
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { TARGETS } from '@nuxt/utils'
|
||||
import { common, locking } from '../options'
|
||||
import { normalizeArg, createLock } from '../utils'
|
||||
|
||||
@ -53,14 +54,25 @@ export default {
|
||||
}
|
||||
},
|
||||
async run (cmd) {
|
||||
const config = await cmd.getNuxtConfig({ dev: false, _generate: true, _build: cmd.argv.build })
|
||||
const config = await cmd.getNuxtConfig({
|
||||
dev: false,
|
||||
_build: cmd.argv.build
|
||||
})
|
||||
|
||||
if (config.target === TARGETS.static) {
|
||||
throw new Error("Please use `nuxt export` when using `target: 'static'`")
|
||||
}
|
||||
|
||||
// Forcing static target anyway
|
||||
config.target = TARGETS.static
|
||||
|
||||
// Disable analyze if set by the nuxt config
|
||||
if (!config.build) {
|
||||
config.build = {}
|
||||
}
|
||||
config.build = config.build || {}
|
||||
config.build.analyze = false
|
||||
|
||||
// Set flag to keep the prerendering behaviour
|
||||
config._legacyGenerate = true
|
||||
|
||||
const nuxt = await cmd.getNuxt(config)
|
||||
|
||||
if (cmd.argv.lock) {
|
||||
@ -82,12 +94,14 @@ export default {
|
||||
}
|
||||
|
||||
const generator = await cmd.getGenerator(nuxt)
|
||||
await nuxt.server.listen()
|
||||
|
||||
const { errors } = await generator.generate({
|
||||
init: true,
|
||||
build: cmd.argv.build
|
||||
})
|
||||
|
||||
await nuxt.close()
|
||||
if (cmd.argv['fail-on-error'] && errors.length > 0) {
|
||||
throw new Error('Error generating pages, exiting with non-zero code')
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
const commands = {
|
||||
start: () => import('./start'),
|
||||
serve: () => import('./serve'),
|
||||
dev: () => import('./dev'),
|
||||
build: () => import('./build'),
|
||||
generate: () => import('./generate'),
|
||||
export: () => import('./export'),
|
||||
webpack: () => import('./webpack'),
|
||||
help: () => import('./help')
|
||||
}
|
||||
|
83
packages/cli/src/commands/serve.js
Normal file
83
packages/cli/src/commands/serve.js
Normal file
@ -0,0 +1,83 @@
|
||||
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 { Listener } from '@nuxt/server'
|
||||
import { common, server } from '../options'
|
||||
import { showBanner } from '../utils/banner'
|
||||
import * as imports from '../imports'
|
||||
|
||||
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(
|
||||
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 = 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)
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { TARGETS } from '@nuxt/utils'
|
||||
import { common, server } from '../options'
|
||||
import { showBanner } from '../utils/banner'
|
||||
|
||||
@ -11,6 +12,9 @@ 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`')
|
||||
}
|
||||
const nuxt = await cmd.getNuxt(config)
|
||||
|
||||
// Listen and show ready banner
|
||||
|
@ -28,10 +28,20 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
target: {
|
||||
alias: 't',
|
||||
type: 'string',
|
||||
description: 'Build/start app for a different target, e.g. server, serverless and static',
|
||||
prepare (cmd, options, argv) {
|
||||
if (argv.target) {
|
||||
options.target = argv.target
|
||||
}
|
||||
}
|
||||
},
|
||||
'force-exit': {
|
||||
type: 'boolean',
|
||||
default (cmd) {
|
||||
return ['build', 'generate'].includes(cmd.name)
|
||||
return ['build', 'generate', 'export'].includes(cmd.name)
|
||||
},
|
||||
description: 'Whether Nuxt.js should force exit after the command has finished'
|
||||
},
|
||||
|
@ -20,11 +20,14 @@ export function showBanner (nuxt, showMemoryUsage = true) {
|
||||
const messageLines = []
|
||||
|
||||
// Name and version
|
||||
const { bannerColor } = nuxt.options.cli
|
||||
const { bannerColor, badgeMessages } = nuxt.options.cli
|
||||
titleLines.push(`${chalk[bannerColor].bold('Nuxt.js')} ${nuxt.constructor.version}`)
|
||||
|
||||
// Running mode
|
||||
titleLines.push(`Running in ${nuxt.options.dev ? chalk.bold.blue('development') : chalk.bold.green('production')} mode (${chalk.bold(nuxt.options.mode)})`)
|
||||
const rendering = nuxt.options.render.ssr ? chalk.bold.yellow('server-side') : chalk.bold.yellow('client-side')
|
||||
const envMode = nuxt.options.dev ? chalk.bold.blue('development') : chalk.bold.green('production')
|
||||
const sentence = `Running in ${envMode}, with ${rendering} rendering and ${chalk.bold.cyan(nuxt.options.target)} target.`
|
||||
titleLines.push(sentence)
|
||||
|
||||
if (showMemoryUsage) {
|
||||
titleLines.push(getFormattedMemoryUsage())
|
||||
@ -36,8 +39,8 @@ export function showBanner (nuxt, showMemoryUsage = true) {
|
||||
}
|
||||
|
||||
// Add custom badge messages
|
||||
if (nuxt.options.cli.badgeMessages.length) {
|
||||
messageLines.push('', ...nuxt.options.cli.badgeMessages)
|
||||
if (badgeMessages.length) {
|
||||
messageLines.push('', ...badgeMessages)
|
||||
}
|
||||
|
||||
process.stdout.write(successBox(messageLines.join('\n'), titleLines.join('\n')))
|
||||
|
@ -1,6 +1,7 @@
|
||||
import path from 'path'
|
||||
import defaultsDeep from 'lodash/defaultsDeep'
|
||||
import { loadNuxtConfig as _loadNuxtConfig, getDefaultNuxtConfig } from '@nuxt/config'
|
||||
import { MODES } from '@nuxt/utils'
|
||||
|
||||
export async function loadNuxtConfig (argv, configContext) {
|
||||
const rootDir = path.resolve(argv._[0] || '.')
|
||||
@ -14,7 +15,8 @@ export async function loadNuxtConfig (argv, configContext) {
|
||||
})
|
||||
|
||||
// Nuxt Mode
|
||||
options.mode = (argv.spa && 'spa') || (argv.universal && 'universal') || options.mode
|
||||
options.mode =
|
||||
(argv.spa && MODES.spa) || (argv.universal && MODES.universal) || options.mode
|
||||
|
||||
// Server options
|
||||
options.server = defaultsDeep({
|
||||
|
@ -18,6 +18,9 @@ exports[`cli/command builds help text 1`] = `
|
||||
--modern, -m Build/Start app for
|
||||
modern browsers, e.g. server, client and
|
||||
false
|
||||
--target, -t Build/start app for a
|
||||
different target, e.g. server,
|
||||
serverless and static
|
||||
--force-exit Whether Nuxt.js
|
||||
should force exit after the command has
|
||||
finished
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { MODES, TARGETS } from '@nuxt/utils'
|
||||
import * as utils from '../../src/utils'
|
||||
import { mockGetNuxt, mockGetBuilder, mockGetGenerator, NuxtCommand } from '../utils'
|
||||
|
||||
@ -23,7 +24,7 @@ describe('build', () => {
|
||||
|
||||
test('builds on universal mode', async () => {
|
||||
mockGetNuxt({
|
||||
mode: 'universal',
|
||||
mode: MODES.universal,
|
||||
build: {
|
||||
analyze: true
|
||||
}
|
||||
@ -37,7 +38,8 @@ describe('build', () => {
|
||||
|
||||
test('generates on spa mode', async () => {
|
||||
mockGetNuxt({
|
||||
mode: 'spa',
|
||||
mode: MODES.spa,
|
||||
target: TARGETS.server,
|
||||
build: {
|
||||
analyze: false
|
||||
}
|
||||
@ -51,7 +53,7 @@ describe('build', () => {
|
||||
|
||||
test('build with devtools', async () => {
|
||||
mockGetNuxt({
|
||||
mode: 'universal'
|
||||
mode: MODES.universal
|
||||
})
|
||||
const builder = mockGetBuilder(Promise.resolve())
|
||||
|
||||
@ -67,7 +69,7 @@ describe('build', () => {
|
||||
|
||||
test('build with modern mode', async () => {
|
||||
mockGetNuxt({
|
||||
mode: 'universal'
|
||||
mode: MODES.universal
|
||||
})
|
||||
mockGetBuilder(Promise.resolve())
|
||||
|
||||
@ -114,7 +116,7 @@ describe('build', () => {
|
||||
|
||||
test('build locks project by default', async () => {
|
||||
mockGetNuxt({
|
||||
mode: 'universal'
|
||||
mode: MODES.universal
|
||||
})
|
||||
mockGetBuilder(Promise.resolve())
|
||||
|
||||
@ -131,7 +133,7 @@ describe('build', () => {
|
||||
|
||||
test('build can disable locking', async () => {
|
||||
mockGetNuxt({
|
||||
mode: 'universal'
|
||||
mode: MODES.universal
|
||||
})
|
||||
mockGetBuilder(Promise.resolve())
|
||||
|
||||
|
@ -21,7 +21,7 @@ describe('cli/command', () => {
|
||||
const cmd = new Command({ options: allOptions })
|
||||
const minimistOptions = cmd._getMinimistOptions()
|
||||
|
||||
expect(minimistOptions.string.length).toBe(5)
|
||||
expect(minimistOptions.string.length).toBe(6)
|
||||
expect(minimistOptions.boolean.length).toBe(5)
|
||||
expect(minimistOptions.alias.c).toBe('config-file')
|
||||
expect(minimistOptions.default.c).toBe(common['config-file'].default)
|
||||
|
118
packages/cli/test/unit/export.test.js
Normal file
118
packages/cli/test/unit/export.test.js
Normal file
@ -0,0 +1,118 @@
|
||||
import * as utils from '../../src/utils'
|
||||
import { mockGetNuxt, mockGetGenerator, NuxtCommand } from '../utils'
|
||||
|
||||
describe('export', () => {
|
||||
let exportCommand
|
||||
|
||||
beforeAll(async () => {
|
||||
exportCommand = await import('../../src/commands/export').then(m => m.default)
|
||||
jest.spyOn(process, 'exit').mockImplementation(code => code)
|
||||
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
|
||||
jest.spyOn(utils, 'createLock').mockImplementation(() => () => {})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
process.exit.mockRestore()
|
||||
})
|
||||
|
||||
afterEach(() => jest.resetAllMocks())
|
||||
|
||||
test('has run function', () => {
|
||||
expect(typeof exportCommand.run).toBe('function')
|
||||
})
|
||||
|
||||
test('init by default, build false', async () => {
|
||||
mockGetNuxt({ generate: { dir: 'dist' } })
|
||||
const generator = mockGetGenerator()
|
||||
|
||||
await NuxtCommand.from(exportCommand).run()
|
||||
|
||||
expect(generator).toHaveBeenCalled()
|
||||
expect(generator.mock.calls[0][0].init).toBe(true)
|
||||
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', '.'])
|
||||
await cmd.run()
|
||||
|
||||
expect(utils.forceExit).toHaveBeenCalledTimes(1)
|
||||
expect(utils.forceExit).toHaveBeenCalledWith('export', 5)
|
||||
})
|
||||
|
||||
test('can set force exit explicitly', async () => {
|
||||
mockGetNuxt({ generate: { dir: 'dist' } })
|
||||
mockGetGenerator()
|
||||
|
||||
const cmd = NuxtCommand.from(exportCommand, ['export', '.', '--force-exit'])
|
||||
await cmd.run()
|
||||
|
||||
expect(utils.forceExit).toHaveBeenCalledTimes(1)
|
||||
expect(utils.forceExit).toHaveBeenCalledWith('export', false)
|
||||
})
|
||||
|
||||
test('can disable force exit explicitly', async () => {
|
||||
mockGetNuxt({ generate: { dir: 'dist' } })
|
||||
mockGetGenerator()
|
||||
|
||||
const cmd = NuxtCommand.from(exportCommand, ['generate', '.', '--no-force-exit'])
|
||||
await cmd.run()
|
||||
|
||||
expect(utils.forceExit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('locks project by default', async () => {
|
||||
const releaseLock = jest.fn(() => Promise.resolve())
|
||||
const createLock = jest.fn(() => releaseLock)
|
||||
jest.spyOn(utils, 'createLock').mockImplementation(createLock)
|
||||
|
||||
mockGetNuxt({ generate: { dir: 'dist' } })
|
||||
mockGetGenerator()
|
||||
|
||||
const cmd = NuxtCommand.from(exportCommand, ['export', '.'])
|
||||
await cmd.run()
|
||||
|
||||
expect(createLock).toHaveBeenCalledTimes(1)
|
||||
expect(releaseLock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('can disable locking', async () => {
|
||||
mockGetNuxt({ generate: { dir: 'dist' } })
|
||||
mockGetGenerator()
|
||||
|
||||
const createLock = jest.fn(() => Promise.resolve())
|
||||
jest.spyOn(utils, 'createLock').mockImplementationOnce(() => createLock)
|
||||
|
||||
const cmd = NuxtCommand.from(exportCommand, ['export', '.', '--no-lock'])
|
||||
await cmd.run()
|
||||
|
||||
expect(createLock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('throw an error when fail-on-error enabled and page errors', async () => {
|
||||
mockGetNuxt({ generate: { dir: 'dist' } })
|
||||
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')
|
||||
})
|
||||
|
||||
test('do not throw an error when fail-on-error disabled and page errors', async () => {
|
||||
mockGetNuxt({ generate: { dir: 'dist' } })
|
||||
mockGetGenerator(() => ({ errors: [{ type: 'dummy' }] }))
|
||||
|
||||
const cmd = NuxtCommand.from(exportCommand, ['export', '.'])
|
||||
await cmd.run()
|
||||
})
|
||||
|
||||
test('do not throw an error when fail-on-error enabled and no page errors', async () => {
|
||||
mockGetNuxt({ generate: { dir: 'dist' } })
|
||||
mockGetGenerator()
|
||||
|
||||
const cmd = NuxtCommand.from(exportCommand, ['export', '.', '--fail-on-error'])
|
||||
await cmd.run()
|
||||
})
|
||||
})
|
44
packages/cli/test/unit/serve.test.js
Normal file
44
packages/cli/test/unit/serve.test.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import { TARGETS } from '@nuxt/utils'
|
||||
import * as utils from '../../src/utils/'
|
||||
import { consola, mockNuxt, mockGetNuxtConfig, NuxtCommand } from '../utils'
|
||||
|
||||
describe('serve', () => {
|
||||
let serve
|
||||
|
||||
beforeAll(async () => {
|
||||
serve = await import('../../src/commands/serve').then(m => m.default)
|
||||
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
test('has run function', () => {
|
||||
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`.'))
|
||||
})
|
||||
|
||||
test('no error if dist/ dir exists', async () => {
|
||||
mockGetNuxtConfig({ target: TARGETS.static })
|
||||
mockNuxt()
|
||||
fs.stat = jest.fn().mockImplementationOnce(() => Promise.resolve(({
|
||||
isDirectory: () => true
|
||||
})))
|
||||
fs.readFile = jest.fn().mockImplementationOnce(() => Promise.resolve('HTML here'))
|
||||
await NuxtCommand.from(serve).run()
|
||||
expect(consola.fatal).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
@ -1,4 +1,5 @@
|
||||
import fs from 'fs-extra'
|
||||
import { TARGETS } from '@nuxt/utils'
|
||||
import * as utils from '../../src/utils/'
|
||||
import { consola, mockGetNuxtStart, mockGetNuxtConfig, NuxtCommand } from '../utils'
|
||||
|
||||
@ -35,8 +36,16 @@ 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()
|
||||
|
||||
const cmd = NuxtCommand.from(start, ['start', '.'])
|
||||
await cmd.run()
|
||||
@ -46,6 +55,7 @@ describe('start', () => {
|
||||
|
||||
test('start can set force exit explicitly', async () => {
|
||||
mockGetNuxtStart()
|
||||
mockGetNuxtConfig()
|
||||
|
||||
const cmd = NuxtCommand.from(start, ['start', '.', '--force-exit'])
|
||||
await cmd.run()
|
||||
@ -56,6 +66,7 @@ describe('start', () => {
|
||||
|
||||
test('start can disable force exit explicitly', async () => {
|
||||
mockGetNuxtStart()
|
||||
mockGetNuxtConfig()
|
||||
|
||||
const cmd = NuxtCommand.from(start, ['start', '.', '--no-force-exit'])
|
||||
await cmd.run()
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { getDefaultNuxtConfig } from '@nuxt/config'
|
||||
import { TARGETS, MODES } from '@nuxt/utils'
|
||||
import { consola } from '../utils'
|
||||
import { loadNuxtConfig } from '../../src/utils/config'
|
||||
import * as utils from '../../src/utils'
|
||||
@ -24,7 +25,7 @@ describe('cli/utils', () => {
|
||||
|
||||
const options = await loadNuxtConfig(argv)
|
||||
expect(options.rootDir).toBe(process.cwd())
|
||||
expect(options.mode).toBe('universal')
|
||||
expect(options.mode).toBe(MODES.universal)
|
||||
expect(options.server.host).toBe('localhost')
|
||||
expect(options.server.port).toBe(3000)
|
||||
expect(options.server.socket).not.toBeDefined()
|
||||
@ -40,7 +41,7 @@ describe('cli/utils', () => {
|
||||
const options = await loadNuxtConfig(argv)
|
||||
expect(options.testOption).toBe(true)
|
||||
expect(options.rootDir).toBe('/some/path')
|
||||
expect(options.mode).toBe('spa')
|
||||
expect(options.mode).toBe(MODES.spa)
|
||||
expect(options.server.host).toBe('nuxt-host')
|
||||
expect(options.server.port).toBe(3001)
|
||||
expect(options.server.socket).toBe('/var/run/nuxt.sock')
|
||||
@ -149,6 +150,9 @@ describe('cli/utils', () => {
|
||||
|
||||
showBanner({
|
||||
options: {
|
||||
render: {
|
||||
ssr: true
|
||||
},
|
||||
cli: {
|
||||
badgeMessages,
|
||||
bannerColor
|
||||
@ -179,6 +183,9 @@ describe('cli/utils', () => {
|
||||
cli: {
|
||||
badgeMessages: [],
|
||||
bannerColor: 'green'
|
||||
},
|
||||
render: {
|
||||
ssr: false
|
||||
}
|
||||
},
|
||||
server: {
|
||||
@ -193,6 +200,37 @@ describe('cli/utils', () => {
|
||||
stdout.mockRestore()
|
||||
})
|
||||
|
||||
test('showBanner does print env, rendering mode and target', () => {
|
||||
const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => {})
|
||||
const successBox = jest.fn().mockImplementation((m, t) => t + m)
|
||||
jest.spyOn(fmt, 'successBox').mockImplementation(successBox)
|
||||
|
||||
showBanner({
|
||||
options: {
|
||||
dev: false,
|
||||
target: TARGETS.static,
|
||||
render: {
|
||||
ssr: false
|
||||
},
|
||||
cli: {
|
||||
bannerColor: 'green',
|
||||
badgeMessages: []
|
||||
}
|
||||
},
|
||||
server: {
|
||||
listeners: []
|
||||
}
|
||||
}, false)
|
||||
|
||||
expect(successBox).toHaveBeenCalledTimes(1)
|
||||
expect(stdout).toHaveBeenCalledTimes(1)
|
||||
expect(stdout).toHaveBeenCalledWith(expect.stringMatching('Nuxt.js'))
|
||||
expect(stdout).toHaveBeenCalledWith(expect.stringMatching('Running in production'))
|
||||
expect(stdout).toHaveBeenCalledWith(expect.stringMatching('client-side rendering'))
|
||||
expect(stdout).toHaveBeenCalledWith(expect.stringMatching('static target'))
|
||||
stdout.mockRestore()
|
||||
})
|
||||
|
||||
test('showMemoryUsage prints memory usage', () => {
|
||||
showMemoryUsage()
|
||||
|
||||
|
@ -22,6 +22,10 @@ export const mockGetNuxt = (options = {}, implementation) => {
|
||||
Command.prototype.getNuxt = jest.fn().mockImplementationOnce(() => {
|
||||
return Object.assign({
|
||||
hook: jest.fn(),
|
||||
server: {
|
||||
listen: jest.fn()
|
||||
},
|
||||
close: jest.fn(),
|
||||
options
|
||||
}, implementation)
|
||||
})
|
||||
@ -68,8 +72,9 @@ export const mockGetNuxtStart = (ssr) => {
|
||||
return { listen }
|
||||
}
|
||||
|
||||
export const mockGetNuxtConfig = () => {
|
||||
export const mockGetNuxtConfig = (config = {}) => {
|
||||
const spy = jest.fn()
|
||||
spy.mockReturnValue(config)
|
||||
Command.prototype.getNuxtConfig = spy
|
||||
return spy
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import capitalize from 'lodash/capitalize'
|
||||
import env from 'std-env'
|
||||
import { TARGETS, MODES } from '@nuxt/utils'
|
||||
|
||||
export default () => ({
|
||||
// Env
|
||||
@ -8,8 +9,15 @@ export default () => ({
|
||||
debug: undefined, // = dev
|
||||
env: {},
|
||||
|
||||
// Target
|
||||
target: TARGETS.server,
|
||||
|
||||
// Rendering
|
||||
ssr: true,
|
||||
|
||||
// TODO: remove in Nuxt 3
|
||||
// Mode
|
||||
mode: 'universal',
|
||||
mode: MODES.universal,
|
||||
modern: undefined,
|
||||
|
||||
globalName: undefined,
|
||||
@ -53,17 +61,6 @@ export default () => ({
|
||||
'**/*.spec.*'
|
||||
],
|
||||
|
||||
// Generate
|
||||
generate: {
|
||||
dir: 'dist',
|
||||
routes: [],
|
||||
exclude: [],
|
||||
concurrency: 500,
|
||||
interval: 0,
|
||||
subFolders: true,
|
||||
fallback: '200.html'
|
||||
},
|
||||
|
||||
// Watch
|
||||
watch: [],
|
||||
watchers: {
|
||||
|
17
packages/config/src/config/generate.js
Normal file
17
packages/config/src/config/generate.js
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
export default () => ({
|
||||
dir: 'dist',
|
||||
routes: [],
|
||||
exclude: [],
|
||||
concurrency: 500,
|
||||
interval: 0,
|
||||
subFolders: true,
|
||||
fallback: '200.html',
|
||||
crawler: true,
|
||||
staticAssets: {
|
||||
base: undefined, // Default: "/_nuxt/static:
|
||||
versionBase: undefined, // Default: "_nuxt/static/{version}""
|
||||
dir: 'static',
|
||||
version: undefined // Default: "{timeStampSec}"
|
||||
}
|
||||
})
|
@ -9,6 +9,7 @@ import render from './render'
|
||||
import router from './router'
|
||||
import server from './server'
|
||||
import cli from './cli'
|
||||
import generate from './generate'
|
||||
|
||||
export const defaultNuxtConfigFile = 'nuxt.config'
|
||||
|
||||
@ -26,6 +27,7 @@ export function getDefaultNuxtConfig (options = {}) {
|
||||
render: render(),
|
||||
router: router(),
|
||||
server: server(options),
|
||||
cli: cli()
|
||||
cli: cli(),
|
||||
generate: generate()
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { MODES } from '@nuxt/utils'
|
||||
|
||||
export default () => ({
|
||||
universal: {
|
||||
[MODES.universal]: {
|
||||
build: {
|
||||
ssr: true
|
||||
},
|
||||
@ -7,7 +9,7 @@ export default () => ({
|
||||
ssr: true
|
||||
}
|
||||
},
|
||||
spa: {
|
||||
[MODES.spa]: {
|
||||
build: {
|
||||
ssr: false
|
||||
},
|
||||
|
@ -13,5 +13,6 @@ export default () => ({
|
||||
stringifyQuery: false,
|
||||
fallback: false,
|
||||
prefetchLinks: true,
|
||||
prefetchPayloads: true,
|
||||
trailingSlash: undefined
|
||||
})
|
||||
|
@ -5,7 +5,7 @@ import defu from 'defu'
|
||||
import pick from 'lodash/pick'
|
||||
import uniq from 'lodash/uniq'
|
||||
import consola from 'consola'
|
||||
import { guardDir, isNonEmptyString, isPureObject, isUrl, getMainModule } from '@nuxt/utils'
|
||||
import { TARGETS, MODES, guardDir, isNonEmptyString, isPureObject, isUrl, getMainModule, urlJoin } from '@nuxt/utils'
|
||||
import { defaultNuxtConfigFile, getDefaultNuxtConfig } from './config'
|
||||
|
||||
export function getNuxtConfig (_options) {
|
||||
@ -89,6 +89,26 @@ export function getNuxtConfig (_options) {
|
||||
|
||||
defaultsDeep(options, nuxtConfig)
|
||||
|
||||
// Target
|
||||
options.target = options.target || 'server'
|
||||
if (!Object.values(TARGETS).includes(options.target)) {
|
||||
consola.warn(`Unknown target: ${options.target}. Falling back to server`)
|
||||
options.target = 'server'
|
||||
}
|
||||
|
||||
// SSR root option
|
||||
if (options.ssr === false) {
|
||||
options.mode = MODES.spa
|
||||
}
|
||||
|
||||
// Apply mode preset
|
||||
const modePreset = options.modes[options.mode || MODES.universal]
|
||||
|
||||
if (!modePreset) {
|
||||
consola.warn(`Unknown mode: ${options.mode}. Falling back to ${MODES.universal}`)
|
||||
}
|
||||
defaultsDeep(options, modePreset || options.modes[MODES.universal])
|
||||
|
||||
// Sanitize router.base
|
||||
if (!/\/$/.test(options.router.base)) {
|
||||
options.router.base += '/'
|
||||
@ -241,7 +261,7 @@ export function getNuxtConfig (_options) {
|
||||
hashAlgorithm: 'sha256',
|
||||
allowedSources: undefined,
|
||||
policies: undefined,
|
||||
addMeta: Boolean(options._generate),
|
||||
addMeta: Boolean(options.target === TARGETS.static),
|
||||
unsafeInlineCompatibility: false,
|
||||
reportOnly: options.debug
|
||||
})
|
||||
@ -316,14 +336,6 @@ export function getNuxtConfig (_options) {
|
||||
delete options.render.gzip
|
||||
}
|
||||
|
||||
// Apply mode preset
|
||||
const modePreset = options.modes[options.mode || 'universal']
|
||||
|
||||
if (!modePreset) {
|
||||
consola.warn(`Unknown mode: ${options.mode}. Falling back to universal`)
|
||||
}
|
||||
defaultsDeep(options, modePreset || options.modes.universal)
|
||||
|
||||
// If no server-side rendering, add appear true transition
|
||||
if (options.render.ssr === false && options.pageTransition) {
|
||||
options.pageTransition.appear = true
|
||||
@ -436,5 +448,18 @@ export function getNuxtConfig (_options) {
|
||||
.map(([path, handler]) => ({ path, handler }))
|
||||
}
|
||||
|
||||
// Generate staticAssets
|
||||
const { staticAssets } = options.generate
|
||||
if (!staticAssets.version) {
|
||||
staticAssets.version = String(Math.round(Date.now() / 1000))
|
||||
}
|
||||
if (!staticAssets.base) {
|
||||
const publicPath = isUrl(options.build.publicPath) ? '' : options.build.publicPath // "/_nuxt" or custom CDN URL
|
||||
staticAssets.base = urlJoin(publicPath, staticAssets.dir)
|
||||
}
|
||||
if (!staticAssets.versionBase) {
|
||||
staticAssets.versionBase = urlJoin(staticAssets.base, staticAssets.version)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
@ -190,11 +190,18 @@ Object {
|
||||
},
|
||||
"generate": Object {
|
||||
"concurrency": 500,
|
||||
"crawler": true,
|
||||
"dir": "/var/nuxt/test/dist",
|
||||
"exclude": Array [],
|
||||
"fallback": "200.html",
|
||||
"interval": 0,
|
||||
"routes": Array [],
|
||||
"staticAssets": Object {
|
||||
"base": "/_nuxt/static",
|
||||
"dir": "static",
|
||||
"version": "x",
|
||||
"versionBase": "/_nuxt/static/x",
|
||||
},
|
||||
"subFolders": true,
|
||||
},
|
||||
"globalName": "nuxt",
|
||||
@ -339,6 +346,7 @@ Object {
|
||||
"mode": "history",
|
||||
"parseQuery": false,
|
||||
"prefetchLinks": true,
|
||||
"prefetchPayloads": true,
|
||||
"routeNameSplitter": "-",
|
||||
"routes": Array [],
|
||||
"scrollBehavior": null,
|
||||
@ -354,6 +362,7 @@ Object {
|
||||
},
|
||||
"serverMiddleware": Array [],
|
||||
"srcDir": "/var/nuxt/test",
|
||||
"ssr": true,
|
||||
"styleExtensions": Array [
|
||||
"css",
|
||||
"pcss",
|
||||
@ -364,6 +373,7 @@ Object {
|
||||
"sass",
|
||||
"less",
|
||||
],
|
||||
"target": "server",
|
||||
"test": true,
|
||||
"vue": Object {
|
||||
"config": Object {
|
||||
|
@ -171,11 +171,18 @@ Object {
|
||||
},
|
||||
"generate": Object {
|
||||
"concurrency": 500,
|
||||
"crawler": true,
|
||||
"dir": "dist",
|
||||
"exclude": Array [],
|
||||
"fallback": "200.html",
|
||||
"interval": 0,
|
||||
"routes": Array [],
|
||||
"staticAssets": Object {
|
||||
"base": undefined,
|
||||
"dir": "static",
|
||||
"version": undefined,
|
||||
"versionBase": undefined,
|
||||
},
|
||||
"subFolders": true,
|
||||
},
|
||||
"globalName": undefined,
|
||||
@ -310,6 +317,7 @@ Object {
|
||||
"mode": "history",
|
||||
"parseQuery": false,
|
||||
"prefetchLinks": true,
|
||||
"prefetchPayloads": true,
|
||||
"routeNameSplitter": "-",
|
||||
"routes": Array [],
|
||||
"scrollBehavior": null,
|
||||
@ -325,6 +333,7 @@ Object {
|
||||
},
|
||||
"serverMiddleware": Array [],
|
||||
"srcDir": undefined,
|
||||
"ssr": true,
|
||||
"styleExtensions": Array [
|
||||
"css",
|
||||
"pcss",
|
||||
@ -335,6 +344,7 @@ Object {
|
||||
"sass",
|
||||
"less",
|
||||
],
|
||||
"target": "server",
|
||||
"test": true,
|
||||
"vue": Object {
|
||||
"config": Object {
|
||||
@ -527,11 +537,18 @@ Object {
|
||||
},
|
||||
"generate": Object {
|
||||
"concurrency": 500,
|
||||
"crawler": true,
|
||||
"dir": "dist",
|
||||
"exclude": Array [],
|
||||
"fallback": "200.html",
|
||||
"interval": 0,
|
||||
"routes": Array [],
|
||||
"staticAssets": Object {
|
||||
"base": undefined,
|
||||
"dir": "static",
|
||||
"version": undefined,
|
||||
"versionBase": undefined,
|
||||
},
|
||||
"subFolders": true,
|
||||
},
|
||||
"globalName": undefined,
|
||||
@ -666,6 +683,7 @@ Object {
|
||||
"mode": "history",
|
||||
"parseQuery": false,
|
||||
"prefetchLinks": true,
|
||||
"prefetchPayloads": true,
|
||||
"routeNameSplitter": "-",
|
||||
"routes": Array [],
|
||||
"scrollBehavior": null,
|
||||
@ -681,6 +699,7 @@ Object {
|
||||
},
|
||||
"serverMiddleware": Array [],
|
||||
"srcDir": undefined,
|
||||
"ssr": true,
|
||||
"styleExtensions": Array [
|
||||
"css",
|
||||
"pcss",
|
||||
@ -691,6 +710,7 @@ Object {
|
||||
"sass",
|
||||
"less",
|
||||
],
|
||||
"target": "server",
|
||||
"test": true,
|
||||
"vue": Object {
|
||||
"config": Object {
|
||||
|
@ -25,7 +25,7 @@ describe('config: options', () => {
|
||||
jest.spyOn(path, 'resolve').mockImplementation((...args) => args.join('/').replace(/\\+/, '/'))
|
||||
jest.spyOn(path, 'join').mockImplementation((...args) => args.join('/').replace(/\\+/, '/'))
|
||||
|
||||
expect(getNuxtConfig({})).toMatchSnapshot()
|
||||
expect(getNuxtConfig({ generate: { staticAssets: { version: 'x' } } })).toMatchSnapshot()
|
||||
|
||||
process.cwd.mockRestore()
|
||||
path.resolve.mockRestore()
|
||||
@ -124,6 +124,17 @@ describe('config: options', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('should fallback to server target', () => {
|
||||
const { target } = getNuxtConfig({ target: 0 })
|
||||
expect(target).toEqual('server')
|
||||
})
|
||||
|
||||
test('should check unknown target', () => {
|
||||
const { target } = getNuxtConfig({ target: 'test' })
|
||||
expect(consola.warn).toHaveBeenCalledWith('Unknown target: test. Falling back to server')
|
||||
expect(target).toEqual('server')
|
||||
})
|
||||
|
||||
test('should check unknown mode', () => {
|
||||
const { build, render } = getNuxtConfig({ mode: 'test' })
|
||||
expect(consola.warn).toHaveBeenCalledWith('Unknown mode: test. Falling back to universal')
|
||||
|
@ -45,11 +45,10 @@ export default class ModuleContainer {
|
||||
throw new Error('Template src not found: ' + src)
|
||||
}
|
||||
|
||||
// Generate unique and human readable dst filename
|
||||
const dst =
|
||||
template.fileName ||
|
||||
path.basename(srcPath.dir) + `.${srcPath.name}.${hash(src)}` + srcPath.ext
|
||||
|
||||
// Mostly for DX, some people prefers `filename` vs `fileName`
|
||||
const fileName = template.fileName || template.filename
|
||||
// Generate unique and human readable dst filename if not provided
|
||||
const dst = fileName || `${path.basename(srcPath.dir)}.${srcPath.name}.${hash(src)}${srcPath.ext}`
|
||||
// Add to templates list
|
||||
const templateObj = {
|
||||
src,
|
||||
@ -58,6 +57,7 @@ export default class ModuleContainer {
|
||||
}
|
||||
|
||||
this.options.build.templates.push(templateObj)
|
||||
|
||||
return templateObj
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,8 @@
|
||||
"chalk": "^3.0.0",
|
||||
"consola": "^2.11.3",
|
||||
"fs-extra": "^8.1.0",
|
||||
"html-minifier": "^4.0.0"
|
||||
"html-minifier": "^4.0.0",
|
||||
"node-html-parser": "^1.2.4"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
@ -1,16 +1,18 @@
|
||||
import path from 'path'
|
||||
import Chalk from 'chalk'
|
||||
import chalk from 'chalk'
|
||||
import consola from 'consola'
|
||||
import fsExtra from 'fs-extra'
|
||||
import htmlMinifier from 'html-minifier'
|
||||
import { parse } from 'node-html-parser'
|
||||
|
||||
import { flatRoutes, isString, isUrl, promisifyRoute, waitFor } from '@nuxt/utils'
|
||||
import { isFullStatic, flatRoutes, isString, isUrl, promisifyRoute, waitFor, TARGETS, MODES } 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.staticRoutes = path.resolve(this.options.srcDir, this.options.dir.static)
|
||||
@ -20,19 +22,25 @@ export default class Generator {
|
||||
this.distPath,
|
||||
isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath
|
||||
)
|
||||
this.generatedRoutes = new Set()
|
||||
}
|
||||
|
||||
async generate ({ build = true, init = true } = {}) {
|
||||
consola.debug('Initializing generator...')
|
||||
|
||||
await this.initiate({ build, init })
|
||||
|
||||
consola.debug('Preparing routes for generate...')
|
||||
// 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')
|
||||
|
||||
const errors = await this.generateRoutes(routes)
|
||||
|
||||
await this.afterGenerate()
|
||||
@ -56,6 +64,22 @@ export default class Generator {
|
||||
|
||||
// Start build process
|
||||
await this.builder.build()
|
||||
this.isFullStatic = isFullStatic(this.options)
|
||||
} else {
|
||||
const hasBuilt = await fsExtra.exists(this.srcBuiltPath)
|
||||
if (!hasBuilt) {
|
||||
throw new Error(
|
||||
`No build files found in ${this.srcBuiltPath}.\nPlease run \`nuxt build --target static\` before calling \`nuxt export\``
|
||||
)
|
||||
}
|
||||
const config = this.getBuildConfig()
|
||||
if (config.target !== TARGETS.static) {
|
||||
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
|
||||
@ -67,7 +91,7 @@ export default class Generator {
|
||||
async initRoutes (...args) {
|
||||
// Resolve config.generate.routes promises before generating the routes
|
||||
let generateRoutes = []
|
||||
if (this.options.router.mode !== 'hash') {
|
||||
if (this.options.mode === MODES.universal && this.options.router.mode !== 'hash') {
|
||||
try {
|
||||
generateRoutes = await promisifyRoute(
|
||||
this.options.generate.routes || [],
|
||||
@ -78,14 +102,14 @@ export default class Generator {
|
||||
throw e // eslint-disable-line no-unreachable
|
||||
}
|
||||
}
|
||||
// Generate only index.html for router.mode = 'hash'
|
||||
let routes =
|
||||
this.options.router.mode === 'hash'
|
||||
? ['/']
|
||||
: flatRoutes(this.options.router.routes)
|
||||
|
||||
routes = routes.filter(route => this.options.generate.exclude.every(regex => !regex.test(route)))
|
||||
|
||||
let routes = []
|
||||
// Generate only index.html for router.mode = 'hash' or client-side apps
|
||||
if (this.options.mode === MODES.spa || this.options.router.mode === 'hash') {
|
||||
routes = ['/']
|
||||
} else {
|
||||
routes = flatRoutes(this.getAppRoutes())
|
||||
}
|
||||
routes = routes.filter(route => this.shouldGenerateRoute(route))
|
||||
routes = this.decorateWithPayloads(routes, generateRoutes)
|
||||
|
||||
// extendRoutes hook
|
||||
@ -94,14 +118,34 @@ export default class Generator {
|
||||
return routes
|
||||
}
|
||||
|
||||
shouldGenerateRoute (route) {
|
||||
return this.options.generate.exclude.every((regex) => {
|
||||
if (typeof regex === 'string') {
|
||||
return regex !== route
|
||||
}
|
||||
return !regex.test(route)
|
||||
})
|
||||
}
|
||||
|
||||
getBuildConfig () {
|
||||
return require(path.join(this.options.buildDir, 'nuxt/config.json'))
|
||||
}
|
||||
|
||||
getAppRoutes () {
|
||||
return require(path.join(this.options.buildDir, 'routes.json'))
|
||||
}
|
||||
|
||||
async generateRoutes (routes) {
|
||||
const errors = []
|
||||
|
||||
this.routes = routes
|
||||
// Add routes to the tracked generated routes (for crawler)
|
||||
this.routes.forEach(({ route }) => this.generatedRoutes.add(route))
|
||||
// Start generate process
|
||||
while (routes.length) {
|
||||
while (this.routes.length) {
|
||||
let n = 0
|
||||
await Promise.all(
|
||||
routes
|
||||
this.routes
|
||||
.splice(0, this.options.generate.concurrency)
|
||||
.map(async ({ route, payload }) => {
|
||||
await waitFor(n++ * this.options.generate.interval)
|
||||
@ -123,12 +167,12 @@ export default class Generator {
|
||||
const isHandled = type === 'handled'
|
||||
const color = isHandled ? 'yellow' : 'red'
|
||||
|
||||
let line = Chalk[color](` ${route}\n\n`)
|
||||
let line = chalk[color](` ${route}\n\n`)
|
||||
|
||||
if (isHandled) {
|
||||
line += Chalk.grey(JSON.stringify(error, undefined, 2) + '\n')
|
||||
line += chalk.grey(JSON.stringify(error, undefined, 2) + '\n')
|
||||
} else {
|
||||
line += Chalk.grey(error.stack || error.message || `${error}`)
|
||||
line += chalk.grey(error.stack || error.message || `${error}`)
|
||||
}
|
||||
|
||||
return line
|
||||
@ -153,7 +197,10 @@ export default class Generator {
|
||||
}
|
||||
|
||||
// Render and write the SPA template to the fallback path
|
||||
let { html } = await this.nuxt.server.renderRoute('/', { spa: true })
|
||||
let { html } = await this.nuxt.server.renderRoute('/', {
|
||||
spa: true,
|
||||
staticAssetsBase: this.staticAssetsBase
|
||||
})
|
||||
|
||||
try {
|
||||
html = this.minifyHtml(html)
|
||||
@ -162,20 +209,27 @@ export default class Generator {
|
||||
}
|
||||
|
||||
await fsExtra.writeFile(fallbackPath, html, 'utf8')
|
||||
consola.success('Client-side fallback created: `' + fallback + '`')
|
||||
}
|
||||
|
||||
async initDist () {
|
||||
// Clean destination folder
|
||||
await fsExtra.remove(this.distPath)
|
||||
|
||||
consola.info(`Generating output directory: ${path.basename(this.distPath)}/`)
|
||||
await this.nuxt.callHook('generate:distRemoved', this)
|
||||
|
||||
// Copy static and built files
|
||||
if (await fsExtra.exists(this.staticRoutes)) {
|
||||
await fsExtra.copy(this.staticRoutes, this.distPath)
|
||||
}
|
||||
// Copy .nuxt/dist/client/ to dist/_nuxt/
|
||||
await fsExtra.copy(this.srcBuiltPath, this.distNuxtPath)
|
||||
|
||||
if (this.payloadDir) {
|
||||
await fsExtra.ensureDir(this.payloadDir)
|
||||
}
|
||||
|
||||
// Add .nojekyll file to let GitHub Pages add the _nuxt/ folder
|
||||
// https://help.github.com/articles/files-that-start-with-an-underscore-are-missing/
|
||||
const nojekyllPath = path.resolve(this.distPath, '.nojekyll')
|
||||
@ -207,11 +261,34 @@ export default class Generator {
|
||||
const pageErrors = []
|
||||
|
||||
try {
|
||||
const res = await this.nuxt.server.renderRoute(route, {
|
||||
_generate: true,
|
||||
payload
|
||||
})
|
||||
;({ html } = res)
|
||||
const renderContext = {
|
||||
payload,
|
||||
staticAssetsBase: this.staticAssetsBase
|
||||
}
|
||||
const res = await this.nuxt.server.renderRoute(route, renderContext)
|
||||
html = res.html
|
||||
|
||||
// If crawler activated and called from generateRoutes()
|
||||
if (this.options.generate.crawler && this.options.render.ssr) {
|
||||
parse(html).querySelectorAll('a').map((el) => {
|
||||
const href = (el.getAttribute('href') || '').split('?')[0].split('#')[0].trim()
|
||||
|
||||
if (href.startsWith('/') && this.shouldGenerateRoute(href) && !this.generatedRoutes.has(href)) {
|
||||
this.generatedRoutes.add(href) // add the route to the tracked list
|
||||
this.routes.push({ route: href })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Save Static Assets
|
||||
if (this.staticAssetsDir && renderContext.staticAssets) {
|
||||
for (const asset of renderContext.staticAssets) {
|
||||
const assetPath = path.join(this.staticAssetsDir, asset.path)
|
||||
await fsExtra.ensureDir(path.dirname(assetPath))
|
||||
await fsExtra.writeFile(assetPath, asset.src, 'utf-8')
|
||||
}
|
||||
}
|
||||
|
||||
if (res.error) {
|
||||
pageErrors.push({ type: 'handled', route, error: res.error })
|
||||
}
|
||||
|
@ -5,10 +5,12 @@ export const createNuxt = () => ({
|
||||
renderRoute: jest.fn(() => ({ html: 'rendered html' }))
|
||||
},
|
||||
options: {
|
||||
mode: 'universal',
|
||||
srcDir: '/var/nuxt/src',
|
||||
buildDir: '/var/nuxt/build',
|
||||
generate: { dir: '/var/nuxt/generate' },
|
||||
build: { publicPath: '__public' },
|
||||
dir: { static: '/var/nuxt/static' }
|
||||
dir: { static: '/var/nuxt/static' },
|
||||
render: {}
|
||||
}
|
||||
})
|
||||
|
@ -76,6 +76,8 @@ describe('generator: initialize', () => {
|
||||
const generator = new Generator(nuxt, builder)
|
||||
|
||||
generator.initDist = jest.fn()
|
||||
fsExtra.exists.mockReturnValueOnce(true)
|
||||
generator.getBuildConfig = jest.fn(() => ({ ssr: true, target: 'static' }))
|
||||
|
||||
await generator.initiate({ build: false, init: false })
|
||||
|
||||
@ -87,7 +89,7 @@ describe('generator: initialize', () => {
|
||||
expect(generator.initDist).not.toBeCalled()
|
||||
})
|
||||
|
||||
test('should init routes with generate.routes and router.routes', async () => {
|
||||
test('should init routes with generate.routes and routes.json', async () => {
|
||||
const nuxt = createNuxt()
|
||||
nuxt.options = {
|
||||
...nuxt.options,
|
||||
@ -97,14 +99,14 @@ describe('generator: initialize', () => {
|
||||
routes: ['/foo', '/foo/bar']
|
||||
},
|
||||
router: {
|
||||
mode: 'history',
|
||||
routes: ['/index', '/about', '/test']
|
||||
mode: 'history'
|
||||
}
|
||||
}
|
||||
const generator = new Generator(nuxt)
|
||||
|
||||
flatRoutes.mockImplementationOnce(routes => routes)
|
||||
promisifyRoute.mockImplementationOnce(routes => routes)
|
||||
generator.getAppRoutes = jest.fn(() => ['/index', '/about', '/test'])
|
||||
generator.decorateWithPayloads = jest.fn(() => 'decoratedRoutes')
|
||||
|
||||
const routes = await generator.initRoutes()
|
||||
@ -130,8 +132,7 @@ describe('generator: initialize', () => {
|
||||
routes: ['/foo', '/foo/bar']
|
||||
},
|
||||
router: {
|
||||
mode: 'hash',
|
||||
routes: ['/index', '/about', '/test']
|
||||
mode: 'hash'
|
||||
}
|
||||
}
|
||||
const generator = new Generator(nuxt)
|
||||
|
@ -43,7 +43,7 @@ describe('generator: generate route', () => {
|
||||
const returned = await generator.generateRoute({ route, payload, errors })
|
||||
|
||||
expect(nuxt.server.renderRoute).toBeCalledTimes(1)
|
||||
expect(nuxt.server.renderRoute).toBeCalledWith('/foo/', { _generate: true, payload })
|
||||
expect(nuxt.server.renderRoute).toBeCalledWith(route, { payload })
|
||||
expect(path.join).toBeCalledTimes(2)
|
||||
expect(path.join).nthCalledWith(1, '[sep]', '/foo.html')
|
||||
expect(path.join).nthCalledWith(2, generator.distPath, 'join([sep], /foo.html)')
|
||||
@ -81,7 +81,7 @@ describe('generator: generate route', () => {
|
||||
const returned = await generator.generateRoute({ route, payload, errors })
|
||||
|
||||
expect(nuxt.server.renderRoute).toBeCalledTimes(1)
|
||||
expect(nuxt.server.renderRoute).toBeCalledWith('/foo', { _generate: true, payload })
|
||||
expect(nuxt.server.renderRoute).toBeCalledWith('/foo', { payload })
|
||||
expect(nuxt.callHook).toBeCalledTimes(1)
|
||||
expect(nuxt.callHook).toBeCalledWith('generate:routeFailed', {
|
||||
route,
|
||||
|
@ -7,7 +7,6 @@ export default async function renderAndGetWindow (
|
||||
{
|
||||
loadedCallback,
|
||||
loadingTimeout = 2000,
|
||||
ssr,
|
||||
globals
|
||||
} = {}
|
||||
) {
|
||||
@ -49,9 +48,7 @@ export default async function renderAndGetWindow (
|
||||
const { window } = await jsdom.JSDOM.fromURL(url, options)
|
||||
|
||||
// If Nuxt could not be loaded (error from the server-side)
|
||||
const nuxtExists = window.document.body.innerHTML.includes(
|
||||
ssr ? `window.${globals.context}` : `<div id="${globals.id}">`
|
||||
)
|
||||
const nuxtExists = window.document.body.innerHTML.includes(`id="${globals.id}"`)
|
||||
|
||||
if (!nuxtExists) {
|
||||
const error = new Error('Could not load the nuxt app')
|
||||
|
@ -2,7 +2,7 @@ import generateETag from 'etag'
|
||||
import fresh from 'fresh'
|
||||
import consola from 'consola'
|
||||
|
||||
import { getContext } from '@nuxt/utils'
|
||||
import { getContext, TARGETS } from '@nuxt/utils'
|
||||
|
||||
export default ({ options, nuxt, renderRoute, resources }) => async function nuxtMiddleware (req, res, next) {
|
||||
// Get context
|
||||
@ -28,7 +28,7 @@ export default ({ options, nuxt, renderRoute, resources }) => async function nux
|
||||
preloadFiles
|
||||
} = result
|
||||
|
||||
if (redirected) {
|
||||
if (redirected && context.target !== TARGETS.static) {
|
||||
await nuxt.callHook('render:routeDone', url, result, context)
|
||||
return html
|
||||
}
|
||||
|
@ -320,13 +320,11 @@ export default class Server {
|
||||
renderAndGetWindow (url, opts = {}, {
|
||||
loadingTimeout = 2000,
|
||||
loadedCallback = this.globals.loadedCallback,
|
||||
ssr = this.options.render.ssr,
|
||||
globals = this.globals
|
||||
} = {}) {
|
||||
return renderAndGetWindow(url, opts, {
|
||||
loadingTimeout,
|
||||
loadedCallback,
|
||||
ssr,
|
||||
globals
|
||||
})
|
||||
}
|
||||
|
9
packages/utils/src/constants.js
Normal file
9
packages/utils/src/constants.js
Normal file
@ -0,0 +1,9 @@
|
||||
export const TARGETS = {
|
||||
server: 'server',
|
||||
static: 'static'
|
||||
}
|
||||
|
||||
export const MODES = {
|
||||
universal: 'universal',
|
||||
spa: 'spa'
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { TARGETS } from './constants'
|
||||
|
||||
export const getContext = function getContext (req, res) {
|
||||
return { req, res }
|
||||
@ -14,3 +15,7 @@ export const determineGlobals = function determineGlobals (globalName, globals)
|
||||
}
|
||||
return _globals
|
||||
}
|
||||
|
||||
export const isFullStatic = function (options) {
|
||||
return !options.dev && !options._legacyGenerate && options.target === TARGETS.static && options.render.ssr
|
||||
}
|
||||
|
@ -8,3 +8,4 @@ export * from './task'
|
||||
export * from './timer'
|
||||
export * from './cjs'
|
||||
export * from './modern'
|
||||
export * from './constants'
|
||||
|
@ -9,6 +9,7 @@ import * as task from '../src/task'
|
||||
import * as timer from '../src/timer'
|
||||
import * as cjs from '../src/cjs'
|
||||
import * as modern from '../src/modern'
|
||||
import * as constants from '../src/constants'
|
||||
|
||||
describe('util: entry', () => {
|
||||
test('should export all methods from utils folder', () => {
|
||||
@ -22,7 +23,8 @@ describe('util: entry', () => {
|
||||
...task,
|
||||
...timer,
|
||||
...cjs,
|
||||
...modern
|
||||
...modern,
|
||||
...constants
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -5,11 +5,14 @@ export const template = {
|
||||
dependencies,
|
||||
dir: path.join(__dirname, '..', 'template'),
|
||||
files: [
|
||||
'nuxt/config.json',
|
||||
'App.js',
|
||||
'client.js',
|
||||
'index.js',
|
||||
'jsonp.js',
|
||||
'router.js',
|
||||
'router.scrollBehavior.js',
|
||||
'routes.json',
|
||||
'server.js',
|
||||
'utils.js',
|
||||
'empty.js',
|
||||
|
@ -4,7 +4,8 @@ import Vue from 'vue'
|
||||
'getMatchedComponentsInstances',
|
||||
'getChildrenComponentInstancesUsingFetch',
|
||||
'promisify',
|
||||
'globalHandleError'
|
||||
'globalHandleError',
|
||||
'urlJoin'
|
||||
] : [],
|
||||
...features.layouts ? [
|
||||
'sanitizeComponent'
|
||||
@ -57,7 +58,8 @@ export default {
|
||||
domProps: {
|
||||
id: '__layout'
|
||||
},
|
||||
key: this.layoutName
|
||||
|
||||
key: this.layoutName
|
||||
}, [layoutEl])
|
||||
<% } else { %>
|
||||
const templateEl = h('nuxt')
|
||||
@ -110,8 +112,8 @@ export default {
|
||||
created () {
|
||||
// Add this.$nuxt in child instances
|
||||
Vue.prototype.<%= globals.nuxt %> = this
|
||||
// add to window so we can listen when ready
|
||||
if (process.client) {
|
||||
// add to window so we can listen when ready
|
||||
window.<%= globals.nuxt %> = <%= (globals.nuxt !== '$nuxt' ? 'window.$nuxt = ' : '') %>this
|
||||
<% if (features.clientOnline) { %>
|
||||
this.refreshOnlineStatus()
|
||||
@ -125,10 +127,22 @@ export default {
|
||||
// Add $nuxt.context
|
||||
this.context = this.$options.context
|
||||
},
|
||||
<% if (loading) { %>
|
||||
mounted () {
|
||||
this.$loading = this.$refs.loading
|
||||
<% if (loading || isFullStatic) { %>
|
||||
async mounted () {
|
||||
<% if (loading) { %>this.$loading = this.$refs.loading<% } %>
|
||||
<% if (isFullStatic) {%>
|
||||
if (this.isPreview) {
|
||||
if (this.$store && this.$store._actions.nuxtServerInit) {
|
||||
<% if (loading) { %>this.$loading.start()<% } %>
|
||||
await app.$store.dispatch('nuxtServerInit', this.context)
|
||||
}
|
||||
await this.refresh()
|
||||
<% if (loading) { %>this.$loading.finish()<% } %>
|
||||
}
|
||||
<% } %>
|
||||
},
|
||||
<% } %>
|
||||
<% if (loading) { %>
|
||||
watch: {
|
||||
'nuxt.err': 'errorChanged'
|
||||
},
|
||||
@ -139,10 +153,13 @@ export default {
|
||||
return !this.isOnline
|
||||
},
|
||||
<% if (features.fetch) { %>
|
||||
isFetching() {
|
||||
isFetching () {
|
||||
return this.nbFetching > 0
|
||||
}
|
||||
<% } %>
|
||||
},<% } %>
|
||||
<% if (nuxtOptions.target === 'static') { %>
|
||||
isPreview () {
|
||||
return Boolean(this.$options.previewData)
|
||||
},<% } %>
|
||||
},
|
||||
<% } %>
|
||||
methods: {
|
||||
@ -257,7 +274,7 @@ export default {
|
||||
return this.<%= globals.nuxt %>.error({ statusCode: 500, message: e.message })
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
<% } else { %>
|
||||
setLayout (layout) {
|
||||
<% if (debug) { %>
|
||||
@ -277,9 +294,27 @@ export default {
|
||||
layout = 'default'
|
||||
}
|
||||
return Promise.resolve(layouts['_' + layout])
|
||||
}
|
||||
},
|
||||
<% } /* splitChunks.layouts */ %>
|
||||
<% } /* features.layouts */ %>
|
||||
<% if (isFullStatic) { %>
|
||||
setPagePayload(payload) {
|
||||
this._pagePayload = payload
|
||||
this._payloadFetchIndex = 0
|
||||
},
|
||||
async fetchPayload(route) {
|
||||
route = (route.replace(/\/$/, '') || '/').split('?')[0]
|
||||
try {
|
||||
const src = urlJoin(window.__NUXT_STATIC__, route, 'payload.js')
|
||||
const payload = await window.__NUXT_IMPORT__(route, src)
|
||||
this.setPagePayload(payload)
|
||||
return payload
|
||||
} catch (err) {
|
||||
this.setPagePayload(false)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
<% } %>
|
||||
},
|
||||
<% if (loading) { %>
|
||||
components: {
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
import { createApp<% if (features.layouts) { %>, NuxtError<% } %> } from './index.js'
|
||||
<% if (features.fetch) { %>import fetchMixin from './mixins/fetch.client'<% } %>
|
||||
import NuxtLink from './components/nuxt-link.<%= features.clientPrefetch ? "client" : "server" %>.js' // should be included after ./index.js
|
||||
<% if (isFullStatic) { %>import './jsonp'<% } %>
|
||||
|
||||
<% if (features.fetch) { %>
|
||||
// Fetch mixin
|
||||
@ -136,7 +137,7 @@ function mapTransitions (toComponents, to, from) {
|
||||
return mergedTransitions
|
||||
}
|
||||
<% } %>
|
||||
<% if (loading) { %>async <% } %>function loadAsyncComponents (to, from, next) {
|
||||
async function loadAsyncComponents (to, from, next) {
|
||||
// Check if route changed (this._routeChanged), only if the page is not an error (for validate())
|
||||
this._routeChanged = Boolean(app.nuxt.err) || from.name !== to.name
|
||||
this._paramChanged = !this._routeChanged && from.path !== to.path
|
||||
@ -150,7 +151,6 @@ function mapTransitions (toComponents, to, from) {
|
||||
<% } %>
|
||||
|
||||
try {
|
||||
<% if (loading) { %>
|
||||
if (this._queryChanged) {
|
||||
const Components = await resolveRouteComponents(
|
||||
to,
|
||||
@ -170,11 +170,12 @@ function mapTransitions (toComponents, to, from) {
|
||||
}
|
||||
return false
|
||||
})
|
||||
<% if (loading) { %>
|
||||
if (startLoader && this.$loading.start && !this.$loading.manual) {
|
||||
this.$loading.start()
|
||||
}
|
||||
<% } %>
|
||||
}
|
||||
<% } %>
|
||||
// Call next()
|
||||
next()
|
||||
} catch (error) {
|
||||
@ -429,7 +430,7 @@ async function render (to, from, next) {
|
||||
<% if (features.asyncData || features.fetch) { %>
|
||||
let instances
|
||||
// Call asyncData & fetch hooks on components matched by the route.
|
||||
await Promise.all(Components.map((Component, i) => {
|
||||
await Promise.all(Components.map(async (Component, i) => {
|
||||
// Check if only children route changed
|
||||
Component._path = compile(to.matched[matches[i]].path)(to.params)
|
||||
Component._dataRefresh = false
|
||||
@ -484,15 +485,31 @@ async function render (to, from, next) {
|
||||
<% if (features.asyncData) { %>
|
||||
// Call asyncData(context)
|
||||
if (hasAsyncData) {
|
||||
const promise = promisify(Component.options.asyncData, app.context)
|
||||
.then((asyncDataResult) => {
|
||||
applyAsyncData(Component, asyncDataResult)
|
||||
<% if (loading) { %>
|
||||
if (this.$loading.increase) {
|
||||
this.$loading.increase(loadingIncrease)
|
||||
<% if (isFullStatic) { %>
|
||||
let promise
|
||||
|
||||
if (this.isPreview) {
|
||||
promise = promisify(Component.options.asyncData, app.context)
|
||||
} else {
|
||||
try {
|
||||
const payload = await this.fetchPayload(to.path).then((payloadData) => payloadData.data[i])
|
||||
promise = Promise.resolve(payload)
|
||||
} catch (err) {
|
||||
// fallback
|
||||
promise = promisify(Component.options.asyncData, app.context)
|
||||
}
|
||||
<% } %>
|
||||
})
|
||||
}
|
||||
<% } else { %>
|
||||
const promise = promisify(Component.options.asyncData, app.context)
|
||||
<% } %>
|
||||
promise.then((asyncDataResult) => {
|
||||
applyAsyncData(Component, asyncDataResult)
|
||||
<% if (loading) { %>
|
||||
if (this.$loading.increase) {
|
||||
this.$loading.increase(loadingIncrease)
|
||||
}
|
||||
<% } %>
|
||||
})
|
||||
promises.push(promise)
|
||||
}
|
||||
<% } %>
|
||||
@ -501,6 +518,12 @@ async function render (to, from, next) {
|
||||
this.$loading.manual = Component.options.loading === false
|
||||
|
||||
<% if (features.fetch) { %>
|
||||
<% if (isFullStatic) { %>
|
||||
if (!this.isPreview) {
|
||||
// Catching the error here for letting the SPA fallback and normal fetch behaviour
|
||||
promises.push(this.fetchPayload(to.path).catch(err => null))
|
||||
}
|
||||
<% } %>
|
||||
// Call fetch(context)
|
||||
if (hasFetch) {
|
||||
let p = Component.options.fetch(app.context)
|
||||
@ -768,7 +791,7 @@ function addHotReload ($component, depth) {
|
||||
}
|
||||
<% } %>
|
||||
|
||||
<% if (features.layouts || features.transitions) { %>async <% } %>function mountApp (__app) {
|
||||
async function mountApp (__app) {
|
||||
// Set global variables
|
||||
app = __app.app
|
||||
router = __app.router
|
||||
@ -777,6 +800,16 @@ function addHotReload ($component, depth) {
|
||||
// Create Vue instance
|
||||
const _app = new Vue(app)
|
||||
|
||||
<% if (isFullStatic) { %>
|
||||
// Load page chunk
|
||||
if (!NUXT.data && !NUXT.spa) {
|
||||
try {
|
||||
const payload = await _app.fetchPayload(_app.context.route.path)
|
||||
Object.assign(NUXT, payload)
|
||||
} catch (err) {}
|
||||
}
|
||||
<% } %>
|
||||
|
||||
<% if (features.layouts && mode !== 'spa') { %>
|
||||
// Load layout
|
||||
const layout = NUXT.layout || 'default'
|
||||
@ -825,6 +858,10 @@ function addHotReload ($component, depth) {
|
||||
router.beforeEach(loadAsyncComponents.bind(_app))
|
||||
router.beforeEach(render.bind(_app))
|
||||
|
||||
// Fix in static: remove trailing slash to force hydration
|
||||
if (process.static && NUXT.serverRendered && NUXT.routePath !== '/' && NUXT.routePath.slice(-1) !== '/' && _app.context.route.path.slice(-1) === '/') {
|
||||
_app.context.route.path = _app.context.route.path.replace(/\/+$/, '')
|
||||
}
|
||||
// If page already is server rendered and it was done on the same route path as client side render
|
||||
if (NUXT.serverRendered && NUXT.routePath === _app.context.route.path) {
|
||||
mount()
|
||||
@ -839,6 +876,8 @@ function addHotReload ($component, depth) {
|
||||
mount()
|
||||
}
|
||||
|
||||
// fix: force next tick to avoid having same timestamp when an error happen on spa fallback
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
render.call(_app, router.currentRoute, router.currentRoute, (path) => {
|
||||
// If not redirected
|
||||
if (!path) {
|
||||
|
@ -43,7 +43,7 @@ export default {
|
||||
meta: [
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no'
|
||||
content: 'width=device-width,initial-scale=1.0,minimum-scale=1.0'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -71,7 +71,12 @@ export default {
|
||||
}<% } %>
|
||||
},
|
||||
shouldPrefetch () {
|
||||
return this.getPrefetchComponents().length > 0
|
||||
<% if (isFullStatic && router.prefetchPayloads) { %>
|
||||
const ref = this.$router.resolve(this.to, this.$route, this.append)
|
||||
const Components = ref.resolved.matched.map(r => r.components.default)
|
||||
|
||||
return Components.filter(Component => ref.href || (typeof Component === 'function' && !Component.options && !Component.__prefetched)).length
|
||||
<% } else { %>return this.getPrefetchComponents().length > 0<% } %>
|
||||
},
|
||||
canPrefetch () {
|
||||
const conn = navigator.connection
|
||||
@ -101,8 +106,16 @@ export default {
|
||||
<% if (router.linkPrefetchedClass) { %>promises.push(componentOrPromise)<% } %>
|
||||
}
|
||||
Component.__prefetched = true
|
||||
}<% if (router.linkPrefetchedClass) { %>
|
||||
return Promise.all(promises).then(() => this.addPrefetchedClass())
|
||||
}
|
||||
<% if (isFullStatic && router.prefetchPayloads) { %>
|
||||
// Preload the data only if not in preview mode
|
||||
if (!this.$root.isPreview) {
|
||||
const { href } = this.$router.resolve(this.to, this.$route, this.append)
|
||||
this.$nuxt.fetchPayload(href).catch(() => {})
|
||||
}
|
||||
<% } %>
|
||||
<% if (router.linkPrefetchedClass) { %>
|
||||
return Promise.all(promises).then(() => this.addPrefetchedClass())
|
||||
<% } %>
|
||||
}<% if (router.linkPrefetchedClass) { %>,
|
||||
addPrefetchedClass () {
|
||||
|
@ -210,6 +210,13 @@ async function createApp (ssrContext) {
|
||||
}
|
||||
<% } %>
|
||||
|
||||
// Add enablePreview(previewData = {}) in context for plugins
|
||||
if (process.static && process.client) {
|
||||
app.context.enablePreview = function (previewData = {}) {
|
||||
app.previewData = Object.assign({}, previewData)
|
||||
inject('preview', previewData)
|
||||
}
|
||||
}
|
||||
// Plugin execution
|
||||
<%= isTest ? '/* eslint-disable camelcase */' : '' %>
|
||||
<% plugins.forEach((plugin) => { %>
|
||||
@ -228,6 +235,12 @@ async function createApp (ssrContext) {
|
||||
<% } %>
|
||||
<% }) %>
|
||||
<%= isTest ? '/* eslint-enable camelcase */' : '' %>
|
||||
// Lock enablePreview in context
|
||||
if (process.static && process.client) {
|
||||
app.context.enablePreview = function () {
|
||||
console.warn('You cannot call enablePreview() outside a plugin.')
|
||||
}
|
||||
}
|
||||
|
||||
// If server-side, wait for async component to be resolved first
|
||||
if (process.server && ssrContext && ssrContext.url) {
|
||||
|
80
packages/vue-app/template/jsonp.js
Normal file
80
packages/vue-app/template/jsonp.js
Normal file
@ -0,0 +1,80 @@
|
||||
const chunks = {} // chunkId => exports
|
||||
const chunksInstalling = {} // chunkId => Promise
|
||||
const failedChunks = {}
|
||||
|
||||
function importChunk(chunkId, src) {
|
||||
// Already installed
|
||||
if (chunks[chunkId]) {
|
||||
return Promise.resolve(chunks[chunkId])
|
||||
}
|
||||
|
||||
// Failed loading
|
||||
if (failedChunks[chunkId]) {
|
||||
return Promise.reject(failedChunks[chunkId])
|
||||
}
|
||||
|
||||
// Installing
|
||||
if (chunksInstalling[chunkId]) {
|
||||
return chunksInstalling[chunkId]
|
||||
}
|
||||
|
||||
// Set a promise in chunk cache
|
||||
let resolve, reject
|
||||
const promise = chunksInstalling[chunkId] = new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve
|
||||
reject = _reject
|
||||
})
|
||||
|
||||
// Clear chunk data from cache
|
||||
delete chunks[chunkId]
|
||||
|
||||
// Start chunk loading
|
||||
const script = document.createElement('script')
|
||||
script.charset = 'utf-8'
|
||||
script.timeout = 120
|
||||
script.src = src
|
||||
let timeout
|
||||
|
||||
// Create error before stack unwound to get useful stacktrace later
|
||||
const error = new Error()
|
||||
|
||||
// Complete handlers
|
||||
const onScriptComplete = script.onerror = script.onload = (event) => {
|
||||
// Cleanups
|
||||
clearTimeout(timeout)
|
||||
delete chunksInstalling[chunkId]
|
||||
|
||||
// Avoid mem leaks in IE
|
||||
script.onerror = script.onload = null
|
||||
|
||||
// Verify chunk is loaded
|
||||
if (chunks[chunkId]) {
|
||||
return resolve(chunks[chunkId])
|
||||
}
|
||||
|
||||
// Something bad happened
|
||||
const errorType = event && (event.type === 'load' ? 'missing' : event.type)
|
||||
const realSrc = event && event.target && event.target.src
|
||||
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'
|
||||
error.name = 'ChunkLoadError'
|
||||
error.type = errorType
|
||||
error.request = realSrc
|
||||
failedChunks[chunkId] = error
|
||||
reject(error)
|
||||
}
|
||||
|
||||
// Timeout
|
||||
timeout = setTimeout(() => {
|
||||
onScriptComplete({ type: 'timeout', target: script })
|
||||
}, 120000)
|
||||
|
||||
// Append script
|
||||
document.head.appendChild(script)
|
||||
|
||||
// Return promise
|
||||
return promise
|
||||
}
|
||||
|
||||
window.__NUXT_JSONP__ = function (chunkId, exports) { chunks[chunkId] = exports }
|
||||
window.__NUXT_JSONP_CACHE__ = chunks
|
||||
window.__NUXT_IMPORT__ = importChunk
|
@ -32,6 +32,7 @@ function beforeMount() {
|
||||
|
||||
function created() {
|
||||
if (!isSsrHydration(this)) {
|
||||
<% if (isFullStatic) { %>createdFullStatic.call(this)<% } %>
|
||||
return
|
||||
}
|
||||
|
||||
@ -52,6 +53,33 @@ function created() {
|
||||
}
|
||||
}
|
||||
|
||||
<% if (isFullStatic) { %>
|
||||
function createdFullStatic() {
|
||||
// Check if component has been fetched on server
|
||||
let fetchedOnServer = this.$options.fetchOnServer !== false
|
||||
if (typeof this.$options.fetchOnServer === 'function') {
|
||||
fetchedOnServer = this.$options.fetchOnServer.call(this) !== false
|
||||
}
|
||||
if (!fetchedOnServer || this.$nuxt.isPreview || !this.$nuxt._pagePayload) {
|
||||
return
|
||||
}
|
||||
this._hydrated = true
|
||||
this._fetchKey = this.$nuxt._payloadFetchIndex++
|
||||
const data = this.$nuxt._pagePayload.fetch[this._fetchKey]
|
||||
|
||||
// If fetch error
|
||||
if (data && data._error) {
|
||||
this.$fetchState.error = data._error
|
||||
return
|
||||
}
|
||||
|
||||
// Merge data
|
||||
for (const key in data) {
|
||||
Vue.set(this.$data, key, data[key])
|
||||
}
|
||||
}
|
||||
<% } %>
|
||||
|
||||
function $fetch() {
|
||||
if (!this._fetchPromise) {
|
||||
this._fetchPromise = $_fetch.call(this)
|
||||
@ -71,6 +99,9 @@ async function $_fetch() {
|
||||
try {
|
||||
await this.$options.fetch.call(this)
|
||||
} catch (err) {
|
||||
if (process.dev) {
|
||||
console.error('Error in fetch():', err)
|
||||
}
|
||||
error = normalizeError(err)
|
||||
}
|
||||
|
||||
@ -85,4 +116,3 @@ async function $_fetch() {
|
||||
|
||||
this.$nextTick(() => this.<%= globals.nuxt %>.nbFetching--)
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,9 @@ async function serverPrefetch() {
|
||||
try {
|
||||
await this.$options.fetch.call(this)
|
||||
} catch (err) {
|
||||
if (process.dev) {
|
||||
console.error('Error in fetch():', err)
|
||||
}
|
||||
this.$fetchState.error = normalizeError(err)
|
||||
}
|
||||
this.$fetchState.pending = false
|
||||
@ -27,7 +30,7 @@ async function serverPrefetch() {
|
||||
}
|
||||
|
||||
export default {
|
||||
beforeCreate() {
|
||||
created() {
|
||||
if (!hasFetch(this)) {
|
||||
return
|
||||
}
|
||||
|
5
packages/vue-app/template/nuxt/config.json
Normal file
5
packages/vue-app/template/nuxt/config.json
Normal file
@ -0,0 +1,5 @@
|
||||
<%= JSON.stringify({
|
||||
isFullStatic: isFullStatic,
|
||||
ssr: nuxtOptions.render.ssr,
|
||||
target: nuxtOptions.target
|
||||
}, null, 2) %>
|
1
packages/vue-app/template/routes.json
Normal file
1
packages/vue-app/template/routes.json
Normal file
@ -0,0 +1 @@
|
||||
<%= JSON.stringify(router.routes, null, 2) %>
|
@ -37,9 +37,9 @@ function urlJoin () {
|
||||
}
|
||||
|
||||
const createNext = ssrContext => (opts) => {
|
||||
// If static target, render on client-side
|
||||
ssrContext.redirected = opts
|
||||
// If nuxt generate
|
||||
if (!ssrContext.res) {
|
||||
if (ssrContext.target === 'static' || !ssrContext.res) {
|
||||
ssrContext.nuxt.serverRendered = false
|
||||
return
|
||||
}
|
||||
@ -71,8 +71,13 @@ export default async (ssrContext) => {
|
||||
ssrContext.next = createNext(ssrContext)
|
||||
// Used for beforeNuxtRender({ Components, nuxtState })
|
||||
ssrContext.beforeRenderFns = []
|
||||
// Nuxt object (window{{globals.context}}, defaults to window.__NUXT__)
|
||||
// Nuxt object (window.{{globals.context}}, defaults to window.__NUXT__)
|
||||
ssrContext.nuxt = { <% if (features.layouts) { %>layout: 'default', <% } %>data: [], <% if (features.fetch) { %>fetch: [], <% } %>error: null<%= (store ? ', state: null' : '') %>, serverRendered: true, routePath: '' }
|
||||
// Remove query from url is static target
|
||||
if (process.static && ssrContext.url) {
|
||||
ssrContext.url = ssrContext.url.split('?')[0]
|
||||
}
|
||||
|
||||
// Create the app definition and the instance (created for each request)
|
||||
const { app, router<%= (store ? ', store' : '') %> } = await createApp(ssrContext)
|
||||
const _app = new Vue(app)
|
||||
@ -100,6 +105,10 @@ export default async (ssrContext) => {
|
||||
}
|
||||
|
||||
const renderErrorPage = async () => {
|
||||
// Don't server-render the page in static target
|
||||
if (ssrContext.target === 'static') {
|
||||
ssrContext.nuxt.serverRendered = false
|
||||
}
|
||||
<% if (features.layouts) { %>
|
||||
// Load layout for error page
|
||||
const layout = (NuxtError.options || NuxtError).layout
|
||||
@ -245,10 +254,6 @@ export default async (ssrContext) => {
|
||||
|
||||
// ...If .validate() returned false
|
||||
if (!isValid) {
|
||||
// Don't server-render the page in generate mode
|
||||
if (ssrContext._generate) {
|
||||
ssrContext.nuxt.serverRendered = false
|
||||
}
|
||||
// Render a 404 error page
|
||||
return render404Page()
|
||||
}
|
||||
|
@ -156,10 +156,10 @@ export async function setContext (app, context) {
|
||||
env: <%= JSON.stringify(env) %><%= isTest ? '// eslint-disable-line' : '' %>
|
||||
}
|
||||
// Only set once
|
||||
if (context.req) {
|
||||
if (!process.static && context.req) {
|
||||
app.context.req = context.req
|
||||
}
|
||||
if (context.res) {
|
||||
if (!process.static && context.res) {
|
||||
app.context.res = context.res
|
||||
}
|
||||
if (context.ssrContext) {
|
||||
@ -642,3 +642,11 @@ export function addLifecycleHook(vm, hook, fn) {
|
||||
vm.$options[hook].push(fn)
|
||||
}
|
||||
}
|
||||
|
||||
export const urlJoin = function urlJoin () {
|
||||
return [].slice
|
||||
.call(arguments)
|
||||
.join('/')
|
||||
.replace(/\/+/g, '/')
|
||||
.replace(':/', '://')
|
||||
}
|
||||
|
@ -274,13 +274,15 @@ export default class VueRenderer {
|
||||
|
||||
// Add url to the renderContext
|
||||
renderContext.url = url
|
||||
// Add target to the renderContext
|
||||
renderContext.target = this.serverContext.nuxt.options.target
|
||||
|
||||
const { req = {} } = renderContext
|
||||
const { req = {}, res = {} } = renderContext
|
||||
|
||||
// renderContext.spa
|
||||
if (renderContext.spa === undefined) {
|
||||
// TODO: Remove reading from renderContext.res in Nuxt3
|
||||
renderContext.spa = !this.SSR || req.spa || (renderContext.res && renderContext.res.spa)
|
||||
renderContext.spa = !this.SSR || req.spa || res.spa
|
||||
}
|
||||
|
||||
// renderContext.modern
|
||||
|
@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep'
|
||||
import VueMeta from 'vue-meta'
|
||||
import { createRenderer } from 'vue-server-renderer'
|
||||
import LRU from 'lru-cache'
|
||||
import { isModernRequest } from '@nuxt/utils'
|
||||
import { TARGETS, isModernRequest } from '@nuxt/utils'
|
||||
import BaseRenderer from './base'
|
||||
|
||||
export default class SPARenderer extends BaseRenderer {
|
||||
@ -27,9 +27,9 @@ export default class SPARenderer extends BaseRenderer {
|
||||
}
|
||||
|
||||
async render (renderContext) {
|
||||
const { url = '/', req = {}, _generate } = renderContext
|
||||
const { url = '/', req = {} } = renderContext
|
||||
const modernMode = this.options.modern
|
||||
const modern = (modernMode && _generate) || isModernRequest(req, modernMode)
|
||||
const modern = (modernMode && this.options.target === TARGETS.static) || isModernRequest(req, modernMode)
|
||||
const cacheKey = `${modern ? 'modern:' : 'legacy:'}${url}`
|
||||
let meta = this.cache.get(cacheKey)
|
||||
|
||||
@ -148,7 +148,12 @@ export default class SPARenderer extends BaseRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
const APP = `${meta.BODY_SCRIPTS_PREPEND}<div id="${this.serverContext.globals.id}">${this.serverContext.resources.loadingHTML}</div>${meta.BODY_SCRIPTS}`
|
||||
let APP = `${meta.BODY_SCRIPTS_PREPEND}<div id="${this.serverContext.globals.id}">${this.serverContext.resources.loadingHTML}</div>${meta.BODY_SCRIPTS}`
|
||||
|
||||
if (renderContext.staticAssetsBase) {
|
||||
// Full static, add window.__NUXT_STATIC__
|
||||
APP += `<script>window.__NUXT_STATIC__='${renderContext.staticAssetsBase}';window.${this.serverContext.globals.context}={spa:!0}</script>`
|
||||
}
|
||||
|
||||
// Prepare template params
|
||||
const templateParams = {
|
||||
|
@ -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 = `<div id="${this.serverContext.globals.id}"></div>`
|
||||
}
|
||||
|
||||
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 += `<script>${serializedSession}</script>`
|
||||
// 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 += `<script defer>${nuxtStaticScript}</script>`
|
||||
APP += `<script defer src="${staticAssetsBase}${statePath}"></script>`
|
||||
preloadScripts.push(stateUrl)
|
||||
} else {
|
||||
APP += `<script defer>${nuxtStaticScript}${stateScript}</script>`
|
||||
}
|
||||
|
||||
// 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 += `<link rel="preload" href="${href}" as="script">`
|
||||
}
|
||||
} 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 += `<script>${serializedSession}</script>`
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
|
@ -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!')
|
||||
})
|
||||
})
|
||||
|
@ -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('<div id="__nuxt"></div>')
|
||||
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('<div id="__nuxt"></div>')
|
||||
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('<h1>User: 1</h1>')
|
||||
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: {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user