diff --git a/examples/minimal-features/README.md b/examples/minimal-features/README.md new file mode 100644 index 0000000000..30b16a2c3b --- /dev/null +++ b/examples/minimal-features/README.md @@ -0,0 +1,3 @@ +# A minimal Hello World Nuxt.js app + +https://nuxtjs.org/examples diff --git a/examples/minimal-features/nuxt.config.js b/examples/minimal-features/nuxt.config.js new file mode 100644 index 0000000000..24b2888229 --- /dev/null +++ b/examples/minimal-features/nuxt.config.js @@ -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 + } +} diff --git a/examples/minimal-features/package.json b/examples/minimal-features/package.json new file mode 100644 index 0000000000..b8661b7295 --- /dev/null +++ b/examples/minimal-features/package.json @@ -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" + } +} diff --git a/examples/minimal-features/pages/about.vue b/examples/minimal-features/pages/about.vue new file mode 100755 index 0000000000..0a6e9a060e --- /dev/null +++ b/examples/minimal-features/pages/about.vue @@ -0,0 +1,21 @@ + + + diff --git a/examples/minimal-features/pages/index.vue b/examples/minimal-features/pages/index.vue new file mode 100755 index 0000000000..79027bff40 --- /dev/null +++ b/examples/minimal-features/pages/index.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/builder/src/builder.js b/packages/builder/src/builder.js index 3964862df5..a25bb8c02f 100644 --- a/packages/builder/src/builder.js +++ b/packages/builder/src/builder.js @@ -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) { diff --git a/packages/builder/src/context/template.js b/packages/builder/src/context/template.js index c51e356122..fe5f04e938 100644 --- a/packages/builder/src/context/template.js +++ b/packages/builder/src/context/template.js @@ -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, diff --git a/packages/builder/test/builder.generate.test.js b/packages/builder/test/builder.generate.test.js index 9bd3ab5ad2..2d7e7b7b56 100644 --- a/packages/builder/test/builder.generate.test.js +++ b/packages/builder/test/builder.generate.test.js @@ -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([{ - name: 'subfolder/midd', - src: 'subfolder/midd.js', - dst: 'subfolder/midd.js' - }]) + 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' diff --git a/packages/builder/test/context/__snapshots__/template.test.js.snap b/packages/builder/test/context/__snapshots__/template.test.js.snap index b40a5fddbc..873cea0853 100644 --- a/packages/builder/test/context/__snapshots__/template.test.js.snap +++ b/packages/builder/test/context/__snapshots__/template.test.js.snap @@ -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 { diff --git a/packages/builder/test/context/template.test.js b/packages/builder/test/context/template.test.js index aa099be03d..982344ea3c 100644 --- a/packages/builder/test/context/template.test.js +++ b/packages/builder/test/context/template.test.js @@ -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: { diff --git a/packages/config/src/config/_app.js b/packages/config/src/config/_app.js index 12df0a4927..5d285c875d 100644 --- a/packages/config/src/config/_app.js +++ b/packages/config/src/config/_app.js @@ -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 } }) diff --git a/packages/config/test/__snapshots__/options.test.js.snap b/packages/config/test/__snapshots__/options.test.js.snap index 9f9917d0cc..5fe1eb9c02 100644 --- a/packages/config/test/__snapshots__/options.test.js.snap +++ b/packages/config/test/__snapshots__/options.test.js.snap @@ -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, diff --git a/packages/config/test/config/__snapshots__/index.test.js.snap b/packages/config/test/config/__snapshots__/index.test.js.snap index c56a3e057c..a0eacacdb0 100644 --- a/packages/config/test/config/__snapshots__/index.test.js.snap +++ b/packages/config/test/config/__snapshots__/index.test.js.snap @@ -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, diff --git a/packages/vue-app/src/index.js b/packages/vue-app/src/index.js index 3b188dbdae..93700aee9a 100644 --- a/packages/vue-app/src/index.js +++ b/packages/vue-app/src/index.js @@ -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', diff --git a/packages/vue-app/template/App.js b/packages/vue-app/template/App.js index e6544907c3..c910e44fa0 100644 --- a/packages/vue-app/template/App.js +++ b/packages/vue-app/template/App.js @@ -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'<% } %> +<% 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 } + <% } %> } diff --git a/packages/vue-app/template/client.js b/packages/vue-app/template/client.js index 8989281d4a..7cdc6b11df 100644 --- a/packages/vue-app/template/client.js +++ b/packages/vue-app/template/client.js @@ -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: 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) diff --git a/packages/vue-app/template/components/nuxt-child.js b/packages/vue-app/template/components/nuxt-child.js index 796ec17edd..bc7c6ad305 100644 --- a/packages/vue-app/template/components/nuxt-child.js +++ b/packages/vue-app/template/components/nuxt-child.js @@ -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' ] +<% } %> diff --git a/packages/vue-app/template/components/nuxt.js b/packages/vue-app/template/components/nuxt.js index 2d29bf378d..c892257744 100644 --- a/packages/vue-app/template/components/nuxt.js +++ b/packages/vue-app/template/components/nuxt.js @@ -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' : '' %> diff --git a/packages/vue-app/template/index.js b/packages/vue-app/template/index.js index aa04dfbc6f..0f4656e761 100644 --- a/packages/vue-app/template/index.js +++ b/packages/vue-app/template/index.js @@ -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: Vue.component(ClientOnly.name, ClientOnly) +<% } %> +<% if (features.deprecations) { %> // TODO: Remove in Nuxt 3: Vue.component(NoSsr.name, { ...NoSsr, @@ -29,16 +32,17 @@ Vue.component(NoSsr.name, { return NoSsr.render(h, ctx) } }) - +<% } %> // Component: 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: ` 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 } } diff --git a/packages/vue-app/template/server.js b/packages/vue-app/template/server.js index 9edda3de3d..4420577026 100644 --- a/packages/vue-app/template/server.js +++ b/packages/vue-app/template/server.js @@ -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: 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() diff --git a/packages/vue-app/template/utils.js b/packages/vue-app/template/utils.js index bffa91f029..0cfe0ee0f3 100644 --- a/packages/vue-app/template/utils.js +++ b/packages/vue-app/template/utils.js @@ -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,10 +249,13 @@ 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 + return Promise.resolve(promise) } // Imported from vue-router @@ -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('&') } +<% } %> diff --git a/packages/vue-renderer/src/renderers/spa.js b/packages/vue-renderer/src/renderers/spa.js index 55a2b1bf31..00be32def1 100644 --- a/packages/vue-renderer/src/renderers/spa.js +++ b/packages/vue-renderer/src/renderers/spa.js @@ -47,50 +47,52 @@ export default class SPARenderer extends BaseRenderer { BODY_SCRIPTS: '' } - // Get vue-meta context - let head - if (typeof this.options.head === 'function') { - head = this.options.head() - } else { - head = this.options.head + if (this.options.features.meta) { + // Get vue-meta context + let head + if (typeof this.options.head === 'function') { + head = this.options.head() + } else { + head = this.options.head + } + + const m = VueMeta.generate(head || {}, this.vueMetaConfig) + + // HTML_ATTRS + meta.HTML_ATTRS = m.htmlAttrs.text() + + // HEAD_ATTRS + meta.HEAD_ATTRS = m.headAttrs.text() + + // BODY_ATTRS + meta.BODY_ATTRS = m.bodyAttrs.text() + + // HEAD tags + meta.HEAD = + m.title.text() + + m.meta.text() + + m.link.text() + + m.style.text() + + m.script.text() + + m.noscript.text() + + // BODY_SCRIPTS (PREPEND) + meta.BODY_SCRIPTS_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 }) + + // BODY_SCRIPTS (APPEND) + meta.BODY_SCRIPTS = + m.meta.text({ body: true }) + + m.link.text({ body: true }) + + m.style.text({ body: true }) + + m.script.text({ body: true }) + + m.noscript.text({ body: true }) } - const m = VueMeta.generate(head || {}, this.vueMetaConfig) - - // HTML_ATTRS - meta.HTML_ATTRS = m.htmlAttrs.text() - - // HEAD_ATTRS - meta.HEAD_ATTRS = m.headAttrs.text() - - // BODY_ATTRS - meta.BODY_ATTRS = m.bodyAttrs.text() - - // HEAD tags - meta.HEAD = - m.title.text() + - m.meta.text() + - m.link.text() + - m.style.text() + - m.script.text() + - m.noscript.text() - - // BODY_SCRIPTS (PREPEND) - meta.BODY_SCRIPTS_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 }) - - // BODY_SCRIPTS (APPEND) - meta.BODY_SCRIPTS = - m.meta.text({ body: true }) + - m.link.text({ body: true }) + - m.style.text({ body: true }) + - m.script.text({ body: true }) + - m.noscript.text({ body: true }) - // Resources Hints meta.resourceHints = '' diff --git a/packages/vue-renderer/src/renderers/ssr.js b/packages/vue-renderer/src/renderers/ssr.js index 2891eaa219..410a9c3a31 100644 --- a/packages/vue-renderer/src/renderers/ssr.js +++ b/packages/vue-renderer/src/renderers/ssr.js @@ -83,15 +83,19 @@ export default class SSRRenderer extends BaseRenderer { APP = `
` } + 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,15 +113,17 @@ export default class SSRRenderer extends BaseRenderer { // Inject styles HEAD += renderContext.renderStyles() - 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 }) + if (meta) { + const BODY_PREPEND = + 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}` + if (BODY_PREPEND) { + APP = `${BODY_PREPEND}${APP}` + } } // Serialize state @@ -152,18 +158,20 @@ export default class SSRRenderer extends BaseRenderer { APP += this.renderScripts(renderContext) } - // 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 }) + if (meta) { + // Append body scripts + 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 diff --git a/test/fixtures/unicode-base/nuxt.config.js b/test/fixtures/unicode-base/nuxt.config.js index 00456d9717..53f0a271b9 100644 --- a/test/fixtures/unicode-base/nuxt.config.js +++ b/test/fixtures/unicode-base/nuxt.config.js @@ -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 + } + } + } + } } } diff --git a/test/unit/async-config.size-limit.test.js b/test/unit/async-config.size-limit.test.js index 87d709c231..39ee749b63 100644 --- a/test/unit/async-config.size-limit.test.js +++ b/test/unit/async-config.size-limit.test.js @@ -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) diff --git a/test/unit/unicode-base.size-limit.test.js b/test/unit/unicode-base.size-limit.test.js new file mode 100644 index 0000000000..1b9cbc12d7 --- /dev/null +++ b/test/unit/unicode-base.size-limit.test.js @@ -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) + }) +}) diff --git a/test/unit/unicode-base.test.js b/test/unit/unicode-base.test.js index bebef4ff4d..b7228efa54 100644 --- a/test/unit/unicode-base.test.js +++ b/test/unit/unicode-base.test.js @@ -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('

Unicode base works!

') + expect(response).toContain('

Unicode base works!

') }) // Close server and ask nuxt to stop listening to file changes diff --git a/test/utils/index.js b/test/utils/index.js index c96d3b061d..1b9dac0534 100644 --- a/test/utils/index.js +++ b/test/utils/index.js @@ -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) { diff --git a/test/utils/resource-size.js b/test/utils/resource-size.js new file mode 100644 index 0000000000..6101555065 --- /dev/null +++ b/test/utils/resource-size.js @@ -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 +}