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

View File

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

View File

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

View File

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

View File

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

View File

@ -56,5 +56,22 @@ export default () => ({
layoutTransition: {
name: 'layout',
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",
"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 {
"client": true,
"server": true,

View File

@ -143,6 +143,22 @@ Object {
"env": Object {},
"extendPlugins": null,
"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 {
"client": true,
"server": true,
@ -474,6 +490,22 @@ Object {
"env": Object {},
"extendPlugins": null,
"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 {
"client": true,
"server": true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import Vue from 'vue'
import Meta from 'vue-meta'
import ClientOnly from 'vue-client-only'
import NoSsr from 'vue-no-ssr'
<% if (features.meta) { %>import Meta from 'vue-meta'<% } %>
<% if (features.componentClientOnly) { %>import ClientOnly from 'vue-client-only'<% } %>
<% if (features.deprecations) { %>import NoSsr from 'vue-no-ssr'<% } %>
import { createRouter } from './router.js'
import NuxtChild from './components/nuxt-child.js'
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 */' : '' %>
<% if (features.componentClientOnly) { %>
// Component: <ClientOnly>
Vue.component(ClientOnly.name, ClientOnly)
<% } %>
<% if (features.deprecations) { %>
// TODO: Remove in Nuxt 3: <NoSsr>
Vue.component(NoSsr.name, {
...NoSsr,
@ -29,16 +32,17 @@ Vue.component(NoSsr.name, {
return NoSsr.render(h, ctx)
}
})
<% } %>
// Component: <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: <Nuxt>`
Vue.component(Nuxt.name, Nuxt)
<% if (features.meta) { %>
// vue-meta configuration
Vue.use(Meta, {
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
tagIDKeyName: 'hid' // the property name that vue-meta uses to determine whether to overwrite or append a tag
})
<% } %>
<% if (features.transitions) { %>
const defaultTransition = <%=
serialize(pageTransition)
.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('appear(', 'function(').replace('afterAppear(', 'function(').replace('appearCancelled(', 'function(')
%><%= isTest ? '// eslint-disable-line' : '' %>
<% } %>
async function createApp(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,
// making them available everywhere as `this.$router` and `this.$store`.
const app = {
router,
<% if (store) { %>store,<% } %>
router,
nuxt: {
<% if (features.transitions) { %>
defaultTransition,
transitions: [ defaultTransition ],
setTransitions(transitions) {
@ -96,6 +104,7 @@ async function createApp(ssrContext) {
this.$options.nuxt.transitions = transitions
return transitions
},
<% } %>
err: null,
dateErr: null,
error(err) {
@ -128,10 +137,10 @@ async function createApp(ssrContext) {
// Set context to app.context
await setContext(app, {
<% if (store) { %>store,<% } %>
route,
next,
error: app.nuxt.error.bind(app),
<% if (store) { %>store,<% } %>
payload: ssrContext ? ssrContext.payload : undefined,
req: ssrContext ? ssrContext.req : undefined,
res: ssrContext ? ssrContext.res : undefined,
@ -213,8 +222,8 @@ async function createApp(ssrContext) {
}
return {
app,
<% if(store) { %>store,<% } %>
app,
router
}
}

View File

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

View File

@ -21,6 +21,7 @@ export function interopDefault(promise) {
return promise.then(m => m.default || m)
}
<% if (features.asyncData) { %>
export function applyAsyncData(Component, asyncData) {
if (
// 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
}
}
<% } %>
export function sanitizeComponent(Component) {
// If Component already sanitized
@ -67,22 +69,17 @@ export function sanitizeComponent(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 Object.keys(m.components).map((key) => {
return Object.keys(m[prop]).map((key) => {
matches && matches.push(index)
return m.components[key]
return m[prop][key]
})
}))
}
export function getMatchedComponentsInstances(route, matches = false) {
return Array.prototype.concat.apply([], route.matched.map((m, index) => {
return Object.keys(m.instances).map((key) => {
matches && matches.push(index)
return m.instances[key]
})
}))
return getMatchedComponents(route, matches, 'instances')
}
export function flatMapComponents(route, fn) {
@ -215,11 +212,11 @@ export async function setContext(app, context) {
app.context.next = context.next
app.context._redirected = 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.query = app.context.route.query || {}
}
<% if (features.middleware) { %>
export function middlewareSeries(promises, appContext) {
if (!promises.length || appContext._redirected || appContext._errored) {
return Promise.resolve()
@ -229,8 +226,9 @@ export function middlewareSeries(promises, appContext) {
return middlewareSeries(promises.slice(1), appContext)
})
}
<% } %>
export function promisify(fn, context) {
<% if (features.deprecations) { %>
let promise
if (fn.length === 2) {
<% if (isDev) { %>
@ -251,11 +249,14 @@ export function promisify(fn, context) {
} else {
promise = fn(context)
}
if (!promise || (!(promise instanceof Promise) && (typeof promise.then !== 'function'))) {
promise = Promise.resolve(promise)
}
<% } else { %>
const promise = fn(context)
<% } %>
if (promise && promise instanceof Promise && typeof promise.then === 'function') {
return promise
}
return Promise.resolve(promise)
}
// Imported from vue-router
export function getLocation(base, mode) {
@ -269,10 +270,6 @@ export function getLocation(base, mode) {
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
/**
@ -412,8 +409,9 @@ function parse(str, options) {
* @param {string}
* @return {string}
*/
function encodeURIComponentPretty(str) {
return encodeURI(str).replace(/[/?#]/g, (c) => {
function encodeURIComponentPretty(str, slashAllowed) {
const re = slashAllowed ? /[?#]/g : /[/?#]/g
return encodeURI(str).replace(re, (c) => {
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
})
}
@ -425,9 +423,27 @@ function encodeURIComponentPretty(str) {
* @return {string}
*/
function encodeAsterisk(str) {
return encodeURI(str).replace(/[?#]/g, (c) => {
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
})
return encodeURIComponentPretty(str, true)
}
/**
* 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
*
@ -542,6 +538,24 @@ function escapeGroup(group) {
* @return {string}
*/
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
const index = url.indexOf('://')
if (index !== -1) {
@ -569,8 +583,9 @@ function formatUrl(url, query) {
result += hash ? '#' + hash : ''
return result
<% } %>
}
<% if (!features.clientUseUrl) { %>
/**
* Transform data object to query string
*
@ -589,3 +604,4 @@ function formatQuery(query) {
return key + '=' + val
}).filter(Boolean).join('&')
}
<% } %>

View File

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

View File

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

View File

@ -1,5 +1,43 @@
export default {
modern: 'server',
router: {
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 zlib from 'zlib'
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)
import { getResourcesSize } from '../utils'
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', () => {
expect.extend({
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 () => {
const legacyResourcesSize = await getResourcesSize('client')
const legacyResourcesSize = await getResourcesSize(distDir, 'client', { gzip: true, brotli: true })
const LEGACY_JS_RESOURCES_KB_SIZE = 194
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 () => {
const modernResourcesSize = await getResourcesSize('modern')
const modernResourcesSize = await getResourcesSize(distDir, 'modern', { gzip: true, brotli: true })
const MODERN_JS_RESOURCES_KB_SIZE = 172
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
const url = route => 'http://localhost:' + port + route
const url = route => 'http://localhost:' + port + encodeURI(route)
let nuxt = null
@ -16,10 +16,9 @@ describe('unicode-base', () => {
})
test('/ö/ (router base)', async () => {
const window = await nuxt.server.renderAndGetWindow(url('/ö/'))
const response = await rp(url('/ö/'))
const html = window.document.body.innerHTML
expect(html).toContain('<h1>Unicode base works!</h1>')
expect(response).toContain('<h1>Unicode base works!</h1>')
})
// 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 * from './nuxt'
export * from './resource-size'
export const listPaths = function listPaths (dir, pathsBefore = [], options = {}) {
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
}