diff --git a/.gitignore b/.gitignore index 306525d111..8bc7941666 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # dependencies -yarn.lock node_modules +examples/**/*/yarn.lock # logs *.log diff --git a/examples/async-data/pages/index.vue b/examples/async-data/pages/index.vue index 387a5725e5..0dd6050846 100644 --- a/examples/async-data/pages/index.vue +++ b/examples/async-data/pages/index.vue @@ -9,7 +9,7 @@ + + diff --git a/examples/dynamic-layouts/layouts/mobile.vue b/examples/dynamic-layouts/layouts/mobile.vue new file mode 100644 index 0000000000..db8735a00e --- /dev/null +++ b/examples/dynamic-layouts/layouts/mobile.vue @@ -0,0 +1,37 @@ + + + diff --git a/examples/dynamic-layouts/middleware/mobile.js b/examples/dynamic-layouts/middleware/mobile.js new file mode 100644 index 0000000000..276889f41d --- /dev/null +++ b/examples/dynamic-layouts/middleware/mobile.js @@ -0,0 +1,4 @@ +export default function (ctx) { + let userAgent = ctx.req ? ctx.req.headers['user-agent'] : navigator.userAgent + ctx.isMobile = /mobile/i.test(userAgent) +} diff --git a/examples/dynamic-layouts/nuxt.config.js b/examples/dynamic-layouts/nuxt.config.js new file mode 100644 index 0000000000..ab913e20b1 --- /dev/null +++ b/examples/dynamic-layouts/nuxt.config.js @@ -0,0 +1,10 @@ +module.exports = { + head: { + meta: [ + { content: 'width=device-width,initial-scale=1', name: 'viewport' } + ] + }, + router: { + middleware: ['mobile'] + } +} diff --git a/examples/dynamic-layouts/package.json b/examples/dynamic-layouts/package.json new file mode 100644 index 0000000000..cd97f88ca2 --- /dev/null +++ b/examples/dynamic-layouts/package.json @@ -0,0 +1,11 @@ +{ + "name": "nuxt-dynamic-layouts", + "dependencies": { + "nuxt": "latest" + }, + "scripts": { + "dev": "nuxt", + "build": "nuxt build", + "start": "nuxt start" + } +} diff --git a/examples/dynamic-layouts/pages/about.vue b/examples/dynamic-layouts/pages/about.vue new file mode 100644 index 0000000000..8ed01c381f --- /dev/null +++ b/examples/dynamic-layouts/pages/about.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/dynamic-layouts/pages/index.vue b/examples/dynamic-layouts/pages/index.vue new file mode 100644 index 0000000000..4095e47a00 --- /dev/null +++ b/examples/dynamic-layouts/pages/index.vue @@ -0,0 +1,12 @@ + + + diff --git a/examples/dynamic-layouts/static/logo.png b/examples/dynamic-layouts/static/logo.png new file mode 100644 index 0000000000..7f238b598c Binary files /dev/null and b/examples/dynamic-layouts/static/logo.png differ diff --git a/examples/head-elements/components/twitter-head-card.vue b/examples/head-elements/components/twitter-head-card.vue index 54391ee10b..5b73e765c0 100644 --- a/examples/head-elements/components/twitter-head-card.vue +++ b/examples/head-elements/components/twitter-head-card.vue @@ -20,6 +20,3 @@ export default { } } - - diff --git a/examples/hello-world-jsx/package.json b/examples/hello-world-jsx/package.json new file mode 100644 index 0000000000..067dc72f52 --- /dev/null +++ b/examples/hello-world-jsx/package.json @@ -0,0 +1,11 @@ +{ + "name": "hello-nuxt-jsx", + "dependencies": { + "nuxt": "latest" + }, + "scripts": { + "dev": "nuxt", + "build": "nuxt build", + "start": "nuxt" + } +} diff --git a/examples/hello-world-jsx/pages/about.vue b/examples/hello-world-jsx/pages/about.vue new file mode 100644 index 0000000000..7f7b2af070 --- /dev/null +++ b/examples/hello-world-jsx/pages/about.vue @@ -0,0 +1,15 @@ + diff --git a/examples/hello-world-jsx/pages/index.vue b/examples/hello-world-jsx/pages/index.vue new file mode 100644 index 0000000000..2786fa37ed --- /dev/null +++ b/examples/hello-world-jsx/pages/index.vue @@ -0,0 +1,10 @@ + diff --git a/examples/hello-world/pages/about.vue b/examples/hello-world/pages/about.vue index 7ed044bcba..becdb2f20a 100755 --- a/examples/hello-world/pages/about.vue +++ b/examples/hello-world/pages/about.vue @@ -7,7 +7,7 @@ ` const html = self.appTemplate({ - dev: self.dev, // Use to add the extracted CSS in production - baseUrl: self.options.router.base, - APP: app, - context: context, - files: { - css: urlJoin(self.options.router.base, '/_nuxt/', self.options.build.filenames.css), - vendor: urlJoin(self.options.router.base, '/_nuxt/', self.options.build.filenames.vendor), - app: urlJoin(self.options.router.base, '/_nuxt/', self.options.build.filenames.app) - } + HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(), + BODY_ATTRS: m.bodyAttrs.text(), + HEAD, + APP }) return { html, @@ -128,10 +149,9 @@ export function renderAndGetWindow (url, opts = {}) { window.scrollTo = function () {} // If Nuxt could not be loaded (error from the server-side) if (!window.__NUXT__) { - return reject({ - message: 'Could not load the nuxt app', - body: window.document.getElementsByTagName('body')[0].innerHTML - }) + let error = new Error('Could not load the nuxt app') + error.body = window.document.getElementsByTagName('body')[0].innerHTML + return reject(error) } // Used by nuxt.js to say when the components are loaded and the app ready window.onNuxtReady(() => { diff --git a/lib/server.js b/lib/server.js index 1b9efae6d7..9f93754a21 100644 --- a/lib/server.js +++ b/lib/server.js @@ -3,7 +3,6 @@ const http = require('http') class Server { - constructor (nuxt) { this.nuxt = nuxt this.server = http.createServer(this.render.bind(this)) @@ -27,7 +26,6 @@ class Server { close (cb) { return this.server.close(cb) } - } export default Server diff --git a/lib/utils.js b/lib/utils.js index aac0823475..aedd6e9d55 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -28,15 +28,19 @@ export function * waitFor (ms) { } export function urlJoin () { - return [].slice.call(arguments).join('/').replace(/\/+/g, '/') + return [].slice.call(arguments).join('/').replace(/\/+/g, '/').replace(':/', '://') } -export function promisifyRouteParams (fn) { - // If routeParams[route] is an array +export function isUrl (url) { + return (url.indexOf('http') === 0 || url.indexOf('//') === 0) +} + +export function promisifyRoute (fn) { + // If routes is an array if (Array.isArray(fn)) { return Promise.resolve(fn) } - // If routeParams[route] is a function expecting a callback + // If routes is a function expecting a callback if (fn.length === 1) { return new Promise((resolve, reject) => { fn(function (err, routeParams) { diff --git a/lib/views/app.html b/lib/views/app.html deleted file mode 100644 index fcf4f15ede..0000000000 --- a/lib/views/app.html +++ /dev/null @@ -1,19 +0,0 @@ -<% var m = context.meta.inject() %> -> - - <%= m.meta.text() %> - <%= m.title.text() %> - <%= m.link.text() %> - <%= m.style.text() %> - <%= m.script.text() %> - <%= m.noscript.text() %> - <% if (baseUrl !== '/') { %><% } %> - <% if (!dev) { %><% } %> - - > - <%= APP %> - - - - - diff --git a/lib/views/app.template.html b/lib/views/app.template.html new file mode 100644 index 0000000000..3ef8d3b0fc --- /dev/null +++ b/lib/views/app.template.html @@ -0,0 +1,9 @@ + + + + {{ HEAD }} + + + {{ APP }} + + diff --git a/lib/views/error.html b/lib/views/error.html index e752bece5f..dd2f8d2d71 100644 --- a/lib/views/error.html +++ b/lib/views/error.html @@ -2,10 +2,10 @@ - Vue.js error + Nuxt.js error -

Vue.js error

-
<%= ansiHTML(encodeHtml(err.stack)) %>
+

Nuxt.js error

+
{{ stack }}
diff --git a/lib/webpack/base.config.js b/lib/webpack/base.config.js index 017f7e99ae..88e68ce6f5 100644 --- a/lib/webpack/base.config.js +++ b/lib/webpack/base.config.js @@ -3,7 +3,7 @@ import vueLoaderConfig from './vue-loader.config' import { defaults } from 'lodash' import { join } from 'path' -import { urlJoin } from '../utils' +import { isUrl, urlJoin } from '../utils' /* |-------------------------------------------------------------------------- @@ -16,14 +16,16 @@ import { urlJoin } from '../utils' export default function ({ isClient, isServer }) { const nodeModulesDir = join(__dirname, '..', 'node_modules') let config = { - devtool: 'source-map', + devtool: (this.dev ? 'cheap-module-eval-source-map' : false), entry: { vendor: ['vue', 'vue-router', 'vue-meta'] }, output: { - publicPath: urlJoin(this.options.router.base, '/_nuxt/') + publicPath: (isUrl(this.options.build.publicPath) ? this.options.build.publicPath : urlJoin(this.options.router.base, this.options.build.publicPath)) }, performance: { + maxEntrypointSize: 300000, + maxAssetSize: 300000, hints: (this.dev ? false : 'warning') }, resolve: { @@ -64,16 +66,13 @@ export default function ({ isClient, isServer }) { loader: 'babel-loader', exclude: /node_modules/, query: defaults(this.options.build.babel, { - plugins: [ - 'transform-async-to-generator', - 'transform-runtime' - ], - presets: [ - ['es2015', { modules: false }], - 'stage-2' - ], + presets: ['vue-app'], cacheDirectory: !!this.dev }) + }, + { + test: /\.css$/, + loader: 'vue-style-loader!css-loader' } ] }, diff --git a/lib/webpack/client.config.js b/lib/webpack/client.config.js index 8cee34438e..7e40a22d29 100644 --- a/lib/webpack/client.config.js +++ b/lib/webpack/client.config.js @@ -2,7 +2,10 @@ import { each } from 'lodash' import webpack from 'webpack' -import ExtractTextPlugin from 'extract-text-webpack-plugin' +import HTMLPlugin from 'html-webpack-plugin' +import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin' +import ScriptExtHtmlWebpackPlugin from 'script-ext-html-webpack-plugin' +import PreloadWebpackPlugin from 'preload-webpack-plugin' import ProgressBarPlugin from 'progress-bar-webpack-plugin' import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' import base from './base.config.js' @@ -40,38 +43,55 @@ export default function () { }) // Webpack plugins config.plugins = (config.plugins || []).concat([ - // strip comments in Vue code + // Strip comments in Vue code new webpack.DefinePlugin(Object.assign(env, { 'process.env.NODE_ENV': JSON.stringify(this.dev ? 'development' : 'production'), 'process.BROWSER_BUILD': true, - 'process.SERVER_BUILD': false + 'process.SERVER_BUILD': false, + 'process.browser': true, + 'process.server': true })), // Extract vendor chunks for better caching new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: this.options.build.filenames.vendor + }), + // Generate output HTML + new HTMLPlugin({ + template: this.options.appTemplatePath + }), + // Add defer to scripts + new ScriptExtHtmlWebpackPlugin({ + defaultAttribute: 'defer' }) ]) + if (!this.dev && this.options.performance.prefetch === true) { + // Add prefetch code-splitted routes + config.plugins.push( + new PreloadWebpackPlugin({ + rel: 'prefetch' + }) + ) + } // client bundle progress bar config.plugins.push( new ProgressBarPlugin() ) - + // Add friendly error plugin + if (this.dev) { + config.plugins.push(new FriendlyErrorsWebpackPlugin()) + } // Production client build if (!this.dev) { config.plugins.push( - // Use ExtractTextPlugin to extract CSS into a single file - new ExtractTextPlugin({ - filename: this.options.build.filenames.css, - allChunks: true - }), // This is needed in webpack 2 for minifying CSS new webpack.LoaderOptionsPlugin({ minimize: true }), // Minify JS new webpack.optimize.UglifyJsPlugin({ + sourceMap: true, compress: { warnings: false } diff --git a/lib/webpack/server.config.js b/lib/webpack/server.config.js index e8f1323f67..3a811d3cf2 100644 --- a/lib/webpack/server.config.js +++ b/lib/webpack/server.config.js @@ -1,6 +1,7 @@ 'use strict' import webpack from 'webpack' +import VueSSRPlugin from 'vue-ssr-webpack-plugin' import base from './base.config.js' import { each, uniq } from 'lodash' import { existsSync, readFileSync } from 'fs' @@ -22,7 +23,7 @@ export default function () { config = Object.assign(config, { target: 'node', - devtool: false, + devtool: 'source-map', entry: resolve(this.dir, '.nuxt', 'server.js'), output: Object.assign({}, config.output, { path: resolve(this.dir, '.nuxt', 'dist'), @@ -30,14 +31,26 @@ export default function () { libraryTarget: 'commonjs2' }), plugins: (config.plugins || []).concat([ + new VueSSRPlugin({ + filename: 'server-bundle.json' + }), new webpack.DefinePlugin(Object.assign(env, { 'process.env.NODE_ENV': JSON.stringify(this.dev ? 'development' : 'production'), - 'process.BROWSER_BUILD': false, - 'process.SERVER_BUILD': true + 'process.BROWSER_BUILD': false, // deprecated + 'process.SERVER_BUILD': true, // deprecated + 'process.browser': false, + 'process.server': true })) ]) }) - + // This is needed in webpack 2 for minifying CSS + if (!this.dev) { + config.plugins.push( + new webpack.LoaderOptionsPlugin({ + minimize: true + }) + ) + } // Externals const nuxtPackageJson = require('../../package.json') const projectPackageJsonPath = resolve(this.dir, 'package.json') @@ -48,6 +61,7 @@ export default function () { config.externals = config.externals.concat(Object.keys(projectPackageJson.dependencies || {})) } catch (e) {} } + config.externals = config.externals.concat(this.options.build.vendor) config.externals = uniq(config.externals) // Extend config diff --git a/lib/webpack/vue-loader.config.js b/lib/webpack/vue-loader.config.js index b045d1a29c..7b431505ac 100644 --- a/lib/webpack/vue-loader.config.js +++ b/lib/webpack/vue-loader.config.js @@ -4,19 +4,14 @@ import { defaults } from 'lodash' export default function ({ isClient }) { let babelOptions = JSON.stringify(defaults(this.options.build.babel, { - plugins: [ - 'transform-async-to-generator', - 'transform-runtime' - ], - presets: [ - ['es2015', { modules: false }], - 'stage-2' - ] + presets: ['vue-app'], + cacheDirectory: !!this.dev })) let config = { postcss: this.options.build.postcss, loaders: { 'js': 'babel-loader?' + babelOptions, + 'css': 'vue-style-loader!css-loader', 'less': 'vue-style-loader!css-loader!less-loader', 'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax', 'scss': 'vue-style-loader!css-loader!sass-loader', @@ -25,17 +20,6 @@ export default function ({ isClient }) { }, preserveWhitespace: false } - - if (!this.dev && isClient) { - // Use ExtractTextPlugin to extract CSS into a single file - const ExtractTextPlugin = require('extract-text-webpack-plugin') - config.loaders.css = ExtractTextPlugin.extract({ loader: 'css-loader' }) - config.loaders.scss = ExtractTextPlugin.extract({ loader: 'css-loader!sass-loader', fallbackLoader: 'vue-style-loader' }) - config.loaders.sass = ExtractTextPlugin.extract({ loader: 'css-loader!sass-loader?indentedSyntax', fallbackLoader: 'vue-style-loader' }) - config.loaders.stylus = ExtractTextPlugin.extract({ loader: 'css-loader!stylus-loader', fallbackLoader: 'vue-style-loader' }) - config.loaders.less = ExtractTextPlugin.extract({ loader: 'css-loader!less-loader', fallbackLoader: 'vue-style-loader' }) - } - // Return the config return config } diff --git a/package.json b/package.json index fb55baa258..1dcd8a050a 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "nuxt": "./bin/nuxt" }, "scripts": { - "test": "nyc ava --serial test/", + "test": "nyc ava --verbose --serial test/", "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov", "lint": "eslint --ext .js,.vue bin lib pages test/*.js --ignore-pattern lib/app", "build": "webpack", @@ -52,63 +52,68 @@ }, "dependencies": { "ansi-html": "^0.0.7", - "autoprefixer": "^6.7.2", - "babel-core": "^6.22.1", - "babel-loader": "^6.2.10", - "babel-plugin-array-includes": "^2.0.3", - "babel-plugin-transform-async-to-generator": "^6.22.0", - "babel-plugin-transform-runtime": "^6.22.0", - "babel-preset-es2015": "^6.22.0", - "babel-preset-stage-2": "^6.22.0", - "chalk": "^1.1.3", + "autoprefixer": "^6.7.7", + "babel-core": "^6.24.0", + "babel-loader": "^6.4.1", + "babel-preset-vue-app": "^0.5.1", "chokidar": "^1.6.1", "co": "^4.6.0", - "css-loader": "^0.26.1", - "debug": "^2.6.1", - "extract-text-webpack-plugin": "2.0.0-beta.4", - "file-loader": "^0.10.0", - "fs-extra": "^2.0.0", + "compression": "^1.6.2", + "css-loader": "^0.27.3", + "debug": "^2.6.3", + "file-loader": "^0.10.1", + "friendly-errors-webpack-plugin": "^1.6.1", + "fs-extra": "^2.1.2", "glob": "^7.1.1", "hash-sum": "^1.0.2", - "html-minifier": "^3.3.1", + "html-minifier": "^3.4.2", + "html-webpack-plugin": "^2.28.0", "lodash": "^4.17.4", "lru-cache": "^4.0.2", "memory-fs": "^0.4.1", - "path-to-regexp": "^1.7.0", "pify": "^2.3.0", - "post-compile-webpack-plugin": "^0.1.1", + "preload-webpack-plugin": "^1.2.1", "progress-bar-webpack-plugin": "^1.9.3", + "script-ext-html-webpack-plugin": "^1.7.1", "serialize-javascript": "^1.3.0", - "serve-static": "^1.11.2", - "url-loader": "^0.5.7", - "vue": "^2.1.10", - "vue-loader": "^10.3.0", - "vue-meta": "^0.5.3", - "vue-router": "^2.2.0", - "vue-server-renderer": "^2.1.10", + "serve-static": "^1.12.1", + "url-loader": "^0.5.8", + "vue": "^2.2.5", + "vue-loader": "^11.3.3", + "vue-meta": "^0.5.5", + "vue-router": "^2.3.0", + "vue-server-renderer": "^2.2.5", "vue-ssr-html-stream": "^2.2.0", - "vue-template-compiler": "^2.1.10", - "vuex": "^2.1.2", - "webpack": "^2.2.1", - "webpack-bundle-analyzer": "^2.2.3", - "webpack-dev-middleware": "^1.10.0", - "webpack-hot-middleware": "^2.16.1" + "vue-ssr-webpack-plugin": "^1.0.2", + "vue-template-compiler": "^2.2.5", + "vuex": "^2.2.1", + "webpack": "^2.3.1", + "webpack-bundle-analyzer": "^2.3.1", + "webpack-dev-middleware": "^1.10.1", + "webpack-hot-middleware": "^2.17.1" }, "devDependencies": { - "ava": "^0.18.1", - "babel-eslint": "^7.1.1", - "codecov": "^1.0.1", + "ava": "^0.18.2", + "babel-eslint": "^7.2.1", + "babel-plugin-array-includes": "^2.0.3", + "babel-plugin-transform-async-to-generator": "^6.22.0", + "babel-plugin-transform-runtime": "^6.23.0", + "babel-preset-es2015": "^6.24.0", + "babel-preset-stage-2": "^6.22.0", + "codecov": "^2.1.0", "copy-webpack-plugin": "^4.0.1", - "eslint": "^3.15.0", - "eslint-config-standard": "^6.2.1", - "eslint-plugin-html": "^2.0.0", - "eslint-plugin-promise": "^3.4.1", - "eslint-plugin-standard": "^2.0.1", - "finalhandler": "^0.5.1", - "jsdom": "^9.10.0", + "eslint": "^3.18.0", + "eslint-config-standard": "^8.0.0-beta.2", + "eslint-plugin-html": "^2.0.1", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-node": "^4.2.1", + "eslint-plugin-promise": "^3.5.0", + "eslint-plugin-standard": "^2.1.1", + "finalhandler": "^1.0.1", + "jsdom": "^9.12.0", "json-loader": "^0.5.4", - "nyc": "^10.1.2", - "request": "^2.79.0", + "nyc": "^10.2.0-candidate.0", + "request": "^2.81.0", "request-promise-native": "^1.0.3", "webpack-node-externals": "^1.5.4" } diff --git a/test/basic.fail.generate.test.js b/test/basic.fail.generate.test.js index 034a6f3365..bdb3552f98 100644 --- a/test/basic.fail.generate.test.js +++ b/test/basic.fail.generate.test.js @@ -1,42 +1,16 @@ import test from 'ava' import { resolve } from 'path' -test('Fail to generate without routeParams', t => { - const Nuxt = require('../') - const options = { - rootDir: resolve(__dirname, 'fixtures/basic'), - dev: false - // no generate.routeParams - } - const nuxt = new Nuxt(options) - return new Promise((resolve) => { - var oldExit = process.exit - var oldCE = console.error // eslint-disable-line no-console - var _log = '' - console.error = (s) => { _log += s } // eslint-disable-line no-console - process.exit = (code) => { - process.exit = oldExit - console.error = oldCE // eslint-disable-line no-console - t.is(code, 1) - t.true(_log.includes('Could not generate the dynamic route /users/:id')) - resolve() - } - nuxt.generate() - }) -}) - -test('Fail with routeParams which throw an error', t => { +test('Fail with routes() which throw an error', t => { const Nuxt = require('../') const options = { rootDir: resolve(__dirname, 'fixtures/basic'), dev: false, generate: { - routeParams: { - '/users/:id': function () { - return new Promise((resolve, reject) => { - reject('Not today!') - }) - } + routes: function () { + return new Promise((resolve, reject) => { + reject(new Error('Not today!')) + }) } } } @@ -50,12 +24,12 @@ test('Fail with routeParams which throw an error', t => { process.exit = oldExit console.error = oldCE // eslint-disable-line no-console t.is(code, 1) - t.true(_log.includes('Could not resolve routeParams[/users/:id]')) + t.true(_log.includes('Could not resolve routes')) resolve() } nuxt.generate() .catch((e) => { - t.true(e === 'Not today!') + t.true(e.message === 'Not today!') }) }) }) diff --git a/test/fixtures/basic/nuxt.config.js b/test/fixtures/basic/nuxt.config.js index 1793846e1f..19c0433cab 100644 --- a/test/fixtures/basic/nuxt.config.js +++ b/test/fixtures/basic/nuxt.config.js @@ -1,11 +1,9 @@ module.exports = { generate: { - routeParams: { - '/users/:id': [ - { id: 1 }, - { id: 2 }, - { id: 3 } - ] - } + routes: [ + '/users/1', + '/users/2', + '/users/3' + ] } } diff --git a/test/fixtures/basic/pages/async-data.vue b/test/fixtures/basic/pages/async-data.vue index 6eec4d9466..f999fb6871 100755 --- a/test/fixtures/basic/pages/async-data.vue +++ b/test/fixtures/basic/pages/async-data.vue @@ -4,7 +4,7 @@