feat(vue-app): support configurable features (#6287)

This commit is contained in:
Pim 2019-09-05 17:15:27 +02:00 committed by Pooya Parsa
parent 05a6efd1eb
commit 174c13d56c
29 changed files with 685 additions and 199 deletions

View File

@ -0,0 +1,3 @@
# A minimal Hello World Nuxt.js app
https://nuxtjs.org/examples

View File

@ -0,0 +1,28 @@
export default {
loading: false,
loadingIndicator: false,
fetch: {
client: false,
server: false
},
features: {
store: false,
layouts: false,
meta: false,
middleware: false,
transitions: false,
deprecations: false,
validate: false,
asyncData: false,
fetch: false,
clientOnline: false,
clientPrefetch: false,
clientUseUrl: true,
componentAliases: false,
componentClientOnly: false
},
build: {
indicator: false,
terser: true
}
}

View File

@ -0,0 +1,12 @@
{
"name": "example-minimal-features",
"dependencies": {
"nuxt": "latest"
},
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"post-update": "yarn upgrade --latest"
}
}

View File

@ -0,0 +1,21 @@
<template>
<div>
<p>Hi from {{ name }}</p>
<nuxt-link to="/">
Home page
</nuxt-link>
</div>
</template>
<script>
export default {
data () {
return {
name: process.static ? 'static' : (process.server ? 'server' : 'client')
}
},
head: {
title: 'About page'
}
}
</script>

View File

@ -0,0 +1,16 @@
<template>
<div>
<h1>Welcome!</h1>
<nuxt-link to="/about">
About Page
</nuxt-link>
</div>
</template>
<script>
export default {
head: {
title: 'Home page'
}
}
</script>

View File

@ -238,6 +238,8 @@ export default class Builder {
this.resolveMiddleware(templateContext) this.resolveMiddleware(templateContext)
]) ])
this.addOptionalTemplates(templateContext)
await this.resolveCustomTemplates(templateContext) await this.resolveCustomTemplates(templateContext)
await this.resolveLoadingIndicator(templateContext) await this.resolveLoadingIndicator(templateContext)
@ -303,6 +305,16 @@ export default class Builder {
) )
} }
addOptionalTemplates (templateContext) {
if (this.options.build.indicator) {
templateContext.templateFiles.push('components/nuxt-build-indicator.vue')
}
if (this.options.loading !== false) {
templateContext.templateFiles.push('components/nuxt-loading.vue')
}
}
async resolveFiles (dir, cwd = this.options.srcDir) { async resolveFiles (dir, cwd = this.options.srcDir) {
return this.ignore.filter(await glob(this.globPathWithExtensions(dir), { return this.ignore.filter(await glob(this.globPathWithExtensions(dir), {
cwd, cwd,
@ -316,6 +328,10 @@ export default class Builder {
} }
async resolveLayouts ({ templateVars, templateFiles }) { async resolveLayouts ({ templateVars, templateFiles }) {
if (!this.options.features.layouts) {
return
}
if (await fsExtra.exists(path.resolve(this.options.srcDir, this.options.dir.layouts))) { if (await fsExtra.exists(path.resolve(this.options.srcDir, this.options.dir.layouts))) {
for (const file of await this.resolveFiles(this.options.dir.layouts)) { for (const file of await this.resolveFiles(this.options.dir.layouts)) {
const name = file const name = file
@ -409,7 +425,7 @@ export default class Builder {
async resolveStore ({ templateVars, templateFiles }) { async resolveStore ({ templateVars, templateFiles }) {
// Add store if needed // Add store if needed
if (!this.options.store) { if (!this.options.features.store || !this.options.store) {
return return
} }
@ -428,17 +444,20 @@ export default class Builder {
templateFiles.push('store.js') templateFiles.push('store.js')
} }
async resolveMiddleware ({ templateVars }) { async resolveMiddleware ({ templateVars, templateFiles }) {
// -- Middleware -- if (!this.options.features.middleware) {
return
}
const middleware = await this.resolveRelative(this.options.dir.middleware) const middleware = await this.resolveRelative(this.options.dir.middleware)
const extRE = new RegExp(`\\.(${this.supportedExtensions.join('|')})$`) const extRE = new RegExp(`\\.(${this.supportedExtensions.join('|')})$`)
templateVars.middleware = middleware.map(({ src }) => { templateVars.middleware = middleware.map(({ src }) => {
const name = src.replace(extRE, '') const name = src.replace(extRE, '')
const dst = this.relativeToBuild(this.options.srcDir, this.options.dir.middleware, src) const dst = this.relativeToBuild(this.options.srcDir, this.options.dir.middleware, src)
return { name, src, dst } return { name, src, dst }
}) })
templateFiles.push('middleware.js')
} }
async resolveCustomTemplates (templateContext) { async resolveCustomTemplates (templateContext) {

View File

@ -11,6 +11,7 @@ export default class TemplateContext {
this.templateFiles = Array.from(builder.template.files) this.templateFiles = Array.from(builder.template.files)
this.templateVars = { this.templateVars = {
nuxtOptions: options, nuxtOptions: options,
features: options.features,
extensions: options.extensions extensions: options.extensions
.map(ext => ext.replace(/^\./, '')) .map(ext => ext.replace(/^\./, ''))
.join('|'), .join('|'),
@ -27,7 +28,7 @@ export default class TemplateContext {
router: options.router, router: options.router,
env: options.env, env: options.env,
head: options.head, head: options.head,
store: options.store, store: options.features.store ? options.store : false,
globalName: options.globalName, globalName: options.globalName,
globals: builder.globals, globals: builder.globals,
css: options.css, css: options.css,

View File

@ -48,12 +48,14 @@ describe('builder: builder generate', () => {
}, },
watch: [] watch: []
} }
const builder = new Builder(nuxt, BundleBuilder) const builder = new Builder(nuxt, BundleBuilder)
builder.normalizePlugins = jest.fn(() => [{ name: 'test_plugin', src: '/var/somesrc' }]) builder.normalizePlugins = jest.fn(() => [{ name: 'test_plugin', src: '/var/somesrc' }])
builder.resolveLayouts = jest.fn(() => 'resolveLayouts') builder.resolveLayouts = jest.fn(() => 'resolveLayouts')
builder.resolveRoutes = jest.fn(() => 'resolveRoutes') builder.resolveRoutes = jest.fn(() => 'resolveRoutes')
builder.resolveStore = jest.fn(() => 'resolveStore') builder.resolveStore = jest.fn(() => 'resolveStore')
builder.resolveMiddleware = jest.fn(() => 'resolveMiddleware') builder.resolveMiddleware = jest.fn(() => 'resolveMiddleware')
builder.addOptionalTemplates = jest.fn()
builder.resolveCustomTemplates = jest.fn() builder.resolveCustomTemplates = jest.fn()
builder.resolveLoadingIndicator = jest.fn() builder.resolveLoadingIndicator = jest.fn()
builder.compileTemplates = jest.fn() builder.compileTemplates = jest.fn()
@ -77,6 +79,7 @@ describe('builder: builder generate', () => {
'resolveStore', 'resolveStore',
'resolveMiddleware' 'resolveMiddleware'
]) ])
expect(builder.addOptionalTemplates).toBeCalledTimes(1)
expect(builder.resolveCustomTemplates).toBeCalledTimes(1) expect(builder.resolveCustomTemplates).toBeCalledTimes(1)
expect(builder.resolveLoadingIndicator).toBeCalledTimes(1) expect(builder.resolveLoadingIndicator).toBeCalledTimes(1)
expect(builder.options.build.watch).toEqual(['/var/nuxt/src/template/**/*.{vue,js}']) expect(builder.options.build.watch).toEqual(['/var/nuxt/src/template/**/*.{vue,js}'])
@ -124,6 +127,7 @@ describe('builder: builder generate', () => {
test('should resolve store modules', async () => { test('should resolve store modules', async () => {
const nuxt = createNuxt() const nuxt = createNuxt()
nuxt.options.features = { store: true }
nuxt.options.store = true nuxt.options.store = true
nuxt.options.dir = { nuxt.options.dir = {
store: '/var/nuxt/src/store' store: '/var/nuxt/src/store'
@ -153,8 +157,26 @@ describe('builder: builder generate', () => {
expect(templateFiles).toEqual(['store.js']) expect(templateFiles).toEqual(['store.js'])
}) })
test('should disable store resolving', async () => { test('should disable store resolving when not set', async () => {
const nuxt = createNuxt() const nuxt = createNuxt()
nuxt.options.features = { store: false }
nuxt.options.dir = {
store: '/var/nuxt/src/store'
}
const builder = new Builder(nuxt, BundleBuilder)
const templateVars = {}
const templateFiles = []
await builder.resolveStore({ templateVars, templateFiles })
expect(templateVars.storeModules).toBeUndefined()
expect(templateFiles).toEqual([])
})
test('should disable store resolving when feature disabled', async () => {
const nuxt = createNuxt()
nuxt.options.features = { store: false }
nuxt.options.store = true
nuxt.options.dir = { nuxt.options.dir = {
store: '/var/nuxt/src/store' store: '/var/nuxt/src/store'
} }
@ -170,6 +192,7 @@ describe('builder: builder generate', () => {
test('should resolve middleware', async () => { test('should resolve middleware', async () => {
const nuxt = createNuxt() const nuxt = createNuxt()
nuxt.options.features = { middleware: true }
nuxt.options.store = false nuxt.options.store = false
nuxt.options.srcDir = '/var/nuxt/src' nuxt.options.srcDir = '/var/nuxt/src'
nuxt.options.dir = { nuxt.options.dir = {
@ -183,13 +206,31 @@ describe('builder: builder generate', () => {
builder.relativeToBuild = jest.fn().mockReturnValue(middlewarePath) builder.relativeToBuild = jest.fn().mockReturnValue(middlewarePath)
const templateVars = {} const templateVars = {}
await builder.resolveMiddleware({ templateVars }) const templateFiles = []
await builder.resolveMiddleware({ templateVars, templateFiles })
expect(templateVars.middleware).toEqual([{ expect(templateVars.middleware).toEqual([
{
name: 'subfolder/midd', name: 'subfolder/midd',
src: 'subfolder/midd.js', src: 'subfolder/midd.js',
dst: 'subfolder/midd.js' dst: 'subfolder/midd.js'
}]) }
])
expect(templateFiles).toEqual(['middleware.js'])
})
test('should disable middleware when feature disabled', async () => {
const nuxt = createNuxt()
nuxt.options.features = { middleware: false }
nuxt.options.store = false
nuxt.options.dir = {
middleware: '/var/nuxt/src/middleware'
}
const builder = new Builder(nuxt, BundleBuilder)
const templateVars = {}
const templateFiles = []
await builder.resolveMiddleware({ templateVars, templateFiles })
expect(templateFiles).toEqual([])
}) })
test('should custom templates', async () => { test('should custom templates', async () => {
@ -414,6 +455,7 @@ describe('builder: builder generate', () => {
describe('builder: builder resolveLayouts', () => { describe('builder: builder resolveLayouts', () => {
test('should resolve layouts', async () => { test('should resolve layouts', async () => {
const nuxt = createNuxt() const nuxt = createNuxt()
nuxt.options.features = { layouts: true }
nuxt.options.srcDir = '/var/nuxt/src' nuxt.options.srcDir = '/var/nuxt/src'
nuxt.options.buildDir = '/var/nuxt/build' nuxt.options.buildDir = '/var/nuxt/build'
nuxt.options.dir = { nuxt.options.dir = {
@ -466,6 +508,7 @@ describe('builder: builder generate', () => {
test('should resolve error layouts', async () => { test('should resolve error layouts', async () => {
const nuxt = createNuxt() const nuxt = createNuxt()
nuxt.options.features = { layouts: true }
nuxt.options.srcDir = '/var/nuxt/src' nuxt.options.srcDir = '/var/nuxt/src'
nuxt.options.dir = { nuxt.options.dir = {
layouts: '/var/nuxt/src/layouts' layouts: '/var/nuxt/src/layouts'
@ -493,6 +536,7 @@ describe('builder: builder generate', () => {
test('should not resolve layouts if layouts dir does not exist', async () => { test('should not resolve layouts if layouts dir does not exist', async () => {
const nuxt = createNuxt() const nuxt = createNuxt()
nuxt.options.features = { layouts: true }
nuxt.options.srcDir = '/var/nuxt/src' nuxt.options.srcDir = '/var/nuxt/src'
nuxt.options.dir = { nuxt.options.dir = {
layouts: '/var/nuxt/src/layouts' layouts: '/var/nuxt/src/layouts'

View File

@ -20,6 +20,9 @@ TemplateContext {
], ],
"env": "test_env", "env": "test_env",
"extensions": "test|ext", "extensions": "test|ext",
"features": Object {
"store": true,
},
"fetch": undefined, "fetch": undefined,
"globalName": "test_global", "globalName": "test_global",
"globals": Array [ "globals": Array [
@ -59,6 +62,9 @@ TemplateContext {
"test", "test",
"ext", "ext",
], ],
"features": Object {
"store": true,
},
"globalName": "test_global", "globalName": "test_global",
"head": "test_head", "head": "test_head",
"layoutTransition": Object { "layoutTransition": Object {

View File

@ -16,6 +16,7 @@ describe('builder: buildContext', () => {
relativeToBuild: jest.fn((...args) => `relativeBuild(${args.join(', ')})`) relativeToBuild: jest.fn((...args) => `relativeBuild(${args.join(', ')})`)
} }
const options = { const options = {
features: { store: true },
extensions: [ 'test', 'ext' ], extensions: [ 'test', 'ext' ],
messages: { test: 'test message' }, messages: { test: 'test message' },
build: { build: {

View File

@ -56,5 +56,22 @@ export default () => ({
layoutTransition: { layoutTransition: {
name: 'layout', name: 'layout',
mode: 'out-in' mode: 'out-in'
},
features: {
store: true,
layouts: true,
meta: true,
middleware: true,
transitions: true,
deprecations: true,
validate: true,
asyncData: true,
fetch: true,
clientOnline: true,
clientPrefetch: true,
clientUseUrl: false,
componentAliases: true,
componentClientOnly: true
} }
}) })

View File

@ -166,6 +166,22 @@ Object {
"js", "js",
"mjs", "mjs",
], ],
"features": Object {
"asyncData": true,
"clientOnline": true,
"clientPrefetch": true,
"clientUseUrl": false,
"componentAliases": true,
"componentClientOnly": true,
"deprecations": true,
"fetch": true,
"layouts": true,
"meta": true,
"middleware": true,
"store": true,
"transitions": true,
"validate": true,
},
"fetch": Object { "fetch": Object {
"client": true, "client": true,
"server": true, "server": true,

View File

@ -143,6 +143,22 @@ Object {
"env": Object {}, "env": Object {},
"extendPlugins": null, "extendPlugins": null,
"extensions": Array [], "extensions": Array [],
"features": Object {
"asyncData": true,
"clientOnline": true,
"clientPrefetch": true,
"clientUseUrl": false,
"componentAliases": true,
"componentClientOnly": true,
"deprecations": true,
"fetch": true,
"layouts": true,
"meta": true,
"middleware": true,
"store": true,
"transitions": true,
"validate": true,
},
"fetch": Object { "fetch": Object {
"client": true, "client": true,
"server": true, "server": true,
@ -474,6 +490,22 @@ Object {
"env": Object {}, "env": Object {},
"extendPlugins": null, "extendPlugins": null,
"extensions": Array [], "extensions": Array [],
"features": Object {
"asyncData": true,
"clientOnline": true,
"clientPrefetch": true,
"clientUseUrl": false,
"componentAliases": true,
"componentClientOnly": true,
"deprecations": true,
"fetch": true,
"layouts": true,
"meta": true,
"middleware": true,
"store": true,
"transitions": true,
"validate": true,
},
"fetch": Object { "fetch": Object {
"client": true, "client": true,
"server": true, "server": true,

View File

@ -8,15 +8,12 @@ export const template = {
'App.js', 'App.js',
'client.js', 'client.js',
'index.js', 'index.js',
'middleware.js',
'router.js', 'router.js',
'router.scrollBehavior.js', 'router.scrollBehavior.js',
'server.js', 'server.js',
'utils.js', 'utils.js',
'empty.js', 'empty.js',
'components/nuxt-build-indicator.vue',
'components/nuxt-error.vue', 'components/nuxt-error.vue',
'components/nuxt-loading.vue',
'components/nuxt-child.js', 'components/nuxt-child.js',
'components/nuxt-link.server.js', 'components/nuxt-link.server.js',
'components/nuxt-link.client.js', 'components/nuxt-link.client.js',

View File

@ -1,11 +1,18 @@
import Vue from 'vue' import Vue from 'vue'
import { getMatchedComponentsInstances, promisify, globalHandleError } from './utils' <% if (features.asyncData || features.fetch) { %>
import {
getMatchedComponentsInstances,
promisify,
globalHandleError
} from './utils'
<% } %>
<% if (loading) { %>import NuxtLoading from '<%= (typeof loading === "string" ? loading : "./components/nuxt-loading.vue") %>'<% } %> <% if (loading) { %>import NuxtLoading from '<%= (typeof loading === "string" ? loading : "./components/nuxt-loading.vue") %>'<% } %>
<% if (buildIndicator) { %>import NuxtBuildIndicator from './components/nuxt-build-indicator'<% } %> <% if (buildIndicator) { %>import NuxtBuildIndicator from './components/nuxt-build-indicator'<% } %>
<% css.forEach((c) => { %> <% css.forEach((c) => { %>
import '<%= relativeToBuild(resolvePath(c.src || c, { isStyle: true })) %>' import '<%= relativeToBuild(resolvePath(c.src || c, { isStyle: true })) %>'
<% }) %> <% }) %>
<% if (features.layouts) { %>
<%= Object.keys(layouts).map((key) => { <%= Object.keys(layouts).map((key) => {
if (splitChunks.layouts) { if (splitChunks.layouts) {
return `const _${hash(key)} = () => import('${layouts[key]}' /* webpackChunkName: "${wChunk('layouts/' + key)}" */).then(m => m.default || m)` return `const _${hash(key)} = () => import('${layouts[key]}' /* webpackChunkName: "${wChunk('layouts/' + key)}" */).then(m => m.default || m)`
@ -17,13 +24,17 @@ import '<%= relativeToBuild(resolvePath(c.src || c, { isStyle: true })) %>'
const layouts = { <%= Object.keys(layouts).map(key => `"_${key}": _${hash(key)}`).join(',') %> }<%= isTest ? '// eslint-disable-line' : '' %> const layouts = { <%= Object.keys(layouts).map(key => `"_${key}": _${hash(key)}`).join(',') %> }<%= isTest ? '// eslint-disable-line' : '' %>
<% if (splitChunks.layouts) { %>let resolvedLayouts = {}<% } %> <% if (splitChunks.layouts) { %>let resolvedLayouts = {}<% } %>
<% } %>
export default { export default {
<% if (features.meta) { %>
<%= isTest ? '/* eslint-disable quotes, semi, indent, comma-spacing, key-spacing, object-curly-spacing, space-before-function-paren */' : '' %> <%= isTest ? '/* eslint-disable quotes, semi, indent, comma-spacing, key-spacing, object-curly-spacing, space-before-function-paren */' : '' %>
head: <%= serializeFunction(head) %>, head: <%= serializeFunction(head) %>,
<%= isTest ? '/* eslint-enable quotes, semi, indent, comma-spacing, key-spacing, object-curly-spacing, space-before-function-paren */' : '' %> <%= isTest ? '/* eslint-enable quotes, semi, indent, comma-spacing, key-spacing, object-curly-spacing, space-before-function-paren */' : '' %>
<% } %>
render(h, props) { render(h, props) {
<% if (loading) { %>const loadingEl = h('NuxtLoading', { ref: 'loading' })<% } %> <% if (loading) { %>const loadingEl = h('NuxtLoading', { ref: 'loading' })<% } %>
<% if (features.layouts) { %>
const layoutEl = h(this.layout || 'nuxt') const layoutEl = h(this.layout || 'nuxt')
const templateEl = h('div', { const templateEl = h('div', {
domProps: { domProps: {
@ -31,7 +42,11 @@ export default {
}, },
key: this.layoutName key: this.layoutName
}, [ layoutEl ]) }, [ layoutEl ])
<% } else { %>
const templateEl = h('nuxt')
<% } %>
<% if (features.transitions) { %>
const transitionEl = h('transition', { const transitionEl = h('transition', {
props: { props: {
name: '<%= layoutTransition.name %>', name: '<%= layoutTransition.name %>',
@ -46,18 +61,29 @@ export default {
} }
} }
}, [ templateEl ]) }, [ templateEl ])
<% } %>
return h('div', { return h('div', {
domProps: { domProps: {
id: '<%= globals.id %>' id: '<%= globals.id %>'
} }
}, [<% if (loading) { %>loadingEl, <% } %><%if (buildIndicator) { %>h(NuxtBuildIndicator), <% } %>transitionEl]) }, [
<% if (loading) { %>loadingEl, <% } %>
<% if (buildIndicator) { %>h(NuxtBuildIndicator), <% } %>
<% if (features.transitions) { %>transitionEl<% } else { %>templateEl<% } %>
])
}, },
<% if (features.clientOnline || features.layouts) { %>
data: () => ({ data: () => ({
<% if (features.clientOnline) { %>
isOnline: true, isOnline: true,
<% } %>
<% if (features.layouts) { %>
layout: null, layout: null,
layoutName: '' layoutName: ''
<% } %>
}), }),
<% } %>
beforeCreate() { beforeCreate() {
Vue.util.defineReactive(this, 'nuxt', this.$options.nuxt) Vue.util.defineReactive(this, 'nuxt', this.$options.nuxt)
}, },
@ -67,10 +93,12 @@ export default {
// add to window so we can listen when ready // add to window so we can listen when ready
if (process.client) { if (process.client) {
window.<%= globals.nuxt %> = <%= (globals.nuxt !== '$nuxt' ? 'window.$nuxt = ' : '') %>this window.<%= globals.nuxt %> = <%= (globals.nuxt !== '$nuxt' ? 'window.$nuxt = ' : '') %>this
<% if (features.clientOnline) { %>
this.refreshOnlineStatus() this.refreshOnlineStatus()
// Setup the listeners // Setup the listeners
window.addEventListener('online', this.refreshOnlineStatus) window.addEventListener('online', this.refreshOnlineStatus)
window.addEventListener('offline', this.refreshOnlineStatus) window.addEventListener('offline', this.refreshOnlineStatus)
<% } %>
} }
// Add $nuxt.error() // Add $nuxt.error()
this.error = this.nuxt.error this.error = this.nuxt.error
@ -85,12 +113,15 @@ export default {
'nuxt.err': 'errorChanged' 'nuxt.err': 'errorChanged'
}, },
<% } %> <% } %>
<% if (features.clientOnline) { %>
computed: { computed: {
isOffline() { isOffline() {
return !this.isOnline return !this.isOnline
} }
}, },
<% } %>
methods: { methods: {
<% if (features.clientOnline) { %>
refreshOnlineStatus() { refreshOnlineStatus() {
if (process.client) { if (process.client) {
if (typeof window.navigator.onLine === 'undefined') { if (typeof window.navigator.onLine === 'undefined') {
@ -103,19 +134,25 @@ export default {
} }
} }
}, },
<% } %>
async refresh() { async refresh() {
<% if (features.asyncData || features.fetch) { %>
const pages = getMatchedComponentsInstances(this.$route) const pages = getMatchedComponentsInstances(this.$route)
if (!pages.length) { if (!pages.length) {
return return
} }
<% if (loading) { %>this.$loading.start()<% } %> <% if (loading) { %>this.$loading.start()<% } %>
const promises = pages.map(async (page) => { const promises = pages.map(async (page) => {
const p = [] const p = []
<% if (features.fetch) { %>
if (page.$options.fetch) { if (page.$options.fetch) {
p.push(promisify(page.$options.fetch, this.context)) p.push(promisify(page.$options.fetch, this.context))
} }
<% } %>
<% if (features.asyncData) { %>
if (page.$options.asyncData) { if (page.$options.asyncData) {
p.push( p.push(
promisify(page.$options.asyncData, this.context) promisify(page.$options.asyncData, this.context)
@ -126,6 +163,7 @@ export default {
}) })
) )
} }
<% } %>
return Promise.all(p) return Promise.all(p)
}) })
try { try {
@ -136,6 +174,7 @@ export default {
this.error(error) this.error(error)
} }
<% if (loading) { %>this.$loading.finish()<% } %> <% if (loading) { %>this.$loading.finish()<% } %>
<% } %>
}, },
<% if (loading) { %> <% if (loading) { %>
errorChanged() { errorChanged() {
@ -145,6 +184,7 @@ export default {
} }
}, },
<% } %> <% } %>
<% if (features.layouts) { %>
<% if (splitChunks.layouts) { %> <% if (splitChunks.layouts) { %>
setLayout(layout) { setLayout(layout) {
<% if (debug) { %> <% if (debug) { %>
@ -193,9 +233,12 @@ export default {
} }
return Promise.resolve(layouts['_' + layout]) return Promise.resolve(layouts['_' + layout])
} }
<% } %> <% } /* splitChunks.layouts */ %>
<% } /* features.layouts */ %>
}, },
<% if (loading) { %>
components: { components: {
<%= (loading ? 'NuxtLoading' : '') %> NuxtLoading
} }
<% } %>
} }

View File

@ -1,15 +1,15 @@
import Vue from 'vue' import Vue from 'vue'
<% if (fetch.client) { %>import fetch from 'unfetch'<% } %> <% if (fetch.client) { %>import fetch from 'unfetch'<% } %>
import middleware from './middleware.js' <% if (features.middleware) { %>import middleware from './middleware.js'<% } %>
import { import {
applyAsyncData, <% if (features.asyncData) { %>applyAsyncData,<% } %>
<% if (features.middleware) { %>middlewareSeries,<% } %>
sanitizeComponent, sanitizeComponent,
resolveRouteComponents, resolveRouteComponents,
getMatchedComponents, getMatchedComponents,
getMatchedComponentsInstances, getMatchedComponentsInstances,
flatMapComponents, flatMapComponents,
setContext, setContext,
middlewareSeries,
promisify, promisify,
getLocation, getLocation,
compile, compile,
@ -17,7 +17,7 @@ import {
globalHandleError globalHandleError
} from './utils.js' } from './utils.js'
import { createApp, NuxtError } from './index.js' import { createApp, NuxtError } from './index.js'
import NuxtLink from './components/nuxt-link.<%= router.prefetchLinks ? "client" : "server" %>.js' // should be included after ./index.js import NuxtLink from './components/nuxt-link.<%= features.clientPrefetch && router.prefetchLinks ? "client" : "server" %>.js' // should be included after ./index.js
<% if (isDev) { %>import consola from 'consola'<% } %> <% if (isDev) { %>import consola from 'consola'<% } %>
<% if (isDev) { %>consola.wrapConsole() <% if (isDev) { %>consola.wrapConsole()
@ -26,7 +26,7 @@ console.log = console.__log
// Component: <NuxtLink> // Component: <NuxtLink>
Vue.component(NuxtLink.name, NuxtLink) Vue.component(NuxtLink.name, NuxtLink)
Vue.component('NLink', NuxtLink) <% if (features.componentAliases) { %>Vue.component('NLink', NuxtLink)<% } %>
<% if (fetch.client) { %>if (!global.fetch) { global.fetch = fetch }<% } %> <% if (fetch.client) { %>if (!global.fetch) { global.fetch = fetch }<% } %>
@ -94,6 +94,7 @@ const errorHandler = Vue.config.errorHandler || console.error
// Create and mount App // Create and mount App
createApp().then(mountApp).catch(errorHandler) createApp().then(mountApp).catch(errorHandler)
<% if (features.transitions) { %>
function componentOption(component, key, ...args) { function componentOption(component, key, ...args) {
if (!component || !component.options || !component.options[key]) { if (!component || !component.options || !component.options[key]) {
return {} return {}
@ -126,7 +127,7 @@ function mapTransitions(Components, to, from) {
return transitions return transitions
}) })
} }
<% } %>
async function loadAsyncComponents(to, from, next) { async function loadAsyncComponents(to, from, next) {
// Check if route path changed (this._pathChanged), only if the page is not an error (for validate()) // Check if route path changed (this._pathChanged), only if the page is not an error (for validate())
this._pathChanged = Boolean(app.nuxt.err) || from.path !== to.path this._pathChanged = Boolean(app.nuxt.err) || from.path !== to.path
@ -184,9 +185,11 @@ async function loadAsyncComponents(to, from, next) {
} }
function applySSRData(Component, ssrData) { function applySSRData(Component, ssrData) {
<% if (features.asyncData) { %>
if (NUXT.serverRendered && ssrData) { if (NUXT.serverRendered && ssrData) {
applyAsyncData(Component, ssrData) applyAsyncData(Component, ssrData)
} }
<% } %>
Component._Ctor = Component Component._Ctor = Component
return Component return Component
} }
@ -206,11 +209,12 @@ function resolveComponents(router) {
return _Component return _Component
}) })
} }
<% if (features.middleware) { %>
function callMiddleware(Components, context, layout) { function callMiddleware(Components, context, layout) {
let midd = <%= devalue(router.middleware) %><%= isTest ? '// eslint-disable-line' : '' %> let midd = <%= devalue(router.middleware) %><%= isTest ? '// eslint-disable-line' : '' %>
let unknownMiddleware = false let unknownMiddleware = false
<% if (features.layouts) { %>
// If layout is undefined, only call global middleware // If layout is undefined, only call global middleware
if (typeof layout !== 'undefined') { if (typeof layout !== 'undefined') {
midd = [] // Exclude global middleware if layout defined (already called before) midd = [] // Exclude global middleware if layout defined (already called before)
@ -224,6 +228,7 @@ function callMiddleware(Components, context, layout) {
} }
}) })
} }
<% } %>
midd = midd.map((name) => { midd = midd.map((name) => {
if (typeof name === 'function') return name if (typeof name === 'function') return name
@ -237,7 +242,14 @@ function callMiddleware(Components, context, layout) {
if (unknownMiddleware) return if (unknownMiddleware) return
return middlewareSeries(midd, context) return middlewareSeries(midd, context)
} }
<% } else if (isDev) {
// This is a placeholder function mainly so we dont have to
// refactor the promise chain in addHotReload()
%>
function callMiddleware() {
return Promise.resolve(true)
}
<% } %>
async function render(to, from, next) { async function render(to, from, next) {
if (this._pathChanged === false && this._queryChanged === false) return next() if (this._pathChanged === false && this._queryChanged === false) return next()
// Handle first render on SPA mode // Handle first render on SPA mode
@ -282,51 +294,71 @@ async function render(to, from, next) {
// If no Components matched, generate 404 // If no Components matched, generate 404
if (!Components.length) { if (!Components.length) {
<% if (features.middleware) { %>
// Default layout // Default layout
await callMiddleware.call(this, Components, app.context) await callMiddleware.call(this, Components, app.context)
if (nextCalled) return if (nextCalled) return
<% } %>
<% if (features.layouts) { %>
// Load layout for error page // Load layout for error page
const layout = await this.loadLayout( const layout = await this.loadLayout(
typeof NuxtError.layout === 'function' typeof NuxtError.layout === 'function'
? NuxtError.layout(app.context) ? NuxtError.layout(app.context)
: NuxtError.layout : NuxtError.layout
) )
<% } %>
<% if (features.middleware) { %>
await callMiddleware.call(this, Components, app.context, layout) await callMiddleware.call(this, Components, app.context, layout)
if (nextCalled) return if (nextCalled) return
<% } %>
// Show error page // Show error page
app.context.error({ statusCode: 404, message: `<%= messages.error_404 %>` }) app.context.error({ statusCode: 404, message: `<%= messages.error_404 %>` })
return next() return next()
} }
<% if (features.asyncData || features.fetch) { %>
// Update ._data and other properties if hot reloaded // Update ._data and other properties if hot reloaded
Components.forEach((Component) => { Components.forEach((Component) => {
if (Component._Ctor && Component._Ctor.options) { if (Component._Ctor && Component._Ctor.options) {
Component.options.asyncData = Component._Ctor.options.asyncData <% if (features.asyncData) { %>Component.options.asyncData = Component._Ctor.options.asyncData<% } %>
Component.options.fetch = Component._Ctor.options.fetch <% if (features.fetch) { %>Component.options.fetch = Component._Ctor.options.fetch<% } %>
} }
}) })
<% } %>
<% if (features.transitions) { %>
// Apply transitions // Apply transitions
this.setTransitions(mapTransitions(Components, to, from)) this.setTransitions(mapTransitions(Components, to, from))
<% } %>
try { try {
<% if (features.middleware) { %>
// Call middleware // Call middleware
await callMiddleware.call(this, Components, app.context) await callMiddleware.call(this, Components, app.context)
if (nextCalled) return if (nextCalled) return
if (app.context._errored) return next() if (app.context._errored) return next()
<% } %>
<% if (features.layouts) { %>
// Set layout // Set layout
let layout = Components[0].options.layout let layout = Components[0].options.layout
if (typeof layout === 'function') { if (typeof layout === 'function') {
layout = layout(app.context) layout = layout(app.context)
} }
layout = await this.loadLayout(layout) layout = await this.loadLayout(layout)
<% } %>
<% if (features.middleware) { %>
// Call middleware for layout // Call middleware for layout
await callMiddleware.call(this, Components, app.context, layout) await callMiddleware.call(this, Components, app.context, layout)
if (nextCalled) return if (nextCalled) return
if (app.context._errored) return next() if (app.context._errored) return next()
<% } %>
<% if (features.validate) { %>
// Call .validate() // Call .validate()
let isValid = true let isValid = true
try { try {
@ -355,7 +387,9 @@ async function render(to, from, next) {
this.error({ statusCode: 404, message: `<%= messages.error_404 %>` }) this.error({ statusCode: 404, message: `<%= messages.error_404 %>` })
return next() return next()
} }
<% } %>
<% if (features.asyncData || features.fetch) { %>
let instances let instances
// Call asyncData & fetch hooks on components matched by the route. // Call asyncData & fetch hooks on components matched by the route.
await Promise.all(Components.map((Component, i) => { await Promise.all(Components.map((Component, i) => {
@ -380,20 +414,31 @@ async function render(to, from, next) {
} }
} }
if (!this._hadError && this._isMounted && !Component._dataRefresh) { if (!this._hadError && this._isMounted && !Component._dataRefresh) {
return Promise.resolve() return
} }
const promises = [] const promises = []
<% if (features.asyncData) { %>
const hasAsyncData = ( const hasAsyncData = (
Component.options.asyncData && Component.options.asyncData &&
typeof Component.options.asyncData === 'function' typeof Component.options.asyncData === 'function'
) )
<% } else { %>
const hasAsyncData = false
<% } %>
<% if (features.fetch) { %>
const hasFetch = Boolean(Component.options.fetch) const hasFetch = Boolean(Component.options.fetch)
<% } else { %>
const hasFetch = false
<% } %>
<% if (loading) { %> <% if (loading) { %>
const loadingIncrease = (hasAsyncData && hasFetch) ? 30 : 45 const loadingIncrease = (hasAsyncData && hasFetch) ? 30 : 45
<% } %> <% } %>
<% if (features.asyncData) { %>
// Call asyncData(context) // Call asyncData(context)
if (hasAsyncData) { if (hasAsyncData) {
const promise = promisify(Component.options.asyncData, app.context) const promise = promisify(Component.options.asyncData, app.context)
@ -407,10 +452,12 @@ async function render(to, from, next) {
}) })
promises.push(promise) promises.push(promise)
} }
<% } %>
// Check disabled page loading // Check disabled page loading
this.$loading.manual = Component.options.loading === false this.$loading.manual = Component.options.loading === false
<% if (features.fetch) { %>
// Call fetch(context) // Call fetch(context)
if (hasFetch) { if (hasFetch) {
let p = Component.options.fetch(app.context) let p = Component.options.fetch(app.context)
@ -426,9 +473,11 @@ async function render(to, from, next) {
}) })
promises.push(p) promises.push(p)
} }
<% } %>
return Promise.all(promises) return Promise.all(promises)
})) }))
<% } %>
// If not redirected // If not redirected
if (!nextCalled) { if (!nextCalled) {
@ -449,12 +498,14 @@ async function render(to, from, next) {
globalHandleError(error) globalHandleError(error)
<% if (features.layouts) { %>
// Load error layout // Load error layout
let layout = NuxtError.layout let layout = NuxtError.layout
if (typeof layout === 'function') { if (typeof layout === 'function') {
layout = layout(app.context) layout = layout(app.context)
} }
await this.loadLayout(layout) await this.loadLayout(layout)
<% } %>
this.error(error) this.error(error)
this.<%= globals.nuxt %>.$emit('routeChanged', to, from, error) this.<%= globals.nuxt %>.$emit('routeChanged', to, from, error)
@ -481,6 +532,7 @@ function showNextPage(to) {
this.error() this.error()
} }
<% if (features.layouts) { %>
// Set layout // Set layout
let layout = this.$options.nuxt.err let layout = this.$options.nuxt.err
? NuxtError.layout ? NuxtError.layout
@ -490,6 +542,7 @@ function showNextPage(to) {
layout = layout(app.context) layout = layout(app.context)
} }
this.setLayout(layout) this.setLayout(layout)
<% } %>
} }
// When navigating on a different route but the same component is used, Vue.js // When navigating on a different route but the same component is used, Vue.js
@ -581,7 +634,9 @@ function addHotReload($component, depth) {
$component.$vnode.context.$forceUpdate = async () => { $component.$vnode.context.$forceUpdate = async () => {
let Components = getMatchedComponents(router.currentRoute) let Components = getMatchedComponents(router.currentRoute)
let Component = Components[depth] let Component = Components[depth]
if (!Component) return _forceUpdate() if (!Component) {
return _forceUpdate()
}
if (typeof Component === 'object' && !Component.options) { if (typeof Component === 'object' && !Component.options) {
// Updated via vue-router resolveAsyncComponents() // Updated via vue-router resolveAsyncComponents()
Component = Vue.extend(Component) Component = Vue.extend(Component)
@ -599,29 +654,45 @@ function addHotReload($component, depth) {
next: next.bind(this) next: next.bind(this)
}) })
const context = app.context const context = app.context
<% if (loading) { %> <% if (loading) { %>
if (this.$loading.start && !this.$loading.manual) this.$loading.start() if (this.$loading.start && !this.$loading.manual) {
this.$loading.start()
}
<% } %> <% } %>
callMiddleware.call(this, Components, context) callMiddleware.call(this, Components, context)
.then(() => { .then(() => {
<% if (features.layouts) { %>
// If layout changed // If layout changed
if (depth !== 0) return Promise.resolve() if (depth !== 0) {
return
}
let layout = Component.options.layout || 'default' let layout = Component.options.layout || 'default'
if (typeof layout === 'function') { if (typeof layout === 'function') {
layout = layout(context) layout = layout(context)
} }
if (this.layoutName === layout) return Promise.resolve() if (this.layoutName === layout) {
return
}
let promise = this.loadLayout(layout) let promise = this.loadLayout(layout)
promise.then(() => { promise.then(() => {
this.setLayout(layout) this.setLayout(layout)
Vue.nextTick(() => hotReloadAPI(this)) Vue.nextTick(() => hotReloadAPI(this))
}) })
return promise return promise
<% } else { %>
return
<% } %>
}) })
<% if (features.layouts) { %>
.then(() => { .then(() => {
return callMiddleware.call(this, Components, context, this.layout) return callMiddleware.call(this, Components, context, this.layout)
}) })
<% } %>
.then(() => { .then(() => {
<% if (features.asyncData) { %>
// Call asyncData(context) // Call asyncData(context)
let pAsyncData = promisify(Component.options.asyncData || noopData, context) let pAsyncData = promisify(Component.options.asyncData || noopData, context)
pAsyncData.then((asyncDataResult) => { pAsyncData.then((asyncDataResult) => {
@ -629,12 +700,16 @@ function addHotReload($component, depth) {
<%= (loading ? 'this.$loading.increase && this.$loading.increase(30)' : '') %> <%= (loading ? 'this.$loading.increase && this.$loading.increase(30)' : '') %>
}) })
promises.push(pAsyncData) promises.push(pAsyncData)
<% } %>
<% if (features.fetch) { %>
// Call fetch() // Call fetch()
Component.options.fetch = Component.options.fetch || noopFetch Component.options.fetch = Component.options.fetch || noopFetch
let pFetch = Component.options.fetch(context) let pFetch = Component.options.fetch(context)
if (!pFetch || (!(pFetch instanceof Promise) && (typeof pFetch.then !== 'function'))) { pFetch = Promise.resolve(pFetch) } if (!pFetch || (!(pFetch instanceof Promise) && (typeof pFetch.then !== 'function'))) { pFetch = Promise.resolve(pFetch) }
<%= (loading ? 'pFetch.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %> <%= (loading ? 'pFetch.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %>
promises.push(pFetch) promises.push(pFetch)
<% } %>
return Promise.all(promises) return Promise.all(promises)
}) })
.then(() => { .then(() => {
@ -658,7 +733,7 @@ async function mountApp(__app) {
// Create Vue instance // Create Vue instance
const _app = new Vue(app) const _app = new Vue(app)
<% if (mode !== 'spa') { %> <% if (features.layouts && mode !== 'spa') { %>
// Load layout // Load layout
const layout = NUXT.layout || 'default' const layout = NUXT.layout || 'default'
await _app.loadLayout(layout) await _app.loadLayout(layout)
@ -683,14 +758,14 @@ async function mountApp(__app) {
<% } %> <% } %>
}) })
} }
<% if (features.transitions) { %>
// Enable transitions // Enable transitions
_app.setTransitions = _app.$options.nuxt.setTransitions.bind(_app) _app.setTransitions = _app.$options.nuxt.setTransitions.bind(_app)
if (Components.length) { if (Components.length) {
_app.setTransitions(mapTransitions(Components, router.currentRoute)) _app.setTransitions(mapTransitions(Components, router.currentRoute))
_lastPaths = router.currentRoute.matched.map(route => compile(route.path)(router.currentRoute.params)) _lastPaths = router.currentRoute.matched.map(route => compile(route.path)(router.currentRoute.params))
} }
<% } %>
// Initialize error handler // Initialize error handler
_app.$loading = {} // To avoid error while _app.$nuxt does not exist _app.$loading = {} // To avoid error while _app.$nuxt does not exist
if (NUXT.error) _app.error(NUXT.error) if (NUXT.error) _app.error(NUXT.error)

View File

@ -14,6 +14,7 @@ export default {
} }
}, },
render(h, { parent, data, props }) { render(h, { parent, data, props }) {
<% if (features.transitions) { %>
data.nuxtChild = true data.nuxtChild = true
const _parent = parent const _parent = parent
const transitions = parent.<%= globals.nuxt %>.nuxt.transitions const transitions = parent.<%= globals.nuxt %>.nuxt.transitions
@ -69,20 +70,25 @@ export default {
} }
} }
} }
<% } %>
let routerView = h('routerView', data) let routerView = h('routerView', data)
if (props.keepAlive) { if (props.keepAlive) {
routerView = h('keep-alive', { props: props.keepAliveProps }, [routerView]) routerView = h('keep-alive', { props: props.keepAliveProps }, [routerView])
} }
<% if (features.transitions) { %>
return h('transition', { return h('transition', {
props: transitionProps, props: transitionProps,
on: listeners on: listeners
}, [routerView]) }, [routerView])
<% } else { %>
return routerView
<% } %>
} }
} }
<% if (features.transitions) { %>
const transitionsKeys = [ const transitionsKeys = [
'name', 'name',
'mode', 'mode',
@ -116,3 +122,4 @@ const listenersKeys = [
'afterAppear', 'afterAppear',
'appearCancelled' 'appearCancelled'
] ]
<% } %>

View File

@ -9,7 +9,7 @@ import NuxtError from '<%= "../" + components.ErrorPage %>'
<% } %> <% } %>
<% } else { %> <% } else { %>
import NuxtError from './nuxt-error.vue' import NuxtError from './nuxt-error.vue'
<% } %> <% } /* components */ %>
import NuxtChild from './nuxt-child' import NuxtChild from './nuxt-child'
<%= isTest ? '// @vue/component' : '' %> <%= isTest ? '// @vue/component' : '' %>

View File

@ -1,7 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import Meta from 'vue-meta' <% if (features.meta) { %>import Meta from 'vue-meta'<% } %>
import ClientOnly from 'vue-client-only' <% if (features.componentClientOnly) { %>import ClientOnly from 'vue-client-only'<% } %>
import NoSsr from 'vue-no-ssr' <% if (features.deprecations) { %>import NoSsr from 'vue-no-ssr'<% } %>
import { createRouter } from './router.js' import { createRouter } from './router.js'
import NuxtChild from './components/nuxt-child.js' import NuxtChild from './components/nuxt-child.js'
import NuxtError from '<%= components.ErrorPage ? components.ErrorPage : "./components/nuxt-error.vue" %>' import NuxtError from '<%= components.ErrorPage ? components.ErrorPage : "./components/nuxt-error.vue" %>'
@ -16,8 +16,11 @@ import { setContext, getLocation, getRouteData, normalizeError } from './utils'
<% }) %> <% }) %>
<%= isTest ? '/* eslint-enable camelcase */' : '' %> <%= isTest ? '/* eslint-enable camelcase */' : '' %>
<% if (features.componentClientOnly) { %>
// Component: <ClientOnly> // Component: <ClientOnly>
Vue.component(ClientOnly.name, ClientOnly) Vue.component(ClientOnly.name, ClientOnly)
<% } %>
<% if (features.deprecations) { %>
// TODO: Remove in Nuxt 3: <NoSsr> // TODO: Remove in Nuxt 3: <NoSsr>
Vue.component(NoSsr.name, { Vue.component(NoSsr.name, {
...NoSsr, ...NoSsr,
@ -29,16 +32,17 @@ Vue.component(NoSsr.name, {
return NoSsr.render(h, ctx) return NoSsr.render(h, ctx)
} }
}) })
<% } %>
// Component: <NuxtChild> // Component: <NuxtChild>
Vue.component(NuxtChild.name, NuxtChild) Vue.component(NuxtChild.name, NuxtChild)
Vue.component('NChild', NuxtChild) <% if (features.componentAliases) { %>Vue.component('NChild', NuxtChild)<% } %>
// Component NuxtLink is imported in server.js or client.js // Component NuxtLink is imported in server.js or client.js
// Component: <Nuxt>` // Component: <Nuxt>`
Vue.component(Nuxt.name, Nuxt) Vue.component(Nuxt.name, Nuxt)
<% if (features.meta) { %>
// vue-meta configuration // vue-meta configuration
Vue.use(Meta, { Vue.use(Meta, {
keyName: 'head', // the component option name that vue-meta looks for meta info on. keyName: 'head', // the component option name that vue-meta looks for meta info on.
@ -46,7 +50,9 @@ Vue.use(Meta, {
ssrAttribute: 'data-n-head-ssr', // the attribute name that lets vue-meta know that meta info has already been server-rendered ssrAttribute: 'data-n-head-ssr', // the attribute name that lets vue-meta know that meta info has already been server-rendered
tagIDKeyName: 'hid' // the property name that vue-meta uses to determine whether to overwrite or append a tag tagIDKeyName: 'hid' // the property name that vue-meta uses to determine whether to overwrite or append a tag
}) })
<% } %>
<% if (features.transitions) { %>
const defaultTransition = <%= const defaultTransition = <%=
serialize(pageTransition) serialize(pageTransition)
.replace('beforeEnter(', 'function(').replace('enter(', 'function(').replace('afterEnter(', 'function(') .replace('beforeEnter(', 'function(').replace('enter(', 'function(').replace('afterEnter(', 'function(')
@ -54,6 +60,7 @@ const defaultTransition = <%=
.replace('afterLeave(', 'function(').replace('leaveCancelled(', 'function(').replace('beforeAppear(', 'function(') .replace('afterLeave(', 'function(').replace('leaveCancelled(', 'function(').replace('beforeAppear(', 'function(')
.replace('appear(', 'function(').replace('afterAppear(', 'function(').replace('appearCancelled(', 'function(') .replace('appear(', 'function(').replace('afterAppear(', 'function(').replace('appearCancelled(', 'function(')
%><%= isTest ? '// eslint-disable-line' : '' %> %><%= isTest ? '// eslint-disable-line' : '' %>
<% } %>
async function createApp(ssrContext) { async function createApp(ssrContext) {
const router = await createRouter(ssrContext) const router = await createRouter(ssrContext)
@ -74,9 +81,10 @@ async function createApp(ssrContext) {
// here we inject the router and store to all child components, // here we inject the router and store to all child components,
// making them available everywhere as `this.$router` and `this.$store`. // making them available everywhere as `this.$router` and `this.$store`.
const app = { const app = {
router,
<% if (store) { %>store,<% } %> <% if (store) { %>store,<% } %>
router,
nuxt: { nuxt: {
<% if (features.transitions) { %>
defaultTransition, defaultTransition,
transitions: [ defaultTransition ], transitions: [ defaultTransition ],
setTransitions(transitions) { setTransitions(transitions) {
@ -96,6 +104,7 @@ async function createApp(ssrContext) {
this.$options.nuxt.transitions = transitions this.$options.nuxt.transitions = transitions
return transitions return transitions
}, },
<% } %>
err: null, err: null,
dateErr: null, dateErr: null,
error(err) { error(err) {
@ -128,10 +137,10 @@ async function createApp(ssrContext) {
// Set context to app.context // Set context to app.context
await setContext(app, { await setContext(app, {
<% if (store) { %>store,<% } %>
route, route,
next, next,
error: app.nuxt.error.bind(app), error: app.nuxt.error.bind(app),
<% if (store) { %>store,<% } %>
payload: ssrContext ? ssrContext.payload : undefined, payload: ssrContext ? ssrContext.payload : undefined,
req: ssrContext ? ssrContext.req : undefined, req: ssrContext ? ssrContext.req : undefined,
res: ssrContext ? ssrContext.res : undefined, res: ssrContext ? ssrContext.res : undefined,
@ -213,8 +222,8 @@ async function createApp(ssrContext) {
} }
return { return {
app,
<% if(store) { %>store,<% } %> <% if(store) { %>store,<% } %>
app,
router router
} }
} }

View File

@ -1,19 +1,29 @@
import { stringify } from 'querystring' import { stringify } from 'querystring'
import Vue from 'vue' import Vue from 'vue'
<% if (fetch.server) { %>import fetch from 'node-fetch'<% } %> <% if (fetch.server) { %>import fetch from 'node-fetch'<% } %>
import middleware from './middleware.js' <% if (features.middleware) { %>import middleware from './middleware.js'<% } %>
import { applyAsyncData, getMatchedComponents, middlewareSeries, promisify, urlJoin, sanitizeComponent } from './utils.js' import {
<% if (features.asyncData) { %>applyAsyncData,<% } %>
<% if (features.middleware) { %>middlewareSeries,<% } %>
getMatchedComponents,
promisify,
sanitizeComponent
} from './utils.js'
import { createApp, NuxtError } from './index.js' import { createApp, NuxtError } from './index.js'
import NuxtLink from './components/nuxt-link.server.js' // should be included after ./index.js import NuxtLink from './components/nuxt-link.server.js' // should be included after ./index.js
// Component: <NuxtLink> // Component: <NuxtLink>
Vue.component(NuxtLink.name, NuxtLink) Vue.component(NuxtLink.name, NuxtLink)
Vue.component('NLink', NuxtLink) <% if (features.componentAliases) { %>Vue.component('NLink', NuxtLink)<% } %>
<% if (fetch.server) { %>if (!global.fetch) { global.fetch = fetch }<% } %> <% if (fetch.server) { %>if (!global.fetch) { global.fetch = fetch }<% } %>
const noopApp = () => new Vue({ render: h => h('div') }) const noopApp = () => new Vue({ render: h => h('div') })
function urlJoin() {
return Array.prototype.slice.call(arguments).join('/').replace(/\/+/g, '/')
}
const createNext = ssrContext => (opts) => { const createNext = ssrContext => (opts) => {
ssrContext.redirected = opts ssrContext.redirected = opts
// If nuxt generate // If nuxt generate
@ -50,32 +60,39 @@ export default async (ssrContext) => {
// Used for beforeNuxtRender({ Components, nuxtState }) // Used for beforeNuxtRender({ Components, nuxtState })
ssrContext.beforeRenderFns = [] ssrContext.beforeRenderFns = []
// Nuxt object (window{{globals.context}}, defaults to window.__NUXT__) // Nuxt object (window{{globals.context}}, defaults to window.__NUXT__)
ssrContext.nuxt = { layout: 'default', data: [], error: null<%= (store ? ', state: null' : '') %>, serverRendered: true } ssrContext.nuxt = { <% if (features.layouts) { %>layout: 'default', <% } %>data: [], error: null<%= (store ? ', state: null' : '') %>, serverRendered: true }
// Create the app definition and the instance (created for each request) // Create the app definition and the instance (created for each request)
const { app, router<%= (store ? ', store' : '') %> } = await createApp(ssrContext) const { app, router<%= (store ? ', store' : '') %> } = await createApp(ssrContext)
const _app = new Vue(app) const _app = new Vue(app)
<% if (features.meta) { %>
// Add meta infos (used in renderer.js) // Add meta infos (used in renderer.js)
ssrContext.meta = _app.$meta() ssrContext.meta = _app.$meta()
<% } %>
<% if (features.asyncData) { %>
// Keep asyncData for each matched component in ssrContext (used in app/utils.js via this.$ssrContext) // Keep asyncData for each matched component in ssrContext (used in app/utils.js via this.$ssrContext)
ssrContext.asyncData = {} ssrContext.asyncData = {}
<% } %>
const beforeRender = async () => { const beforeRender = async () => {
// Call beforeNuxtRender() methods // Call beforeNuxtRender() methods
await Promise.all(ssrContext.beforeRenderFns.map(fn => promisify(fn, { Components, nuxtState: ssrContext.nuxt }))) await Promise.all(ssrContext.beforeRenderFns.map(fn => promisify(fn, { Components, nuxtState: ssrContext.nuxt })))
ssrContext.rendered = () => {
<% if (store) { %> <% if (store) { %>
ssrContext.rendered = () => {
// Add the state from the vuex store // Add the state from the vuex store
ssrContext.nuxt.state = store.state ssrContext.nuxt.state = store.state
}
<% } %> <% } %>
} }
}
const renderErrorPage = async () => { const renderErrorPage = async () => {
<% if (features.layouts) { %>
// Load layout for error page // Load layout for error page
const errLayout = (typeof NuxtError.layout === 'function' ? NuxtError.layout(app.context) : NuxtError.layout) const errLayout = (typeof NuxtError.layout === 'function' ? NuxtError.layout(app.context) : NuxtError.layout)
ssrContext.nuxt.layout = errLayout || 'default' ssrContext.nuxt.layout = errLayout || 'default'
await _app.loadLayout(errLayout) await _app.loadLayout(errLayout)
_app.setLayout(errLayout) _app.setLayout(errLayout)
<% } %>
await beforeRender() await beforeRender()
return _app return _app
} }
@ -106,6 +123,7 @@ export default async (ssrContext) => {
if (ssrContext.nuxt.error) return renderErrorPage() if (ssrContext.nuxt.error) return renderErrorPage()
<% } %> <% } %>
<% if (features.middleware) { %>
/* /*
** Call global middleware (nuxt.config.js) ** Call global middleware (nuxt.config.js)
*/ */
@ -121,7 +139,9 @@ export default async (ssrContext) => {
// ...If there is a redirect or an error, stop the process // ...If there is a redirect or an error, stop the process
if (ssrContext.redirected) return noopApp() if (ssrContext.redirected) return noopApp()
if (ssrContext.nuxt.error) return renderErrorPage() if (ssrContext.nuxt.error) return renderErrorPage()
<% } %>
<% if (features.layouts) { %>
/* /*
** Set layout ** Set layout
*/ */
@ -131,13 +151,19 @@ export default async (ssrContext) => {
if (ssrContext.nuxt.error) return renderErrorPage() if (ssrContext.nuxt.error) return renderErrorPage()
layout = _app.setLayout(layout) layout = _app.setLayout(layout)
ssrContext.nuxt.layout = _app.layoutName ssrContext.nuxt.layout = _app.layoutName
<% } %>
<% if (features.middleware) { %>
/* /*
** Call middleware (layout + pages) ** Call middleware (layout + pages)
*/ */
midd = [] midd = []
<% if (features.layouts) { %>
layout = sanitizeComponent(layout) layout = sanitizeComponent(layout)
if (layout.options.middleware) midd = midd.concat(layout.options.middleware) if (layout.options.middleware) {
midd = midd.concat(layout.options.middleware)
}
<% } %>
Components.forEach((Component) => { Components.forEach((Component) => {
if (Component.options.middleware) { if (Component.options.middleware) {
midd = midd.concat(Component.options.middleware) midd = midd.concat(Component.options.middleware)
@ -154,7 +180,9 @@ export default async (ssrContext) => {
// ...If there is a redirect or an error, stop the process // ...If there is a redirect or an error, stop the process
if (ssrContext.redirected) return noopApp() if (ssrContext.redirected) return noopApp()
if (ssrContext.nuxt.error) return renderErrorPage() if (ssrContext.nuxt.error) return renderErrorPage()
<% } %>
<% if (features.validate) { %>
/* /*
** Call .validate() ** Call .validate()
*/ */
@ -187,14 +215,17 @@ export default async (ssrContext) => {
// Render a 404 error page // Render a 404 error page
return render404Page() return render404Page()
} }
<% } %>
// If no Components found, returns 404 // If no Components found, returns 404
if (!Components.length) return render404Page() if (!Components.length) return render404Page()
<% if (features.asyncData || features.fetch) { %>
// Call asyncData & fetch hooks on components matched by the route. // Call asyncData & fetch hooks on components matched by the route.
const asyncDatas = await Promise.all(Components.map((Component) => { const asyncDatas = await Promise.all(Components.map((Component) => {
const promises = [] const promises = []
<% if (features.asyncData) { %>
// Call asyncData(context) // Call asyncData(context)
if (Component.options.asyncData && typeof Component.options.asyncData === 'function') { if (Component.options.asyncData && typeof Component.options.asyncData === 'function') {
const promise = promisify(Component.options.asyncData, app.context) const promise = promisify(Component.options.asyncData, app.context)
@ -207,13 +238,16 @@ export default async (ssrContext) => {
} else { } else {
promises.push(null) promises.push(null)
} }
<% } %>
<% if (features.fetch) { %>
// Call fetch(context) // Call fetch(context)
if (Component.options.fetch) { if (Component.options.fetch) {
promises.push(Component.options.fetch(app.context)) promises.push(Component.options.fetch(app.context))
} else { } else {
promises.push(null) promises.push(null)
} }
<% } %>
return Promise.all(promises) return Promise.all(promises)
})) }))
@ -222,6 +256,7 @@ export default async (ssrContext) => {
// datas are the first row of each // datas are the first row of each
ssrContext.nuxt.data = asyncDatas.map(r => r[0] || {}) ssrContext.nuxt.data = asyncDatas.map(r => r[0] || {})
<% } %>
// ...If there is a redirect or an error, stop the process // ...If there is a redirect or an error, stop the process
if (ssrContext.redirected) return noopApp() if (ssrContext.redirected) return noopApp()

View File

@ -21,6 +21,7 @@ export function interopDefault(promise) {
return promise.then(m => m.default || m) return promise.then(m => m.default || m)
} }
<% if (features.asyncData) { %>
export function applyAsyncData(Component, asyncData) { export function applyAsyncData(Component, asyncData) {
if ( if (
// For SSR, we once all this function without second param to just apply asyncData // For SSR, we once all this function without second param to just apply asyncData
@ -47,6 +48,7 @@ export function applyAsyncData(Component, asyncData) {
Component._Ctor.options.data = Component.options.data Component._Ctor.options.data = Component.options.data
} }
} }
<% } %>
export function sanitizeComponent(Component) { export function sanitizeComponent(Component) {
// If Component already sanitized // If Component already sanitized
@ -67,22 +69,17 @@ export function sanitizeComponent(Component) {
return Component return Component
} }
export function getMatchedComponents(route, matches = false) { export function getMatchedComponents(route, matches = false, prop = 'components') {
return Array.prototype.concat.apply([], route.matched.map((m, index) => { return Array.prototype.concat.apply([], route.matched.map((m, index) => {
return Object.keys(m.components).map((key) => { return Object.keys(m[prop]).map((key) => {
matches && matches.push(index) matches && matches.push(index)
return m.components[key] return m[prop][key]
}) })
})) }))
} }
export function getMatchedComponentsInstances(route, matches = false) { export function getMatchedComponentsInstances(route, matches = false) {
return Array.prototype.concat.apply([], route.matched.map((m, index) => { return getMatchedComponents(route, matches, 'instances')
return Object.keys(m.instances).map((key) => {
matches && matches.push(index)
return m.instances[key]
})
}))
} }
export function flatMapComponents(route, fn) { export function flatMapComponents(route, fn) {
@ -215,11 +212,11 @@ export async function setContext(app, context) {
app.context.next = context.next app.context.next = context.next
app.context._redirected = false app.context._redirected = false
app.context._errored = false app.context._errored = false
app.context.isHMR = Boolean(context.isHMR) app.context.isHMR = <% if(isDev) { %>Boolean(context.isHMR)<% } else { %>false<% } %>
app.context.params = app.context.route.params || {} app.context.params = app.context.route.params || {}
app.context.query = app.context.route.query || {} app.context.query = app.context.route.query || {}
} }
<% if (features.middleware) { %>
export function middlewareSeries(promises, appContext) { export function middlewareSeries(promises, appContext) {
if (!promises.length || appContext._redirected || appContext._errored) { if (!promises.length || appContext._redirected || appContext._errored) {
return Promise.resolve() return Promise.resolve()
@ -229,8 +226,9 @@ export function middlewareSeries(promises, appContext) {
return middlewareSeries(promises.slice(1), appContext) return middlewareSeries(promises.slice(1), appContext)
}) })
} }
<% } %>
export function promisify(fn, context) { export function promisify(fn, context) {
<% if (features.deprecations) { %>
let promise let promise
if (fn.length === 2) { if (fn.length === 2) {
<% if (isDev) { %> <% if (isDev) { %>
@ -251,11 +249,14 @@ export function promisify(fn, context) {
} else { } else {
promise = fn(context) promise = fn(context)
} }
if (!promise || (!(promise instanceof Promise) && (typeof promise.then !== 'function'))) { <% } else { %>
promise = Promise.resolve(promise) const promise = fn(context)
} <% } %>
if (promise && promise instanceof Promise && typeof promise.then === 'function') {
return promise return promise
} }
return Promise.resolve(promise)
}
// Imported from vue-router // Imported from vue-router
export function getLocation(base, mode) { export function getLocation(base, mode) {
@ -269,10 +270,6 @@ export function getLocation(base, mode) {
return (path || '/') + window.location.search + window.location.hash return (path || '/') + window.location.search + window.location.hash
} }
export function urlJoin() {
return Array.prototype.slice.call(arguments).join('/').replace(/\/+/g, '/')
}
// Imported from path-to-regexp // Imported from path-to-regexp
/** /**
@ -412,8 +409,9 @@ function parse(str, options) {
* @param {string} * @param {string}
* @return {string} * @return {string}
*/ */
function encodeURIComponentPretty(str) { function encodeURIComponentPretty(str, slashAllowed) {
return encodeURI(str).replace(/[/?#]/g, (c) => { const re = slashAllowed ? /[?#]/g : /[/?#]/g
return encodeURI(str).replace(re, (c) => {
return '%' + c.charCodeAt(0).toString(16).toUpperCase() return '%' + c.charCodeAt(0).toString(16).toUpperCase()
}) })
} }
@ -425,9 +423,27 @@ function encodeURIComponentPretty(str) {
* @return {string} * @return {string}
*/ */
function encodeAsterisk(str) { function encodeAsterisk(str) {
return encodeURI(str).replace(/[?#]/g, (c) => { return encodeURIComponentPretty(str, true)
return '%' + c.charCodeAt(0).toString(16).toUpperCase() }
})
/**
* Escape a regular expression string.
*
* @param {string} str
* @return {string}
*/
function escapeString(str) {
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
}
/**
* Escape the capturing group by escaping special characters and meaning.
*
* @param {string} group
* @return {string}
*/
function escapeGroup(group) {
return group.replace(/([=!:$/()])/g, '\\$1')
} }
/** /**
@ -514,26 +530,6 @@ function tokensToFunction(tokens) {
} }
} }
/**
* Escape a regular expression string.
*
* @param {string} str
* @return {string}
*/
function escapeString(str) {
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
}
/**
* Escape the capturing group by escaping special characters and meaning.
*
* @param {string} group
* @return {string}
*/
function escapeGroup(group) {
return group.replace(/([=!:$/()])/g, '\\$1')
}
/** /**
* Format given url, append query to url query string * Format given url, append query to url query string
* *
@ -542,6 +538,24 @@ function escapeGroup(group) {
* @return {string} * @return {string}
*/ */
function formatUrl(url, query) { function formatUrl(url, query) {
<% if (features.clientUseUrl) { %>
url = new URL(url, top.location.href)
for (const key in query) {
const value = query[key]
if (value == null) {
continue
}
if (Array.isArray(value)) {
for (const arrayValue of value) {
url.searchParams.append(key, arrayValue)
}
continue
}
url.searchParams.append(key, value)
}
url.searchParams.sort()
return url.toString()
<% } else { %>
let protocol let protocol
const index = url.indexOf('://') const index = url.indexOf('://')
if (index !== -1) { if (index !== -1) {
@ -569,8 +583,9 @@ function formatUrl(url, query) {
result += hash ? '#' + hash : '' result += hash ? '#' + hash : ''
return result return result
<% } %>
} }
<% if (!features.clientUseUrl) { %>
/** /**
* Transform data object to query string * Transform data object to query string
* *
@ -589,3 +604,4 @@ function formatQuery(query) {
return key + '=' + val return key + '=' + val
}).filter(Boolean).join('&') }).filter(Boolean).join('&')
} }
<% } %>

View File

@ -47,6 +47,7 @@ export default class SPARenderer extends BaseRenderer {
BODY_SCRIPTS: '' BODY_SCRIPTS: ''
} }
if (this.options.features.meta) {
// Get vue-meta context // Get vue-meta context
let head let head
if (typeof this.options.head === 'function') { if (typeof this.options.head === 'function') {
@ -90,6 +91,7 @@ export default class SPARenderer extends BaseRenderer {
m.style.text({ body: true }) + m.style.text({ body: true }) +
m.script.text({ body: true }) + m.script.text({ body: true }) +
m.noscript.text({ body: true }) m.noscript.text({ body: true })
}
// Resources Hints // Resources Hints
meta.resourceHints = '' meta.resourceHints = ''

View File

@ -83,15 +83,19 @@ export default class SSRRenderer extends BaseRenderer {
APP = `<div id="${this.serverContext.globals.id}"></div>` APP = `<div id="${this.serverContext.globals.id}"></div>`
} }
let HEAD = ''
// Inject head meta // Inject head meta
const m = renderContext.meta.inject() // (this is unset when features.meta is false in server template)
let HEAD = const meta = renderContext.meta && renderContext.meta.inject()
m.title.text() + if (meta) {
m.meta.text() + HEAD += meta.title.text() +
m.link.text() + meta.meta.text() +
m.style.text() + meta.link.text() +
m.script.text() + meta.style.text() +
m.noscript.text() meta.script.text() +
meta.noscript.text()
}
// Check if we need to inject scripts and state // Check if we need to inject scripts and state
const shouldInjectScripts = this.options.render.injectScripts !== false const shouldInjectScripts = this.options.render.injectScripts !== false
@ -109,16 +113,18 @@ export default class SSRRenderer extends BaseRenderer {
// Inject styles // Inject styles
HEAD += renderContext.renderStyles() HEAD += renderContext.renderStyles()
if (meta) {
const BODY_PREPEND = const BODY_PREPEND =
m.meta.text({ pbody: true }) + meta.meta.text({ pbody: true }) +
m.link.text({ pbody: true }) + meta.link.text({ pbody: true }) +
m.style.text({ pbody: true }) + meta.style.text({ pbody: true }) +
m.script.text({ pbody: true }) + meta.script.text({ pbody: true }) +
m.noscript.text({ pbody: true }) meta.noscript.text({ pbody: true })
if (BODY_PREPEND) { if (BODY_PREPEND) {
APP = `${BODY_PREPEND}${APP}` APP = `${BODY_PREPEND}${APP}`
} }
}
// Serialize state // Serialize state
const serializedSession = `window.${this.serverContext.globals.context}=${devalue(renderContext.nuxt)};` const serializedSession = `window.${this.serverContext.globals.context}=${devalue(renderContext.nuxt)};`
@ -152,18 +158,20 @@ export default class SSRRenderer extends BaseRenderer {
APP += this.renderScripts(renderContext) APP += this.renderScripts(renderContext)
} }
if (meta) {
// Append body scripts // Append body scripts
APP += m.meta.text({ body: true }) APP += meta.meta.text({ body: true })
APP += m.link.text({ body: true }) APP += meta.link.text({ body: true })
APP += m.style.text({ body: true }) APP += meta.style.text({ body: true })
APP += m.script.text({ body: true }) APP += meta.script.text({ body: true })
APP += m.noscript.text({ body: true }) APP += meta.noscript.text({ body: true })
}
// Template params // Template params
const templateParams = { const templateParams = {
HTML_ATTRS: m.htmlAttrs.text(true /* addSrrAttribute */), HTML_ATTRS: meta ? meta.htmlAttrs.text(true /* addSrrAttribute */) : '',
HEAD_ATTRS: m.headAttrs.text(), HEAD_ATTRS: meta ? meta.headAttrs.text() : '',
BODY_ATTRS: m.bodyAttrs.text(), BODY_ATTRS: meta ? meta.bodyAttrs.text() : '',
HEAD, HEAD,
APP, APP,
ENV: this.options.env ENV: this.options.env

View File

@ -1,5 +1,43 @@
export default { export default {
modern: 'server',
router: { router: {
base: '/%C3%B6/' base: '/%C3%B6/'
},
loading: false,
loadingIndicator: false,
fetch: {
client: false,
server: false
},
features: {
store: false,
layouts: false,
meta: false,
middleware: false,
transitions: false,
deprecations: false,
validate: false,
asyncData: false,
fetch: false,
clientOnline: false,
clientPrefetch: false,
clientUseUrl: true,
componentAliases: false,
componentClientOnly: false
},
build: {
indicator: false,
terser: true,
optimization: {
splitChunks: {
cacheGroups: {
nuxtApp: {
test: /[\\/]\.nuxt[\\/]/,
filename: 'vue-app.nuxt.js',
enforce: true
}
}
}
}
} }
} }

View File

@ -1,30 +1,8 @@
import { resolve } from 'path' import { resolve } from 'path'
import zlib from 'zlib' import { getResourcesSize } from '../utils'
import fs from 'fs-extra'
import pify from 'pify'
const gzip = pify(zlib.gzip)
const brotli = pify(zlib.brotliCompress)
const compressSize = (input, compressor) => compressor(input).then(data => data.length)
const distDir = resolve(__dirname, '../fixtures/async-config/.nuxt/dist') const distDir = resolve(__dirname, '../fixtures/async-config/.nuxt/dist')
const getResourcesSize = async (mode) => {
const { all } = await import(resolve(distDir, 'server', `${mode}.manifest.json`))
const resources = all.filter(filename => filename.endsWith('.js'))
const sizes = { uncompressed: 0, gzip: 0, brotli: 0 }
for (const resource of resources) {
const file = resolve(distDir, 'client', resource)
const stat = await fs.stat(file)
sizes.uncompressed += stat.size / 1024
const fileContent = await fs.readFile(file)
sizes.gzip += await compressSize(fileContent, gzip) / 1024
sizes.brotli += await compressSize(fileContent, brotli) / 1024
}
return sizes
}
describe('nuxt basic resources size limit', () => { describe('nuxt basic resources size limit', () => {
expect.extend({ expect.extend({
toBeWithinSize (received, size) { toBeWithinSize (received, size) {
@ -40,7 +18,7 @@ describe('nuxt basic resources size limit', () => {
}) })
it('should stay within the size limit range in legacy mode', async () => { it('should stay within the size limit range in legacy mode', async () => {
const legacyResourcesSize = await getResourcesSize('client') const legacyResourcesSize = await getResourcesSize(distDir, 'client', { gzip: true, brotli: true })
const LEGACY_JS_RESOURCES_KB_SIZE = 194 const LEGACY_JS_RESOURCES_KB_SIZE = 194
expect(legacyResourcesSize.uncompressed).toBeWithinSize(LEGACY_JS_RESOURCES_KB_SIZE) expect(legacyResourcesSize.uncompressed).toBeWithinSize(LEGACY_JS_RESOURCES_KB_SIZE)
@ -53,7 +31,7 @@ describe('nuxt basic resources size limit', () => {
}) })
it('should stay within the size limit range in modern mode', async () => { it('should stay within the size limit range in modern mode', async () => {
const modernResourcesSize = await getResourcesSize('modern') const modernResourcesSize = await getResourcesSize(distDir, 'modern', { gzip: true, brotli: true })
const MODERN_JS_RESOURCES_KB_SIZE = 172 const MODERN_JS_RESOURCES_KB_SIZE = 172
expect(modernResourcesSize.uncompressed).toBeWithinSize(MODERN_JS_RESOURCES_KB_SIZE) expect(modernResourcesSize.uncompressed).toBeWithinSize(MODERN_JS_RESOURCES_KB_SIZE)

View File

@ -0,0 +1,27 @@
import { resolve } from 'path'
import { getResourcesSize } from '../utils'
const distDir = resolve(__dirname, '../fixtures/unicode-base/.nuxt/dist')
describe('nuxt minimal vue-app bundle size limit', () => {
expect.extend({
toBeWithinSize (received, size) {
const maxSize = size * 1.02
const minSize = size * 0.98
const pass = received >= minSize && received <= maxSize
return {
pass,
message: () =>
`expected ${received} to be within range ${minSize} - ${maxSize}`
}
}
})
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.8
expect(legacyResourcesSize.uncompressed).toBeWithinSize(LEGACY_JS_RESOURCES_KB_SIZE)
})
})

View File

@ -1,7 +1,7 @@
import { getPort, loadFixture, Nuxt } from '../utils' import { getPort, loadFixture, Nuxt, rp } from '../utils'
let port let port
const url = route => 'http://localhost:' + port + route const url = route => 'http://localhost:' + port + encodeURI(route)
let nuxt = null let nuxt = null
@ -16,10 +16,9 @@ describe('unicode-base', () => {
}) })
test('/ö/ (router base)', async () => { test('/ö/ (router base)', async () => {
const window = await nuxt.server.renderAndGetWindow(url('/ö/')) const response = await rp(url('/ö/'))
const html = window.document.body.innerHTML expect(response).toContain('<h1>Unicode base works!</h1>')
expect(html).toContain('<h1>Unicode base works!</h1>')
}) })
// Close server and ask nuxt to stop listening to file changes // Close server and ask nuxt to stop listening to file changes

View File

@ -5,6 +5,7 @@ export { default as getPort } from 'get-port'
export { default as rp } from 'request-promise-native' export { default as rp } from 'request-promise-native'
export * from './nuxt' export * from './nuxt'
export * from './resource-size'
export const listPaths = function listPaths (dir, pathsBefore = [], options = {}) { export const listPaths = function listPaths (dir, pathsBefore = [], options = {}) {
if (Array.isArray(pathsBefore) && pathsBefore.length) { if (Array.isArray(pathsBefore) && pathsBefore.length) {

View File

@ -0,0 +1,35 @@
import { resolve } from 'path'
import zlib from 'zlib'
import fs from 'fs-extra'
import pify from 'pify'
const gzipCompressor = pify(zlib.gzip)
const brotliCompressor = pify(zlib.brotliCompress)
const compressSize = (input, compressor) => compressor(input).then(data => data.length)
export const getResourcesSize = async (distDir, mode, { filter, gzip, brotli } = {}) => {
if (!filter) {
filter = filename => filename.endsWith('.js')
}
const { all } = await import(resolve(distDir, 'server', `${mode}.manifest.json`))
const resources = all.filter(filter)
const sizes = { uncompressed: 0, gzip: 0, brotli: 0 }
for (const resource of resources) {
const file = resolve(distDir, 'client', resource)
const stat = await fs.stat(file)
sizes.uncompressed += stat.size / 1024
if (gzip || brotli) {
const fileContent = await fs.readFile(file)
if (gzip) {
sizes.gzip += await compressSize(fileContent, gzipCompressor) / 1024
}
if (brotli) {
sizes.brotli += await compressSize(fileContent, brotliCompressor) / 1024
}
}
}
return sizes
}