Merge branch '0.10.0'

This commit is contained in:
Sébastien Chopin 2017-03-24 19:00:48 +01:00
commit d1550f914b
73 changed files with 6896 additions and 521 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
# dependencies # dependencies
yarn.lock
node_modules node_modules
examples/**/*/yarn.lock
# logs # logs
*.log *.log

View File

@ -9,7 +9,7 @@
<script> <script>
export default { export default {
data ({ req }, callback) { asyncData ({ req }, callback) {
setTimeout(function () { setTimeout(function () {
// callback(err, data) // callback(err, data)
callback(null, { callback(null, {

View File

@ -11,7 +11,7 @@
import axios from 'axios' import axios from 'axios'
export default { export default {
async data ({ params }) { async asyncData ({ params }) {
// We can use async/await ES6 feature // We can use async/await ES6 feature
let { data } = await axios.get(`https://jsonplaceholder.typicode.com/posts/${params.id}`) let { data } = await axios.get(`https://jsonplaceholder.typicode.com/posts/${params.id}`)
return { post: data } return { post: data }

View File

@ -15,7 +15,7 @@
import axios from 'axios' import axios from 'axios'
export default { export default {
data ({ req, params }) { asyncData ({ req, params }) {
// We can return a Promise instead of calling the callback // We can return a Promise instead of calling the callback
return axios.get('https://jsonplaceholder.typicode.com/posts') return axios.get('https://jsonplaceholder.typicode.com/posts')
.then((res) => { .then((res) => {

View File

@ -8,7 +8,7 @@
<script> <script>
export default { export default {
layout: 'dark', layout: 'dark',
data ({ req }) { asyncData ({ req }) {
return { return {
name: req ? 'server' : 'client' name: req ? 'server' : 'client'
} }

View File

@ -7,7 +7,7 @@
<script> <script>
export default { export default {
data () { asyncData () {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(function () { setTimeout(function () {
resolve({}) resolve({})

View File

@ -7,7 +7,7 @@
<script> <script>
export default { export default {
data () { asyncData () {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(function () { setTimeout(function () {
resolve({ name: 'world' }) resolve({ name: 'world' })

View File

@ -13,7 +13,7 @@
import axios from 'axios' import axios from 'axios'
export default { export default {
data () { asyncData () {
return axios.get('https://jsonplaceholder.typicode.com/users') return axios.get('https://jsonplaceholder.typicode.com/users')
.then((res) => { .then((res) => {
return { users: res.data } return { users: res.data }

View File

@ -14,7 +14,7 @@ export default {
validate ({ params }) { validate ({ params }) {
return !isNaN(+params.id) return !isNaN(+params.id)
}, },
data ({ params, error }) { asyncData ({ params, error }) {
return axios.get(`https://jsonplaceholder.typicode.com/users/${+params.id}`) return axios.get(`https://jsonplaceholder.typicode.com/users/${+params.id}`)
.then((res) => res.data) .then((res) => res.data)
.catch(() => { .catch(() => {

View File

@ -0,0 +1,3 @@
# Dynamic Layouts
https://nuxtjs.org/examples/layouts

View File

@ -0,0 +1,25 @@
<template>
<div class="container">
<h1 v-if="error.statusCode === 404">Page not found</h1>
<h1 v-else>An error occured</h1>
<nuxt-link to="/">Home page</nuxt-link>
</div>
</template>
<script>
export default {
layout: ({ isMobile }) => isMobile ? 'mobile' : 'default',
props: ['error']
}
</script>
<style scoped>
.container {
font-family: sans-serif;
padding-top: 10%;
text-align: center;
}
h1 {
font-size: 20px;
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<div class="dark">
<div class="mobile-banner">Mobile website</div>
<nuxt/>
</div>
</template>
<style>
* {
box-sizing: border-box;
}
.dark {
position: absolute;
top: 0;
left: 0;
width: 100%;
min-height: 100%;
background: black;
color: white;
padding: 10px;
padding-top: 40px;
}
.dark a {
color: white;
}
.mobile-banner {
position: absolute;
top: 0;
left: 0;
width: 100%;
text-align: center;
padding: 10px;
background: #333;
font-family: sans-serif;
font-size: 13px;
}
</style>

View File

@ -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)
}

View File

@ -0,0 +1,10 @@
module.exports = {
head: {
meta: [
{ content: 'width=device-width,initial-scale=1', name: 'viewport' }
]
},
router: {
middleware: ['mobile']
}
}

View File

@ -0,0 +1,11 @@
{
"name": "nuxt-dynamic-layouts",
"dependencies": {
"nuxt": "latest"
},
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start"
}
}

View File

@ -0,0 +1,17 @@
<template>
<div>
<p>Hi from {{ name }}</p>
<nuxt-link to="/">Home page</nuxt-link>
</div>
</template>
<script>
export default {
layout: ({ isMobile }) => isMobile ? 'mobile' : 'default',
asyncData ({ req }) {
return {
name: req ? 'server' : 'client'
}
}
}
</script>

View File

@ -0,0 +1,12 @@
<template>
<div class="container">
<h1>Welcome!</h1>
<nuxt-link to="/about">About page</nuxt-link>
</div>
</template>
<script>
export default {
layout: ({ isMobile }) => isMobile ? 'mobile' : 'default',
}
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -20,6 +20,3 @@ export default {
} }
} }
</script> </script>
<style lang="css">
</style>

View File

@ -0,0 +1,11 @@
{
"name": "hello-nuxt-jsx",
"dependencies": {
"nuxt": "latest"
},
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt"
}
}

View File

@ -0,0 +1,15 @@
<script>
export default {
asyncData ({ req }) {
return {
name: req ? 'server' : 'client'
}
},
render (h) {
return <div>
<p>Hi from {this.name}</p>
<nuxt-link to="/">Home page</nuxt-link>
</div>
}
}
</script>

View File

@ -0,0 +1,10 @@
<script>
export default {
render (h) {
return <div>
<h1>Welcome!</h1>
<nuxt-link to="/about">About page</nuxt-link>
</div>
}
}
</script>

View File

@ -7,7 +7,7 @@
<script> <script>
export default { export default {
data ({ req }) { asyncData ({ req }) {
return { return {
name: req ? 'server' : 'client' name: req ? 'server' : 'client'
} }

View File

@ -11,7 +11,7 @@
<script> <script>
export default { export default {
data ({ store, route, userAgent }) { asyncData ({ store, route, userAgent }) {
return { return {
userAgent, userAgent,
slugs: [ slugs: [

View File

@ -16,7 +16,7 @@
<script> <script>
export default { export default {
data ({ env }) { asyncData ({ env }) {
return { users: env.users } return { users: env.users }
} }
} }

View File

@ -10,7 +10,7 @@ export default {
validate ({ params }) { validate ({ params }) {
return !isNaN(+params.id) return !isNaN(+params.id)
}, },
data ({ params, env, error }) { asyncData ({ params, env, error }) {
const user = env.users.find((user) => String(user.id) === params.id) const user = env.users.find((user) => String(user.id) === params.id)
if (!user) { if (!user) {
return error({ message: 'User not found', statusCode: 404 }) return error({ message: 'User not found', statusCode: 404 })

View File

@ -3,6 +3,7 @@ module.exports = {
vendor: ['axios', 'mini-toastr', 'vue-notifications'] vendor: ['axios', 'mini-toastr', 'vue-notifications']
}, },
plugins: [ plugins: [
'~plugins/vue-notifications.js' // ssr: false to only include it on client-side
{ src: '~plugins/vue-notifications.js', ssr: false }
] ]
} }

View File

@ -9,7 +9,7 @@
import axios from 'axios' import axios from 'axios'
export default { export default {
data () { asyncData () {
return axios.get('https://jsonplaceholder.typicode.com/photos/4').then(res => res.data) return axios.get('https://jsonplaceholder.typicode.com/photos/4').then(res => res.data)
} }
} }

View File

@ -7,7 +7,7 @@
<script> <script>
let miniToastr let miniToastr
if (process.BROWSER_BUILD) { if (process.browser) {
miniToastr = require('mini-toastr') miniToastr = require('mini-toastr')
} }

View File

@ -1,26 +1,23 @@
// This code will be injected before initializing the root App // This code will be injected before initializing the root App
import Vue from 'vue' import Vue from 'vue'
import VueNotifications from 'vue-notifications' import VueNotifications from 'vue-notifications'
// Include mini-toaster (or any other UI-notification library
import miniToastr from 'mini-toastr'
if (process.BROWSER_BUILD) { // Here we setup messages output to `mini-toastr`
// Include mini-toaster (or any other UI-notification library const toast = function ({ title, message, type, timeout, cb }) {
const miniToastr = require('mini-toastr')
// Here we setup messages output to `mini-toastr`
const toast = function ({ title, message, type, timeout, cb }) {
return miniToastr[type](message, title, timeout, cb) return miniToastr[type](message, title, timeout, cb)
} }
// Binding for methods .success(), .error() and etc. You can specify and map your own methods here. // Binding for methods .success(), .error() and etc. You can specify and map your own methods here.
// Required to pipe our outout to UI library (mini-toastr in example here) // Required to pipe our outout to UI library (mini-toastr in example here)
// All not-specified events (types) would be piped to output in console. // All not-specified events (types) would be piped to output in console.
const options = { const options = {
success: toast, success: toast,
error: toast, error: toast,
info: toast, info: toast,
warn: toast warn: toast
}
// Activate plugin
Vue.use(VueNotifications, options)
} }
// Activate plugin
Vue.use(VueNotifications, options)

View File

@ -23,7 +23,7 @@ export default {
if (!from) return 'slide-left' if (!from) return 'slide-left'
return +to.query.page < +from.query.page ? 'slide-right' : 'slide-left' return +to.query.page < +from.query.page ? 'slide-right' : 'slide-left'
}, },
data ({ query }) { asyncData ({ query }) {
const page = +query.page || 1 const page = +query.page || 1
return axios.get('https://reqres.in/api/users?page=' + page) return axios.get('https://reqres.in/api/users?page=' + page)
.then((res) => { .then((res) => {

View File

@ -9,7 +9,7 @@
<script> <script>
export default { export default {
data ({ req }) { asyncData ({ req }) {
return { return {
name: req ? 'server' : 'client' name: req ? 'server' : 'client'
} }

View File

@ -7,7 +7,7 @@
<script> <script>
export default { export default {
data ({ req, isServer }) { asyncData ({ req, isServer }) {
return { return {
name: req ? 'server' : 'client' name: req ? 'server' : 'client'
} }

View File

@ -17,7 +17,7 @@
import socket from '~plugins/socket.io.js' import socket from '~plugins/socket.io.js'
export default { export default {
data (context, callback) { asyncData (context, callback) {
socket.emit('last-messages', function (messages) { socket.emit('last-messages', function (messages) {
callback(null, { callback(null, {
messages, messages,

View File

@ -4,6 +4,9 @@
** @Atinux & @alexchopin ** @Atinux & @alexchopin
*/ */
// Until babel-loader 7 is released
process.noDeprecation = true
var Nuxt = require('./dist/nuxt.js') var Nuxt = require('./dist/nuxt.js')
module.exports = Nuxt.default ? Nuxt.default : Nuxt module.exports = Nuxt.default ? Nuxt.default : Nuxt

View File

@ -12,7 +12,7 @@ let layouts = {
<% <%
var layoutsKeys = Object.keys(layouts); var layoutsKeys = Object.keys(layouts);
layoutsKeys.forEach(function (key, i) { %> layoutsKeys.forEach(function (key, i) { %>
"_<%= key %>": process.BROWSER_BUILD ? () => System.import('<%= layouts[key] %>') : require('<%= layouts[key] %>')<%= (i + 1) < layoutsKeys.length ? ',' : '' %> "_<%= key %>": () => import('<%= layouts[key] %>')<%= (i + 1) < layoutsKeys.length ? ',' : '' %>
<% }) %> <% }) %>
} }
@ -33,18 +33,19 @@ export default {
if (!layout || !layouts['_' + layout]) layout = 'default' if (!layout || !layouts['_' + layout]) layout = 'default'
this.layoutName = layout this.layoutName = layout
let _layout = '_' + layout let _layout = '_' + layout
if (typeof layouts[_layout] === 'function') {
return this.loadLayout(_layout)
}
this.layout = layouts[_layout] this.layout = layouts[_layout]
return Promise.resolve(this.layout) return this.layout
}, },
loadLayout (_layout) { loadLayout (layout) {
if (!layout || !layouts['_' + layout]) layout = 'default'
let _layout = '_' + layout
if (typeof layouts[_layout] !== 'function') {
return Promise.resolve(layouts[_layout])
}
return layouts[_layout]() return layouts[_layout]()
.then((Component) => { .then((Component) => {
layouts[_layout] = Component layouts[_layout] = Component
this.layout = layouts[_layout] return layouts[_layout]
return this.layout
}) })
.catch((e) => { .catch((e) => {
if (this.$nuxt) { if (this.$nuxt) {

View File

@ -2,7 +2,7 @@
import Vue from 'vue' import Vue from 'vue'
import middleware from './middleware' import middleware from './middleware'
import { app, router<%= (store ? ', store' : '') %> } from './index' import { app, router<%= (store ? ', store' : '') %>, NuxtError } from './index'
import { getMatchedComponents, getMatchedComponentsInstances, flatMapComponents, getContext, promiseSeries, promisify, getLocation, compile } from './utils' import { getMatchedComponents, getMatchedComponentsInstances, flatMapComponents, getContext, promiseSeries, promisify, getLocation, compile } from './utils'
const noopData = () => { return {} } const noopData = () => { return {} }
const noopFetch = () => {} const noopFetch = () => {}
@ -55,8 +55,11 @@ function loadAsyncComponents (to, from, next) {
} }
function callMiddleware (Components, context, layout) { function callMiddleware (Components, context, layout) {
// Call middleware // if layout is undefined, only call global middleware
let midd = <%= serialize(router.middleware, { isJSON: true }) %> let midd = <%= serialize(router.middleware, { isJSON: true }) %>
let unknownMiddleware = false
if (typeof layout !== 'undefined') {
midd = [] // exclude global middleware if layout defined (already called before)
if (layout.middleware) { if (layout.middleware) {
midd = midd.concat(layout.middleware) midd = midd.concat(layout.middleware)
} }
@ -65,13 +68,15 @@ function callMiddleware (Components, context, layout) {
midd = midd.concat(Component.options.middleware) midd = midd.concat(Component.options.middleware)
} }
}) })
}
midd = midd.map((name) => { midd = midd.map((name) => {
if (typeof middleware[name] !== 'function') { if (typeof middleware[name] !== 'function') {
unknownMiddleware = true
this.error({ statusCode: 500, message: 'Unknown middleware ' + name }) this.error({ statusCode: 500, message: 'Unknown middleware ' + name })
} }
return middleware[name] return middleware[name]
}) })
if (this.$options._nuxt.err) return if (unknownMiddleware) return
return promiseSeries(midd, context) return promiseSeries(midd, context)
} }
@ -82,13 +87,15 @@ function render (to, from, next) {
nextCalled = true nextCalled = true
next(path) next(path)
} }
const context = getContext({ to<%= (store ? ', store' : '') %>, isClient: true, next: _next.bind(this), error: this.error.bind(this) }) let context = getContext({ to<%= (store ? ', store' : '') %>, isClient: true, next: _next.bind(this), error: this.error.bind(this) })
let Components = getMatchedComponents(to) let Components = getMatchedComponents(to)
this._context = context
this._dateLastError = this.$options._nuxt.dateErr this._dateLastError = this.$options._nuxt.dateErr
this._hadError = !!this.$options._nuxt.err this._hadError = !!this.$options._nuxt.err
if (!Components.length) { if (!Components.length) {
// Default layout // Default layout
this.setLayout() callMiddleware.call(this, Components, context)
.then(() => this.loadLayout(typeof NuxtError.layout === 'function' ? NuxtError.layout(context) : NuxtError.layout))
.then(callMiddleware.bind(this, Components, context)) .then(callMiddleware.bind(this, Components, context))
.then(() => { .then(() => {
this.error({ statusCode: 404, message: 'This page could not be found.' }) this.error({ statusCode: 404, message: 'This page could not be found.' })
@ -98,26 +105,22 @@ function render (to, from, next) {
} }
// Update ._data and other properties if hot reloaded // Update ._data and other properties if hot reloaded
Components.forEach(function (Component) { Components.forEach(function (Component) {
if (!Component._data) {
Component._data = Component.options.data || noopData
}
if (Component._Ctor && Component._Ctor.options) { if (Component._Ctor && Component._Ctor.options) {
Component.options.asyncData = Component._Ctor.options.asyncData
Component.options.fetch = Component._Ctor.options.fetch Component.options.fetch = Component._Ctor.options.fetch
if (Component._dataFn) {
const originalDataFn = Component._data.toString().replace(/\s/g, '')
const dataFn = Component._dataFn
const newDataFn = (Component._Ctor.options.data || noopData).toString().replace(/\s/g, '')
// If component data method changed
if (newDataFn !== originalDataFn && newDataFn !== dataFn) {
Component._data = Component._Ctor.options.data || noopData
}
}
} }
}) })
this.setTransitions(mapTransitions(Components, to, from)) this.setTransitions(mapTransitions(Components, to, from))
let nextCalled = false let nextCalled = false
// Set layout // Set layout
this.setLayout(Components[0].options.layout) callMiddleware.call(this, Components, context)
.then(() => {
let layout = Components[0].options.layout
if (typeof layout === 'function') {
layout = layout(context)
}
return this.loadLayout(layout)
})
.then(callMiddleware.bind(this, Components, context)) .then(callMiddleware.bind(this, Components, context))
.then(() => { .then(() => {
// Pass validation? // Pass validation?
@ -141,13 +144,19 @@ function render (to, from, next) {
return Promise.resolve() return Promise.resolve()
} }
let promises = [] let promises = []
// Validate method // asyncData method
if (Component._data && typeof Component._data === 'function') { if (Component.options.asyncData && typeof Component.options.asyncData === 'function') {
var promise = promisify(Component._data, context) var promise = promisify(Component.options.asyncData, context)
promise.then((data) => { promise.then((asyncDataResult) => {
Component._cData = () => data || {} let data = {}
Component.options.data = Component._cData // Call data() if defined
Component._dataFn = Component.options.data.toString().replace(/\s/g, '') if (Component.options.data && typeof Component.options.data === 'function') {
data = Component.options.data()
}
// Merge data() and asyncData() results
data = Object.assign(data, asyncDataResult)
// Overwrite .data() method with merged data
Component.options.data = () => data
if (Component._Ctor && Component._Ctor.options) { if (Component._Ctor && Component._Ctor.options) {
Component._Ctor.options.data = Component.options.data Component._Ctor.options.data = Component.options.data
} }
@ -175,9 +184,16 @@ function render (to, from, next) {
.catch((error) => { .catch((error) => {
_lastPaths = [] _lastPaths = []
error.statusCode = error.statusCode || error.status || (error.response && error.response.status) || 500 error.statusCode = error.statusCode || error.status || (error.response && error.response.status) || 500
let layout = NuxtError.layout
if (typeof layout === 'function') {
layout = layout(context)
}
this.loadLayout(layout)
.then(() => {
this.error(error) this.error(error)
next(false) next(false)
}) })
})
} }
// Fix components format in matched, it's due to code-splitting of vue-router // Fix components format in matched, it's due to code-splitting of vue-router
@ -213,73 +229,104 @@ function fixPrepatch (to, ___) {
if (this._hadError && this._dateLastError === this.$options._nuxt.dateErr) { if (this._hadError && this._dateLastError === this.$options._nuxt.dateErr) {
this.error() this.error()
} }
// Set layout
let layout = this.$options._nuxt.err ? NuxtError.layout : to.matched[0].components.default.options.layout
if (typeof layout === 'function') {
layout = layout(this._context)
}
this.setLayout(layout)
// hot reloading // hot reloading
hotReloadAPI(this) setTimeout(() => hotReloadAPI(this), 100)
}) })
} }
// Special hot reload with data(context) // Special hot reload with asyncData(context)
function hotReloadAPI (_app) { function hotReloadAPI (_app) {
if (!module.hot) return if (!module.hot) return
const $nuxt = _app.$nuxt let $components = []
var _forceUpdate = $nuxt.$forceUpdate.bind($nuxt) let $nuxt = _app.$nuxt
$nuxt.$forceUpdate = function () { while ($nuxt && $nuxt.$children && $nuxt.$children.length) {
let Component = getMatchedComponents(router.currentRoute)[0] $nuxt.$children.forEach(function (child, i) {
if (child.$vnode.data.nuxtChild) {
let hasAlready = false
$components.forEach(function (component) {
if (component.$options.__file === child.$options.__file) {
hasAlready = true
}
})
if (!hasAlready) {
$components.push(child)
}
}
$nuxt = child
})
}
$components.forEach(addHotReload.bind(_app))
}
function addHotReload ($component, depth) {
if ($component.$vnode.data._hasHotReload) return
$component.$vnode.data._hasHotReload = true
var _forceUpdate = $component.$forceUpdate.bind($component.$parent)
$component.$vnode.context.$forceUpdate = () => {
let Components = getMatchedComponents(router.currentRoute)
let Component = Components[depth]
if (!Component) return _forceUpdate() if (!Component) return _forceUpdate()
if (typeof Component === 'object' && !Component.options) { if (typeof Component === 'object' && !Component.options) {
// Updated via vue-router resolveAsyncComponents() // Updated via vue-router resolveAsyncComponents()
Component = Vue.extend(Component) Component = Vue.extend(Component)
Component._Ctor = Component Component._Ctor = Component
} }
this.error()
let promises = [] let promises = []
// If layout changed
if (_app.layoutName !== Component.options.layout) {
let promise = _app.setLayout(Component.options.layout)
promise.then(() => {
hotReloadAPI(_app)
})
promises.push(promise)
}
const next = function (path) { const next = function (path) {
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %> <%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
router.push(path) router.push(path)
} }
const context = getContext({ route: router.currentRoute<%= (store ? ', store' : '') %>, isClient: true, next: next.bind(this), error: _app.error }) let context = getContext({ route: router.currentRoute<%= (store ? ', store' : '') %>, isClient: true, next: next.bind(this), error: this.error })
// Check if data has been updated <%= (loading ? 'this.$loading.start && this.$loading.start()' : '') %>
const originalDataFn = (Component._data || noopData).toString().replace(/\s/g, '') callMiddleware.call(this, Components, context)
const newDataFn = (Component._Ctor.options.data || noopData).toString().replace(/\s/g, '') .then(() => {
if (originalDataFn !== newDataFn) { // If layout changed
Component._data = Component._Ctor.options.data || noopData if (depth !== 0) return Promise.resolve()
let p = promisify(Component._data, context) let layout = Component.options.layout || 'default'
p.then((data) => { if (typeof layout === 'function') {
Component._cData = () => data || {} layout = layout(context)
Component.options.data = Component._cData }
Component._dataFn = Component.options.data.toString().replace(/\s/g, '') if (this.layoutName === layout) return Promise.resolve()
let promise = this.loadLayout(layout)
promise.then(() => {
this.setLayout(layout)
Vue.nextTick(() => hotReloadAPI(this))
})
return promise
})
.then(() => {
return callMiddleware.call(this, Components, context, this.layout)
})
.then(() => {
// Call asyncData()
let pAsyncData = promisify(Component.options.asyncData || noopData, context)
pAsyncData.then((asyncDataResult) => {
let data = (typeof Component.options.data === 'function' ? Component.options.data() : noopData())
data = Object.assign(data, asyncDataResult)
Component.options.data = () => data
Component._Ctor.options.data = Component.options.data Component._Ctor.options.data = Component.options.data
<%= (loading ? 'this.$loading.increase && this.$loading.increase(30)' : '') %> <%= (loading ? 'this.$loading.increase && this.$loading.increase(30)' : '') %>
}) })
promises.push(p) promises.push(pAsyncData)
} else if (Component._cData) { // Call fetch()
Component._data = Component.options.data Component.options.fetch = Component.options.fetch || noopFetch
Component.options.data = Component._cData let pFetch = Component.options.fetch(context)
Component._Ctor.options.data = Component.options.data if (!(pFetch instanceof Promise)) { pFetch = Promise.resolve(pFetch) }
} <%= (loading ? 'pFetch.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %>
// Check if fetch has been updated promises.push(pFetch)
const originalFetchFn = (Component.options.fetch || noopFetch).toString().replace(/\s/g, '') return Promise.all(promises)
const newFetchFn = (Component._Ctor.options.fetch || noopFetch).toString().replace(/\s/g, '') })
// Fetch has been updated, we call it to update the store .then(() => {
if (originalFetchFn !== newFetchFn) {
Component.options.fetch = Component._Ctor.options.fetch || noopFetch
let p = Component.options.fetch(context)
if (!(p instanceof Promise)) { p = Promise.resolve(p) }
<%= (loading ? 'p.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %>
promises.push(p)
}
if (!promises.length) return;
<%= (loading ? 'this.$loading.start && this.$loading.start()' : '') %>
return Promise.all(promises).then(() => {
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %> <%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
_forceUpdate() _forceUpdate()
setTimeout(() => hotReloadAPI(this), 100)
}) })
} }
} }
@ -302,13 +349,14 @@ const resolveComponents = flatMapComponents(router.match(path), (Component, _, m
Component._Ctor = Component Component._Ctor = Component
Component.extendOptions = Component.options Component.extendOptions = Component.options
} }
if (Component.options.data && typeof Component.options.data === 'function') {
Component._data = Component.options.data
if (NUXT.serverRendered) { if (NUXT.serverRendered) {
Component._cData = () => NUXT.data[index] || {} let data = {}
Component.options.data = Component._cData if (Component.options.data && typeof Component.options.data === 'function') {
Component._dataFn = Component.options.data.toString().replace(/\s/g, '') data = Component.options.data()
} }
// Merge data() and asyncData() results
data = Object.assign(data, NUXT.data[index])
Component.options.data = () => data
if (Component._Ctor && Component._Ctor.options) { if (Component._Ctor && Component._Ctor.options) {
Component._Ctor.options.data = Component.options.data Component._Ctor.options.data = Component.options.data
} }
@ -338,25 +386,31 @@ Promise.all(resolveComponents)
.then((Components) => { .then((Components) => {
const _app = new Vue(app) const _app = new Vue(app)
return _app.setLayout(Components.length ? Components[0].options.layout : '') let context = getContext({ to: router.currentRoute, isClient: true })
let layoutName = 'default'
return callMiddleware.call(_app, Components, context)
.then(() => { .then(() => {
layoutName = Components.length ? Components[0].options.layout : NuxtError.layout
if (typeof layoutName === 'function') {
layoutName = layoutName(context)
}
return _app.loadLayout(layoutName)
})
.then(() => {
_app.setLayout(layoutName)
return { _app, Components } return { _app, Components }
}) })
}) })
.then(({ _app, Components }) => { .then(({ _app, Components }) => {
const mountApp = () => { const mountApp = () => {
_app.$mount('#__nuxt') _app.$mount('#__nuxt')
Vue.nextTick(() => {
// Hot reloading // Hot reloading
hotReloadAPI(_app) hotReloadAPI(_app)
// Call window.onNuxtReady callbacks // Call window.onNuxtReady callbacks
Vue.nextTick(() => nuxtReady(_app)) nuxtReady(_app)
})
} }
<% if (store) { %>
// Replace store state
if (NUXT.state) {
store.replaceState(NUXT.state)
}
<% } %>
_app.setTransitions = _app.$options._nuxt.setTransitions.bind(_app) _app.setTransitions = _app.$options._nuxt.setTransitions.bind(_app)
if (Components.length) { if (Components.length) {
_app.setTransitions(mapTransitions(Components, router.currentRoute)) _app.setTransitions(mapTransitions(Components, router.currentRoute))

View File

@ -5,6 +5,7 @@ const transitionsKeys = [
'mode', 'mode',
'css', 'css',
'type', 'type',
'duration',
'enterClass', 'enterClass',
'leaveClass', 'leaveClass',
'enterActiveClass', 'enterActiveClass',

View File

@ -6,7 +6,7 @@
<script> <script>
import Vue from 'vue' import Vue from 'vue'
import NuxtChild from './nuxt-child' import NuxtChild from './nuxt-child'
import NuxtError from '<%= components.ErrorPage %>' import NuxtError from '<%= components.ErrorPage ? components.ErrorPage : "./nuxt-error.vue" %>'
export default { export default {
name: 'nuxt', name: 'nuxt',

View File

@ -6,6 +6,7 @@ import router from './router.js'
<% if (store) { %>import store from './store.js'<% } %> <% if (store) { %>import store from './store.js'<% } %>
import NuxtChild from './components/nuxt-child.js' import NuxtChild from './components/nuxt-child.js'
import NuxtLink from './components/nuxt-link.js' import NuxtLink from './components/nuxt-link.js'
import NuxtError from '<%= components.ErrorPage ? components.ErrorPage : "./components/nuxt-error.vue" %>'
import Nuxt from './components/nuxt.vue' import Nuxt from './components/nuxt.vue'
import App from '<%= appPath %>' import App from '<%= appPath %>'
@ -19,12 +20,18 @@ Vue.component(Nuxt.name, Nuxt)
// vue-meta configuration // vue-meta configuration
Vue.use(Meta, { Vue.use(Meta, {
keyName: 'head', // the component option name that vue-meta looks for meta info on. keyName: 'head', // the component option name that vue-meta looks for meta info on.
attribute: 'n-head', // the attribute name vue-meta adds to the tags it observes attribute: 'data-n-head', // the attribute name vue-meta adds to the tags it observes
ssrAttribute: 'n-head-ssr', // the attribute name that lets vue-meta know that meta info has already been server-rendered ssrAttribute: 'data-n-head-ssr', // the attribute name that lets vue-meta know that meta info has already been server-rendered
tagIDKeyName: 'hid' // the property name that vue-meta uses to determine whether to overwrite or append a tag tagIDKeyName: 'hid' // the property name that vue-meta uses to determine whether to overwrite or append a tag
}) })
if (process.BROWSER_BUILD) { if (process.browser) {
<% if (store) { %>
// Replace store state before calling plugins
if (window.__NUXT__ && window.__NUXT__.state) {
store.replaceState(window.__NUXT__.state)
}
<% } %>
// window.onNuxtReady(() => console.log('Ready')) hook // window.onNuxtReady(() => console.log('Ready')) hook
// Useful for jsdom testing or plugins (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading) // Useful for jsdom testing or plugins (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading)
window._nuxtReadyCbs = [] window._nuxtReadyCbs = []
@ -34,8 +41,16 @@ if (process.BROWSER_BUILD) {
} }
// Includes external plugins // Includes external plugins
<% plugins.forEach(function (pluginPath) { %> <% plugins.forEach(function (plugin) {
require('<%= pluginPath %>') if (typeof plugin === 'string') { %>
require('<%= plugin %>')
<% } else if (plugin.src && plugin.ssr !== false) { %>
require('<%= plugin.src %>')
<% } else if (plugin.src) { %>
if (process.browser) {
require('<%= plugin.src %>')
}
<% } %>
<% }) %> <% }) %>
// root instance // root instance
@ -85,4 +100,4 @@ const app = {
...App ...App
} }
export { app, router<%= (store ? ', store' : '') %> } export { app, router<%= (store ? ', store' : '') %>, NuxtError }

View File

@ -23,7 +23,7 @@ function recursiveRoutes(routes, tab, components) {
var _components = [] var _components = []
var _routes = recursiveRoutes(router.routes, '\t\t', _components) var _routes = recursiveRoutes(router.routes, '\t\t', _components)
uniqBy(_components, '_name').forEach((route) => { %> uniqBy(_components, '_name').forEach((route) => { %>
const <%= route._name %> = process.BROWSER_BUILD ? () => System.import('<%= route.component %>') : require('<%= route.component %>') const <%= route._name %> = () => import('<%= route.component %>')
<% }) %> <% }) %>
<% if (router.scrollBehavior) { %> <% if (router.scrollBehavior) { %>

View File

@ -3,10 +3,11 @@
const debug = require('debug')('nuxt:render') const debug = require('debug')('nuxt:render')
debug.color = 4 // force blue color debug.color = 4 // force blue color
import Vue from 'vue' import Vue from 'vue'
import { stringify } from 'querystring' import { stringify } from 'querystring'
import { omit } from 'lodash' import { omit } from 'lodash'
import middleware from './middleware' import middleware from './middleware'
import { app, router<%= (store ? ', store' : '') %> } from './index' import { app, router<%= (store ? ', store' : '') %>, NuxtError } from './index'
import { getMatchedComponents, getContext, promiseSeries, promisify, urlJoin } from './utils' import { getMatchedComponents, getContext, promiseSeries, promisify, urlJoin } from './utils'
const isDev = <%= isDev %> const isDev = <%= isDev %>
@ -25,9 +26,9 @@ export default context => {
// create context.next for simulate next() of beforeEach() when wanted to redirect // create context.next for simulate next() of beforeEach() when wanted to redirect
context.redirected = false context.redirected = false
context.next = function (opts) { context.next = function (opts) {
context.redirected = opts
// if nuxt generate // if nuxt generate
if (!context.res) { if (!context.res) {
context.redirected = opts
context.nuxt.serverRendered = false context.nuxt.serverRendered = false
return return
} }
@ -39,18 +40,45 @@ export default context => {
}) })
context.res.end() context.res.end()
} }
// set router's location
router.push(context.url)
// Add route to the context
context.route = router.currentRoute
// Add meta infos // Add meta infos
context.meta = _app.$meta() context.meta = _app.$meta()
// Error function // Error function
context.error = _app.$options._nuxt.error.bind(_app) context.error = _app.$options._nuxt.error.bind(_app)
<%= (isDev ? 'const s = isDev && Date.now()' : '') %> <%= (isDev ? 'const s = isDev && Date.now()' : '') %>
const ctx = getContext(context) let ctx = null
let Components = getMatchedComponents(context.route) let componentsLoaded = false
let Components = []
let promises = getMatchedComponents(router.match(context.url)).map((Component) => {
return new Promise((resolve, reject) => {
const _resolve = (Component) => {
if (!Component.options) {
Component = Vue.extend(Component) // fix issue #6
Component._Ctor = Component
} else {
Component._Ctor = Component
Component.extendOptions = Component.options
}
resolve(Component)
}
Component().then(_resolve).catch(reject)
})
})
return Promise.all(promises)
.then((_Components) => {
componentsLoaded = true
Components = _Components
// set router's location
return new Promise((resolve) => {
router.push(context.url, resolve)
})
})
.then(() => {
// Add route to the context
context.route = router.currentRoute
// Update context
ctx = getContext(context)
// nuxtServerInit
<% if (store) { %> <% if (store) { %>
let promise = (store._actions && store._actions.nuxtServerInit ? store.dispatch('nuxtServerInit', omit(getContext(context), 'redirect', 'error')) : null) let promise = (store._actions && store._actions.nuxtServerInit ? store.dispatch('nuxtServerInit', omit(getContext(context), 'redirect', 'error')) : null)
if (!(promise instanceof Promise)) promise = Promise.resolve() if (!(promise instanceof Promise)) promise = Promise.resolve()
@ -58,6 +86,7 @@ export default context => {
let promise = Promise.resolve() let promise = Promise.resolve()
<% } %> <% } %>
return promise return promise
})
.then(() => { .then(() => {
// Sanitize Components // Sanitize Components
Components = Components.map((Component) => { Components = Components.map((Component) => {
@ -71,12 +100,28 @@ export default context => {
} }
return Component return Component
}) })
// Call global middleware (nuxt.config.js)
let midd = <%= serialize(router.middleware, { isJSON: true }) %>
midd = midd.map((name) => {
if (typeof middleware[name] !== 'function') {
context.nuxt.error = context.error({ statusCode: 500, message: 'Unknown middleware ' + name })
}
return middleware[name]
})
if (context.nuxt.error) return
return promiseSeries(midd, ctx)
})
.then(() => {
// Set layout // Set layout
return _app.setLayout(Components.length ? Components[0].options.layout : '') let layout = Components.length ? Components[0].options.layout : NuxtError.layout
if (typeof layout === 'function') {
layout = layout(ctx)
}
return _app.loadLayout(layout).then(() => _app.setLayout(layout))
}) })
.then((layout) => { .then((layout) => {
// Call middleware // Call middleware (layout + pages)
let midd = <%= serialize(router.middleware, { isJSON: true }) %> let midd = []
if (layout.middleware) { if (layout.middleware) {
midd = midd.concat(layout.middleware) midd = midd.concat(layout.middleware)
} }
@ -114,13 +159,20 @@ export default context => {
Components = [] Components = []
return _app return _app
} }
// Call data & fetch hooks on components matched by the route. // Call asyncData & fetch hooks on components matched by the route.
return Promise.all(Components.map((Component) => { return Promise.all(Components.map((Component) => {
let promises = [] let promises = []
if (Component.options.asyncData && typeof Component.options.asyncData === 'function') {
let promise = promisify(Component.options.asyncData, ctx)
// Call asyncData(context)
promise.then((asyncDataResult) => {
let data = {}
// Call data() if defined
if (Component.options.data && typeof Component.options.data === 'function') { if (Component.options.data && typeof Component.options.data === 'function') {
Component._data = Component.options.data data = Component.options.data()
let promise = promisify(Component._data, ctx) }
promise.then((data) => { // Merge data() and asyncData() results
data = Object.assign(data, asyncDataResult)
Component.options.data = () => data Component.options.data = () => data
Component._Ctor.options.data = Component.options.data Component._Ctor.options.data = Component.options.data
}) })
@ -130,6 +182,8 @@ export default context => {
} }
if (Component.options.fetch) { if (Component.options.fetch) {
promises.push(Component.options.fetch(ctx)) promises.push(Component.options.fetch(ctx))
} else {
promises.push(null)
} }
return Promise.all(promises) return Promise.all(promises)
})) }))
@ -141,16 +195,19 @@ export default context => {
return _app return _app
} }
<% if (isDev) { %> <% if (isDev) { %>
debug('Data fetching ' + context.req.url + ': ' + (Date.now() - s) + 'ms') debug('Data fetching ' + context.url + ': ' + (Date.now() - s) + 'ms')
<% } %> <% } %>
// datas are the first row of each // datas are the first row of each
context.nuxt.data = res.map((tab) => tab[0]) context.nuxt.data = res.map((r) => (r[0] || {}))
context.nuxt.error = _app.$options._nuxt.err context.nuxt.error = _app.$options._nuxt.err
<%= (store ? '// Add the state from the vuex store' : '') %> <%= (store ? '// Add the state from the vuex store' : '') %>
<%= (store ? 'context.nuxt.state = store.state' : '') %> <%= (store ? 'context.nuxt.state = store.state' : '') %>
return _app return _app
}) })
.catch(function (error) { .catch(function (error) {
if (!componentsLoaded && error instanceof Error) {
return Promise.reject(error)
}
if (error && (error instanceof Error || error.constructor.toString().indexOf('Error()') !== -1)) { if (error && (error instanceof Error || error.constructor.toString().indexOf('Error()') !== -1)) {
let statusCode = error.statusCode || error.status || (error.response && error.response.status) || 500 let statusCode = error.statusCode || error.status || (error.response && error.response.status) || 500
error = { statusCode, message: error.message } error = { statusCode, message: error.message }

View File

@ -1,6 +1,5 @@
'use strict' 'use strict'
const debug = require('debug')('nuxt:build')
import _ from 'lodash' import _ from 'lodash'
import co from 'co' import co from 'co'
import chokidar from 'chokidar' import chokidar from 'chokidar'
@ -11,10 +10,10 @@ import webpack from 'webpack'
import serialize from 'serialize-javascript' import serialize from 'serialize-javascript'
import { createBundleRenderer } from 'vue-server-renderer' import { createBundleRenderer } from 'vue-server-renderer'
import { join, resolve, sep } from 'path' import { join, resolve, sep } from 'path'
import { isUrl } from './utils'
import clientWebpackConfig from './webpack/client.config.js' import clientWebpackConfig from './webpack/client.config.js'
import serverWebpackConfig from './webpack/server.config.js' import serverWebpackConfig from './webpack/server.config.js'
import chalk from 'chalk' const debug = require('debug')('nuxt:build')
import PostCompilePlugin from 'post-compile-webpack-plugin'
const remove = pify(fs.remove) const remove = pify(fs.remove)
const readFile = pify(fs.readFile) const readFile = pify(fs.readFile)
const writeFile = pify(fs.writeFile) const writeFile = pify(fs.writeFile)
@ -38,21 +37,16 @@ const r = function () {
args = args.map(normalize) args = args.map(normalize)
return wp(resolve.apply(null, args)) return wp(resolve.apply(null, args))
} }
const webpackStats = { let webpackStats = 'none'
chunks: false,
children: false,
modules: false,
colors: true
}
// force green color // force green color
debug.color = 2 debug.color = 2
const defaults = { const defaults = {
analyze: false, analyze: false,
publicPath: '/_nuxt/',
filenames: { filenames: {
css: 'style.css', vendor: 'vendor.bundle.[hash].js',
vendor: 'vendor.bundle.js', app: 'nuxt.bundle.[chunkhash].js'
app: 'nuxt.bundle.js'
}, },
vendor: [], vendor: [],
loaders: [], loaders: [],
@ -90,13 +84,24 @@ export function options () {
if (this.options.build && !Array.isArray(this.options.build.loaders)) extraDefaults.loaders = defaultsLoaders if (this.options.build && !Array.isArray(this.options.build.loaders)) extraDefaults.loaders = defaultsLoaders
if (this.options.build && !Array.isArray(this.options.build.postcss)) extraDefaults.postcss = defaultsPostcss if (this.options.build && !Array.isArray(this.options.build.postcss)) extraDefaults.postcss = defaultsPostcss
this.options.build = _.defaultsDeep(this.options.build, defaults, extraDefaults) this.options.build = _.defaultsDeep(this.options.build, defaults, extraDefaults)
/* istanbul ignore if */
if (this.dev && isUrl(this.options.build.publicPath)) {
this.options.build.publicPath = defaults.publicPath
}
// Production, create server-renderer // Production, create server-renderer
if (!this.dev) { if (!this.dev) {
webpackStats = {
chunks: false,
children: false,
modules: false,
colors: true
}
const serverConfig = getWebpackServerConfig.call(this) const serverConfig = getWebpackServerConfig.call(this)
const bundlePath = join(serverConfig.output.path, serverConfig.output.filename) const bundlePath = join(serverConfig.output.path, 'server-bundle.json')
if (fs.existsSync(bundlePath)) { if (fs.existsSync(bundlePath)) {
const bundle = fs.readFileSync(bundlePath, 'utf8') const bundle = fs.readFileSync(bundlePath, 'utf8')
createRenderer.call(this, bundle) createRenderer.call(this, JSON.parse(bundle))
addAppTemplate.call(this)
} }
} }
} }
@ -138,6 +143,16 @@ function * buildFiles () {
webpackRunClient.call(this), webpackRunClient.call(this),
webpackRunServer.call(this) webpackRunServer.call(this)
] ]
addAppTemplate.call(this)
}
}
function addAppTemplate () {
let templatePath = resolve(this.dir, '.nuxt', 'dist', 'index.html')
if (fs.existsSync(templatePath)) {
this.appTemplate = _.template(fs.readFileSync(templatePath, 'utf8'), {
interpolate: /{{([\s\S]+?)}}/g
})
} }
} }
@ -151,17 +166,8 @@ function * generateRoutesAndFiles () {
if (name === 'error') return if (name === 'error') return
layouts[name] = r(this.srcDir, file) layouts[name] = r(this.srcDir, file)
}) })
// Generate routes based on files
const files = yield glob('pages/**/*.vue', { cwd: this.srcDir }) const files = yield glob('pages/**/*.vue', { cwd: this.srcDir })
this.routes = _.uniq(_.map(files, (file) => {
return file.replace(/^pages/, '').replace(/\.vue$/, '').replace(/\/index/g, '').replace(/_/g, ':').replace('', '/').replace(/\/{2,}/g, '/')
}))
if (typeof this.options.router.extendRoutes === 'function') {
// let the user extend the routes
this.options.router.extendRoutes(this.routes)
}
// Interpret and move template files to .nuxt/ // Interpret and move template files to .nuxt/
debug('Generating files...')
let templatesFiles = [ let templatesFiles = [
'App.vue', 'App.vue',
'client.js', 'client.js',
@ -191,21 +197,29 @@ function * generateRoutesAndFiles () {
middleware: this.options.middleware, middleware: this.options.middleware,
store: this.options.store, store: this.options.store,
css: this.options.css, css: this.options.css,
plugins: this.options.plugins.map((p) => r(this.srcDir, p)), plugins: this.options.plugins.map((p) => {
if (typeof p === 'string') {
return { src: r(this.srcDir, p), ssr: true }
}
return { src: r(this.srcDir, p.src), ssr: !!p.ssr }
}),
appPath: './App.vue', appPath: './App.vue',
layouts: layouts, layouts: layouts,
loading: (typeof this.options.loading === 'string' ? r(this.srcDir, this.options.loading) : this.options.loading), loading: (typeof this.options.loading === 'string' ? r(this.srcDir, this.options.loading) : this.options.loading),
transition: this.options.transition, transition: this.options.transition,
components: { components: {
ErrorPage: './nuxt-error.vue' ErrorPage: null
} }
} }
// Format routes for the lib/app/router.js template // Format routes for the lib/app/router.js template
templateVars.router.routes = createRoutes(files, this.srcDir) templateVars.router.routes = createRoutes(files, this.srcDir)
if (typeof this.options.router.extendRoutes === 'function') { if (typeof this.options.router.extendRoutes === 'function') {
// let the user extend the routes // let the user extend the routes
this.options.router.extendRoutes(templateVars.router.routes) this.options.router.extendRoutes(templateVars.router.routes, r)
} }
// Routes for Generate command
this.routes = flatRoutes(templateVars.router.routes)
debug('Generating files...')
if (layoutsFiles.includes('layouts/error.vue')) { if (layoutsFiles.includes('layouts/error.vue')) {
templateVars.components.ErrorPage = r(this.srcDir, 'layouts/error.vue') templateVars.components.ErrorPage = r(this.srcDir, 'layouts/error.vue')
} }
@ -325,6 +339,19 @@ function cleanChildrenRoutes (routes, isChild = false) {
return routes return routes
} }
function flatRoutes (router, path = '', routes = []) {
router.forEach((r) => {
if (!r.path.includes(':') && !r.path.includes('*')) {
if (r.children) {
flatRoutes(r.children, path + r.path + '/', routes)
} else {
routes.push((r.path === '' && path[path.length - 1] === '/' ? path.slice(0, -1) : path) + r.path)
}
}
})
return routes
}
function getWebpackClientConfig () { function getWebpackClientConfig () {
return clientWebpackConfig.call(this) return clientWebpackConfig.call(this)
} }
@ -335,27 +362,11 @@ function getWebpackServerConfig () {
function createWebpackMiddleware () { function createWebpackMiddleware () {
const clientConfig = getWebpackClientConfig.call(this) const clientConfig = getWebpackClientConfig.call(this)
const host = process.env.HOST || '127.0.0.1'
const port = process.env.PORT || '3000'
// setup on the fly compilation + hot-reload // setup on the fly compilation + hot-reload
clientConfig.entry.app = _.flatten(['webpack-hot-middleware/client?reload=true', clientConfig.entry.app]) clientConfig.entry.app = _.flatten(['webpack-hot-middleware/client?reload=true', clientConfig.entry.app])
clientConfig.plugins.push( clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(), new webpack.NoEmitOnErrorsPlugin()
new PostCompilePlugin(stats => {
process.stdout.write('\x1Bc')
if (stats.hasErrors() || stats.hasWarnings()) {
console.log(stats.toString('errors-only')) // eslint-disable-line no-console
console.log() // eslint-disable-line no-console
console.log(chalk.bgRed.black(' ERROR '), 'Compiling failed!') // eslint-disable-line no-console
} else {
console.log(stats.toString(webpackStats)) // eslint-disable-line no-console
console.log(chalk.bold(`\n> Open http://${host}:${port}\n`)) // eslint-disable-line no-console
console.log(chalk.bgGreen.black(' DONE '), 'Compiled successfully!') // eslint-disable-line no-console
}
console.log() // eslint-disable-line no-console
})
) )
const clientCompiler = webpack(clientConfig) const clientCompiler = webpack(clientConfig)
// Add the middleware to the instance context // Add the middleware to the instance context
@ -368,6 +379,16 @@ function createWebpackMiddleware () {
this.webpackHotMiddleware = pify(require('webpack-hot-middleware')(clientCompiler, { this.webpackHotMiddleware = pify(require('webpack-hot-middleware')(clientCompiler, {
log: () => {} log: () => {}
})) }))
clientCompiler.plugin('done', () => {
const fs = this.webpackDevMiddleware.fileSystem
const filePath = join(clientConfig.output.path, 'index.html')
if (fs.existsSync(filePath)) {
const template = fs.readFileSync(filePath, 'utf-8')
this.appTemplate = _.template(template, {
interpolate: /{{([\s\S]+?)}}/g
})
}
})
} }
function webpackWatchAndUpdate () { function webpackWatchAndUpdate () {
@ -375,11 +396,11 @@ function webpackWatchAndUpdate () {
const mfs = new MFS() const mfs = new MFS()
const serverConfig = getWebpackServerConfig.call(this) const serverConfig = getWebpackServerConfig.call(this)
const serverCompiler = webpack(serverConfig) const serverCompiler = webpack(serverConfig)
const outputPath = join(serverConfig.output.path, serverConfig.output.filename) const outputPath = join(serverConfig.output.path, 'server-bundle.json')
serverCompiler.outputFileSystem = mfs serverCompiler.outputFileSystem = mfs
this.webpackServerWatcher = serverCompiler.watch({}, (err) => { this.webpackServerWatcher = serverCompiler.watch({}, (err) => {
if (err) throw err if (err) throw err
createRenderer.call(this, mfs.readFileSync(outputPath, 'utf-8')) createRenderer.call(this, JSON.parse(mfs.readFileSync(outputPath, 'utf-8')))
}) })
} }
@ -390,7 +411,7 @@ function webpackRunClient () {
clientCompiler.run((err, stats) => { clientCompiler.run((err, stats) => {
if (err) return reject(err) if (err) return reject(err)
console.log('[nuxt:build:client]\n', stats.toString(webpackStats)) // eslint-disable-line no-console console.log('[nuxt:build:client]\n', stats.toString(webpackStats)) // eslint-disable-line no-console
if (stats.hasErrors()) return reject('Webpack build exited with errors') if (stats.hasErrors()) return reject(new Error('Webpack build exited with errors'))
resolve() resolve()
}) })
}) })
@ -403,11 +424,11 @@ function webpackRunServer () {
serverCompiler.run((err, stats) => { serverCompiler.run((err, stats) => {
if (err) return reject(err) if (err) return reject(err)
console.log('[nuxt:build:server]\n', stats.toString(webpackStats)) // eslint-disable-line no-console console.log('[nuxt:build:server]\n', stats.toString(webpackStats)) // eslint-disable-line no-console
if (stats.hasErrors()) return reject('Webpack build exited with errors') if (stats.hasErrors()) return reject(new Error('Webpack build exited with errors'))
const bundlePath = join(serverConfig.output.path, serverConfig.output.filename) const bundlePath = join(serverConfig.output.path, 'server-bundle.json')
readFile(bundlePath, 'utf8') readFile(bundlePath, 'utf8')
.then((bundle) => { .then((bundle) => {
createRenderer.call(this, bundle) createRenderer.call(this, JSON.parse(bundle))
resolve() resolve()
}) })
}) })

View File

@ -1,14 +1,13 @@
'use strict' 'use strict'
const debug = require('debug')('nuxt:generate')
import fs from 'fs-extra' import fs from 'fs-extra'
import co from 'co' import co from 'co'
import pify from 'pify' import pify from 'pify'
import pathToRegexp from 'path-to-regexp'
import _ from 'lodash' import _ from 'lodash'
import { resolve, join, dirname, sep } from 'path' import { resolve, join, dirname, sep } from 'path'
import { promisifyRouteParams } from './utils' import { isUrl, promisifyRoute } from './utils'
import { minify } from 'html-minifier' import { minify } from 'html-minifier'
const debug = require('debug')('nuxt:generate')
const copy = pify(fs.copy) const copy = pify(fs.copy)
const remove = pify(fs.remove) const remove = pify(fs.remove)
const writeFile = pify(fs.writeFile) const writeFile = pify(fs.writeFile)
@ -16,21 +15,32 @@ const mkdirp = pify(fs.mkdirp)
const defaults = { const defaults = {
dir: 'dist', dir: 'dist',
routeParams: {} routes: [],
minify: {
collapseBooleanAttributes: true,
collapseWhitespace: true,
decodeEntities: true,
minifyCSS: true,
minifyJS: true,
processConditionalComments: true,
removeAttributeQuotes: false,
removeComments: false,
removeEmptyAttributes: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: false,
removeStyleLinkTypeAttributes: false,
removeTagWhitespace: false,
sortAttributes: true,
sortClassName: true,
trimCustomFragments: true,
useShortDoctype: true
}
} }
export default function () { export default function () {
const s = Date.now() const s = Date.now()
/* /*
** Update loaders config to add router.base path
*/
// this.options.build.loaders.forEach((config) => {
// if (['file', 'url', 'file-loader', 'url-loader'].includes(config.loader)) {
// config.query = config.query || {}
// config.query.publicPath = urlJoin(this.options.router.base, '/_nuxt/')
// }
// })
/*
** Set variables ** Set variables
*/ */
this.options.generate = _.defaultsDeep(this.options.generate, defaults) this.options.generate = _.defaultsDeep(this.options.generate, defaults)
@ -38,7 +48,7 @@ export default function () {
var srcStaticPath = resolve(this.srcDir, 'static') var srcStaticPath = resolve(this.srcDir, 'static')
var srcBuiltPath = resolve(this.dir, '.nuxt', 'dist') var srcBuiltPath = resolve(this.dir, '.nuxt', 'dist')
var distPath = resolve(this.dir, this.options.generate.dir) var distPath = resolve(this.dir, this.options.generate.dir)
var distNuxtPath = resolve(distPath, '_nuxt') var distNuxtPath = join(distPath, (isUrl(this.options.build.publicPath) ? '_nuxt' : this.options.build.publicPath))
return co(function * () { return co(function * () {
/* /*
** Launch build process ** Launch build process
@ -61,55 +71,31 @@ export default function () {
debug('Static & build files copied') debug('Static & build files copied')
}) })
.then(() => { .then(() => {
// Resolve config.generate.routesParams promises before generating the routes // Resolve config.generate.routes promises before generating the routes
return resolveRouteParams(this.options.generate.routeParams) return promisifyRoute(this.options.generate.routes || [])
.catch((e) => {
console.error('Could not resolve routes') // eslint-disable-line no-console
console.error(e) // eslint-disable-line no-console
process.exit(1)
throw e // eslint-disable-line no-unreachable
}) })
.then(() => { })
.then((generateRoutes) => {
/* /*
** Generate html files from routes ** Generate html files from routes
*/ */
let routes = [] generateRoutes.forEach((route) => {
this.routes.forEach((route) => { if (this.routes.indexOf(route) < 0) {
if (route.includes(':') || route.includes('*')) { this.routes.push(route)
const routeParams = this.options.generate.routeParams[route]
if (!routeParams) {
console.error(`Could not generate the dynamic route ${route}, please add the mapping params in nuxt.config.js (generate.routeParams).`) // eslint-disable-line no-console
return process.exit(1)
}
route = route + '?'
const toPath = pathToRegexp.compile(route)
routes = routes.concat(routeParams.map((params) => {
return toPath(params)
}))
} else {
routes.push(route)
} }
}) })
let routes = this.routes
return co(function * () { return co(function * () {
while (routes.length) { while (routes.length) {
yield routes.splice(0, 500).map((route) => { yield routes.splice(0, 500).map((route) => {
return co(function * () { return co(function * () {
var { html } = yield self.renderRoute(route, { _generate: true }) var { html } = yield self.renderRoute(route, { _generate: true })
html = minify(html, { html = minify(html, self.options.generate.minify)
collapseBooleanAttributes: true,
collapseWhitespace: true,
decodeEntities: true,
minifyCSS: true,
minifyJS: true,
processConditionalComments: true,
removeAttributeQuotes: false,
removeComments: false,
removeEmptyAttributes: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
removeTagWhitespace: true,
sortAttributes: true,
sortClassName: true,
trimCustomFragments: true,
useShortDoctype: true
})
var path = join(route, sep, 'index.html') // /about -> /about/index.html var path = join(route, sep, 'index.html') // /about -> /about/index.html
debug('Generate file: ' + path) debug('Generate file: ' + path)
path = join(distPath, path) path = join(distPath, path)
@ -133,20 +119,3 @@ export default function () {
return this return this
}) })
} }
function resolveRouteParams (routeParams) {
let promises = []
Object.keys(routeParams).forEach(function (routePath) {
let promise = promisifyRouteParams(routeParams[routePath])
promise.then((routeParamsData) => {
routeParams[routePath] = routeParamsData
})
.catch((e) => {
console.error(`Could not resolve routeParams[${routePath}]`) // eslint-disable-line no-console
console.error(e) // eslint-disable-line no-console
process.exit(1)
})
promises.push(promise)
})
return Promise.all(promises)
}

View File

@ -2,10 +2,9 @@
import _ from 'lodash' import _ from 'lodash'
import co from 'co' import co from 'co'
import compression from 'compression'
import fs from 'fs-extra' import fs from 'fs-extra'
import pify from 'pify' import pify from 'pify'
import ansiHTML from 'ansi-html'
import serialize from 'serialize-javascript'
import Server from './server' import Server from './server'
import * as build from './build' import * as build from './build'
import * as render from './render' import * as render from './render'
@ -13,10 +12,8 @@ import generate from './generate'
import serveStatic from 'serve-static' import serveStatic from 'serve-static'
import { resolve, join } from 'path' import { resolve, join } from 'path'
import * as utils from './utils' import * as utils from './utils'
utils.setAnsiColors(ansiHTML)
class Nuxt { class Nuxt {
constructor (options = {}) { constructor (options = {}) {
var defaults = { var defaults = {
dev: true, dev: true,
@ -43,6 +40,10 @@ class Nuxt {
extendRoutes: null, extendRoutes: null,
scrollBehavior: null scrollBehavior: null
}, },
performance: {
gzip: true,
prefetch: true
},
build: {} build: {}
} }
// Sanitization // Sanitization
@ -63,28 +64,34 @@ class Nuxt {
if (fs.existsSync(join(this.srcDir, 'middleware'))) { if (fs.existsSync(join(this.srcDir, 'middleware'))) {
this.options.middleware = true this.options.middleware = true
} }
// Template // If app.html is defined, set the template path to the user template
this.appTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'app.html'), 'utf8'), { this.options.appTemplatePath = resolve(__dirname, 'views/app.template.html')
imports: { serialize } if (fs.existsSync(join(this.srcDir, 'app.html'))) {
}) this.options.appTemplatePath = join(this.srcDir, 'app.html')
this.errorTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'), {
imports: {
ansiHTML,
encodeHtml: utils.encodeHtml
} }
})
// renderer used by Vue.js (via createBundleRenderer) // renderer used by Vue.js (via createBundleRenderer)
this.renderer = null this.renderer = null
// For serving static/ files to / // For serving static/ files to /
this.serveStatic = pify(serveStatic(resolve(this.srcDir, 'static'))) this.serveStatic = pify(serveStatic(resolve(this.srcDir, 'static')))
// For serving .nuxt/dist/ files // For serving .nuxt/dist/ files (only when build.publicPath is not an URL)
this._nuxtRegexp = /^\/_nuxt\// this.serveStaticNuxt = pify(serveStatic(resolve(this.dir, '.nuxt', 'dist'), {
this.serveStaticNuxt = pify(serveStatic(resolve(this.dir, '.nuxt', 'dist'))) maxAge: (this.dev ? 0 : '1y') // 1 year in production
}))
// gzip for production
if (!this.dev) {
this.gzipMiddleware = pify(compression({
threshold: 0
}))
}
// Add this.Server Class // Add this.Server Class
this.Server = Server this.Server = Server
// Add this.build // Add this.build
build.options.call(this) // Add build options build.options.call(this) // Add build options
this.build = () => co(build.build.bind(this)) this.build = () => co(build.build.bind(this))
// Error template
this.errorTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'), {
interpolate: /{{([\s\S]+?)}}/g
})
// Add this.render and this.renderRoute // Add this.render and this.renderRoute
this.render = render.render.bind(this) this.render = render.render.bind(this)
this.renderRoute = render.renderRoute.bind(this) this.renderRoute = render.renderRoute.bind(this)
@ -123,7 +130,6 @@ class Nuxt {
if (typeof callback === 'function') callback() if (typeof callback === 'function') callback()
}) })
} }
} }
export default Nuxt export default Nuxt

View File

@ -1,10 +1,13 @@
'use strict' 'use strict'
const debug = require('debug')('nuxt:render') import ansiHTML from 'ansi-html'
import co from 'co' import co from 'co'
import { urlJoin, getContext } from './utils' import serialize from 'serialize-javascript'
import { getContext, setAnsiColors, encodeHtml } from './utils'
const debug = require('debug')('nuxt:render')
// force blue color // force blue color
debug.color = 4 debug.color = 4
setAnsiColors(ansiHTML)
export function render (req, res) { export function render (req, res) {
if (!this.renderer && !this.dev) { if (!this.renderer && !this.dev) {
@ -12,20 +15,25 @@ export function render (req, res) {
process.exit(1) process.exit(1)
} }
/* istanbul ignore if */ /* istanbul ignore if */
if (!this.renderer) { if (!this.renderer || !this.appTemplate) {
return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
this.render(req, res) resolve(this.render(req, res))
}, 1000) }, 1000)
return })
} }
const self = this const self = this
const context = getContext(req, res) const context = getContext(req, res)
return co(function * () { return co(function * () {
res.statusCode = 200
if (self.dev) { if (self.dev) {
// Call webpack middleware only in development // Call webpack middleware only in development
yield self.webpackDevMiddleware(req, res) yield self.webpackDevMiddleware(req, res)
yield self.webpackHotMiddleware(req, res) yield self.webpackHotMiddleware(req, res)
} }
if (!self.dev && self.options.performance.gzip === true) {
yield self.gzipMiddleware(req, res)
}
// If base in req.url, remove it for the middleware and vue-router // If base in req.url, remove it for the middleware and vue-router
if (self.options.router.base !== '/' && req.url.indexOf(self.options.router.base) === 0) { if (self.options.router.base !== '/' && req.url.indexOf(self.options.router.base) === 0) {
// Compatibility with base url for dev server // Compatibility with base url for dev server
@ -34,9 +42,9 @@ export function render (req, res) {
// Serve static/ files // Serve static/ files
yield self.serveStatic(req, res) yield self.serveStatic(req, res)
// Serve .nuxt/dist/ files (only for production) // Serve .nuxt/dist/ files (only for production)
if (!self.dev && self._nuxtRegexp.test(req.url)) { if (!self.dev && req.url.indexOf(self.options.build.publicPath) === 0) {
const url = req.url const url = req.url
req.url = req.url.replace(self._nuxtRegexp, '/') req.url = req.url.replace(self.options.build.publicPath, '/')
yield self.serveStaticNuxt(req, res) yield self.serveStaticNuxt(req, res)
/* istanbul ignore next */ /* istanbul ignore next */
req.url = url req.url = url
@ -44,23 +52,34 @@ export function render (req, res) {
}) })
.then(() => { .then(() => {
/* istanbul ignore if */ /* istanbul ignore if */
if (this.dev && this._nuxtRegexp.test(req.url) && req.url.includes('.hot-update.json')) { if (this.dev && req.url.indexOf(self.options.build.publicPath) === 0 && req.url.includes('.hot-update.json')) {
res.statusCode = 404 res.statusCode = 404
return res.end() return { html: '' }
} }
return this.renderRoute(req.url, context) return this.renderRoute(req.url, context)
}) })
.then(({ html, error }) => { .then(({ html, error, redirected }) => {
if (redirected) {
return html
}
if (error) { if (error) {
res.statusCode = context.nuxt.error.statusCode || 500 res.statusCode = context.nuxt.error.statusCode || 500
} }
res.setHeader('Content-Type', 'text/html; charset=utf-8') res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Content-Length', Buffer.byteLength(html)) res.setHeader('Content-Length', Buffer.byteLength(html))
res.end(html, 'utf8') res.end(html, 'utf8')
return html
}) })
.catch((err) => { .catch((err) => {
const html = this.errorTemplate({
error: err,
stack: ansiHTML(encodeHtml(err.stack))
})
res.statusCode = 500 res.statusCode = 500
res.end(this.errorTemplate({ err }), 'utf8') res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Content-Length', Buffer.byteLength(html))
res.end(html, 'utf8')
return err
}) })
} }
@ -72,20 +91,22 @@ export function renderRoute (url, context = {}) {
// Call rendertoSting from the bundleRenderer and generate the HTML (will update the context as well) // Call rendertoSting from the bundleRenderer and generate the HTML (will update the context as well)
const self = this const self = this
return co(function * () { return co(function * () {
let app = yield self.renderToString(context) let APP = yield self.renderToString(context)
if (!context.nuxt.serverRendered) { if (!context.nuxt.serverRendered) {
app = '<div id="__nuxt"></div>' APP = '<div id="__nuxt"></div>'
} }
const m = context.meta.inject()
let HEAD = m.meta.text() + m.title.text() + m.link.text() + m.style.text() + m.script.text() + m.noscript.text()
if (self.options.router.base !== '/') {
HEAD += `<base href="${self.options.router.base}">`
}
HEAD += context.styles
APP += `<script type="text/javascript">window.__NUXT__=${serialize(context.nuxt, { isJSON: true })}</script>`
const html = self.appTemplate({ const html = self.appTemplate({
dev: self.dev, // Use to add the extracted CSS <link> in production HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
baseUrl: self.options.router.base, BODY_ATTRS: m.bodyAttrs.text(),
APP: app, HEAD,
context: context, APP
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)
}
}) })
return { return {
html, html,
@ -128,10 +149,9 @@ export function renderAndGetWindow (url, opts = {}) {
window.scrollTo = function () {} window.scrollTo = function () {}
// If Nuxt could not be loaded (error from the server-side) // If Nuxt could not be loaded (error from the server-side)
if (!window.__NUXT__) { if (!window.__NUXT__) {
return reject({ let error = new Error('Could not load the nuxt app')
message: 'Could not load the nuxt app', error.body = window.document.getElementsByTagName('body')[0].innerHTML
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 // Used by nuxt.js to say when the components are loaded and the app ready
window.onNuxtReady(() => { window.onNuxtReady(() => {

View File

@ -3,7 +3,6 @@
const http = require('http') const http = require('http')
class Server { class Server {
constructor (nuxt) { constructor (nuxt) {
this.nuxt = nuxt this.nuxt = nuxt
this.server = http.createServer(this.render.bind(this)) this.server = http.createServer(this.render.bind(this))
@ -27,7 +26,6 @@ class Server {
close (cb) { close (cb) {
return this.server.close(cb) return this.server.close(cb)
} }
} }
export default Server export default Server

View File

@ -28,15 +28,19 @@ export function * waitFor (ms) {
} }
export function urlJoin () { export function urlJoin () {
return [].slice.call(arguments).join('/').replace(/\/+/g, '/') return [].slice.call(arguments).join('/').replace(/\/+/g, '/').replace(':/', '://')
} }
export function promisifyRouteParams (fn) { export function isUrl (url) {
// If routeParams[route] is an array return (url.indexOf('http') === 0 || url.indexOf('//') === 0)
}
export function promisifyRoute (fn) {
// If routes is an array
if (Array.isArray(fn)) { if (Array.isArray(fn)) {
return Promise.resolve(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) { if (fn.length === 1) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fn(function (err, routeParams) { fn(function (err, routeParams) {

View File

@ -1,19 +0,0 @@
<% var m = context.meta.inject() %><!DOCTYPE html>
<html n-head-ssr <%= m.htmlAttrs.text() %>>
<head>
<%= m.meta.text() %>
<%= m.title.text() %>
<%= m.link.text() %>
<%= m.style.text() %>
<%= m.script.text() %>
<%= m.noscript.text() %>
<% if (baseUrl !== '/') { %><base href="<%= baseUrl %>"><% } %>
<% if (!dev) { %><link rel="stylesheet" href="<%= files.css %>"><% } %>
</head>
<body <%= m.bodyAttrs.text() %>>
<%= APP %>
<script type="text/javascript" defer>window.__NUXT__=<%= serialize(context.nuxt, { isJSON: true }) %></script>
<script src="<%= files.vendor %>" defer></script>
<script src="<%= files.app %>" defer></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
<head>
{{ HEAD }}
</head>
<body {{ BODY_ATTRS }}>
{{ APP }}
</body>
</html>

View File

@ -2,10 +2,10 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Vue.js error</title> <title>Nuxt.js error</title>
</head> </head>
<body style="background-color: #a6004c;color: #efe;font-family: monospace;"> <body style="background-color: #a6004c;color: #efe;font-family: monospace;">
<h2>Vue.js error</h2> <h2>Nuxt.js error</h2>
<pre><%= ansiHTML(encodeHtml(err.stack)) %></pre> <pre>{{ stack }}</pre>
</body> </body>
</html> </html>

View File

@ -3,7 +3,7 @@
import vueLoaderConfig from './vue-loader.config' import vueLoaderConfig from './vue-loader.config'
import { defaults } from 'lodash' import { defaults } from 'lodash'
import { join } from 'path' 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 }) { export default function ({ isClient, isServer }) {
const nodeModulesDir = join(__dirname, '..', 'node_modules') const nodeModulesDir = join(__dirname, '..', 'node_modules')
let config = { let config = {
devtool: 'source-map', devtool: (this.dev ? 'cheap-module-eval-source-map' : false),
entry: { entry: {
vendor: ['vue', 'vue-router', 'vue-meta'] vendor: ['vue', 'vue-router', 'vue-meta']
}, },
output: { 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: { performance: {
maxEntrypointSize: 300000,
maxAssetSize: 300000,
hints: (this.dev ? false : 'warning') hints: (this.dev ? false : 'warning')
}, },
resolve: { resolve: {
@ -64,16 +66,13 @@ export default function ({ isClient, isServer }) {
loader: 'babel-loader', loader: 'babel-loader',
exclude: /node_modules/, exclude: /node_modules/,
query: defaults(this.options.build.babel, { query: defaults(this.options.build.babel, {
plugins: [ presets: ['vue-app'],
'transform-async-to-generator',
'transform-runtime'
],
presets: [
['es2015', { modules: false }],
'stage-2'
],
cacheDirectory: !!this.dev cacheDirectory: !!this.dev
}) })
},
{
test: /\.css$/,
loader: 'vue-style-loader!css-loader'
} }
] ]
}, },

View File

@ -2,7 +2,10 @@
import { each } from 'lodash' import { each } from 'lodash'
import webpack from 'webpack' 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 ProgressBarPlugin from 'progress-bar-webpack-plugin'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import base from './base.config.js' import base from './base.config.js'
@ -40,38 +43,55 @@ export default function () {
}) })
// Webpack plugins // Webpack plugins
config.plugins = (config.plugins || []).concat([ config.plugins = (config.plugins || []).concat([
// strip comments in Vue code // Strip comments in Vue code
new webpack.DefinePlugin(Object.assign(env, { new webpack.DefinePlugin(Object.assign(env, {
'process.env.NODE_ENV': JSON.stringify(this.dev ? 'development' : 'production'), 'process.env.NODE_ENV': JSON.stringify(this.dev ? 'development' : 'production'),
'process.BROWSER_BUILD': true, 'process.BROWSER_BUILD': true,
'process.SERVER_BUILD': false 'process.SERVER_BUILD': false,
'process.browser': true,
'process.server': true
})), })),
// Extract vendor chunks for better caching // Extract vendor chunks for better caching
new webpack.optimize.CommonsChunkPlugin({ new webpack.optimize.CommonsChunkPlugin({
name: 'vendor', name: 'vendor',
filename: this.options.build.filenames.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 // client bundle progress bar
config.plugins.push( config.plugins.push(
new ProgressBarPlugin() new ProgressBarPlugin()
) )
// Add friendly error plugin
if (this.dev) {
config.plugins.push(new FriendlyErrorsWebpackPlugin())
}
// Production client build // Production client build
if (!this.dev) { if (!this.dev) {
config.plugins.push( 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 // This is needed in webpack 2 for minifying CSS
new webpack.LoaderOptionsPlugin({ new webpack.LoaderOptionsPlugin({
minimize: true minimize: true
}), }),
// Minify JS // Minify JS
new webpack.optimize.UglifyJsPlugin({ new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
compress: { compress: {
warnings: false warnings: false
} }

View File

@ -1,6 +1,7 @@
'use strict' 'use strict'
import webpack from 'webpack' import webpack from 'webpack'
import VueSSRPlugin from 'vue-ssr-webpack-plugin'
import base from './base.config.js' import base from './base.config.js'
import { each, uniq } from 'lodash' import { each, uniq } from 'lodash'
import { existsSync, readFileSync } from 'fs' import { existsSync, readFileSync } from 'fs'
@ -22,7 +23,7 @@ export default function () {
config = Object.assign(config, { config = Object.assign(config, {
target: 'node', target: 'node',
devtool: false, devtool: 'source-map',
entry: resolve(this.dir, '.nuxt', 'server.js'), entry: resolve(this.dir, '.nuxt', 'server.js'),
output: Object.assign({}, config.output, { output: Object.assign({}, config.output, {
path: resolve(this.dir, '.nuxt', 'dist'), path: resolve(this.dir, '.nuxt', 'dist'),
@ -30,14 +31,26 @@ export default function () {
libraryTarget: 'commonjs2' libraryTarget: 'commonjs2'
}), }),
plugins: (config.plugins || []).concat([ plugins: (config.plugins || []).concat([
new VueSSRPlugin({
filename: 'server-bundle.json'
}),
new webpack.DefinePlugin(Object.assign(env, { new webpack.DefinePlugin(Object.assign(env, {
'process.env.NODE_ENV': JSON.stringify(this.dev ? 'development' : 'production'), 'process.env.NODE_ENV': JSON.stringify(this.dev ? 'development' : 'production'),
'process.BROWSER_BUILD': false, 'process.BROWSER_BUILD': false, // deprecated
'process.SERVER_BUILD': true '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 // Externals
const nuxtPackageJson = require('../../package.json') const nuxtPackageJson = require('../../package.json')
const projectPackageJsonPath = resolve(this.dir, '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 || {})) config.externals = config.externals.concat(Object.keys(projectPackageJson.dependencies || {}))
} catch (e) {} } catch (e) {}
} }
config.externals = config.externals.concat(this.options.build.vendor)
config.externals = uniq(config.externals) config.externals = uniq(config.externals)
// Extend config // Extend config

View File

@ -4,19 +4,14 @@ import { defaults } from 'lodash'
export default function ({ isClient }) { export default function ({ isClient }) {
let babelOptions = JSON.stringify(defaults(this.options.build.babel, { let babelOptions = JSON.stringify(defaults(this.options.build.babel, {
plugins: [ presets: ['vue-app'],
'transform-async-to-generator', cacheDirectory: !!this.dev
'transform-runtime'
],
presets: [
['es2015', { modules: false }],
'stage-2'
]
})) }))
let config = { let config = {
postcss: this.options.build.postcss, postcss: this.options.build.postcss,
loaders: { loaders: {
'js': 'babel-loader?' + babelOptions, 'js': 'babel-loader?' + babelOptions,
'css': 'vue-style-loader!css-loader',
'less': 'vue-style-loader!css-loader!less-loader', 'less': 'vue-style-loader!css-loader!less-loader',
'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax', 'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax',
'scss': 'vue-style-loader!css-loader!sass-loader', 'scss': 'vue-style-loader!css-loader!sass-loader',
@ -25,17 +20,6 @@ export default function ({ isClient }) {
}, },
preserveWhitespace: false 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 the config
return config return config
} }

View File

@ -38,7 +38,7 @@
"nuxt": "./bin/nuxt" "nuxt": "./bin/nuxt"
}, },
"scripts": { "scripts": {
"test": "nyc ava --serial test/", "test": "nyc ava --verbose --serial test/",
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov", "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
"lint": "eslint --ext .js,.vue bin lib pages test/*.js --ignore-pattern lib/app", "lint": "eslint --ext .js,.vue bin lib pages test/*.js --ignore-pattern lib/app",
"build": "webpack", "build": "webpack",
@ -52,63 +52,68 @@
}, },
"dependencies": { "dependencies": {
"ansi-html": "^0.0.7", "ansi-html": "^0.0.7",
"autoprefixer": "^6.7.2", "autoprefixer": "^6.7.7",
"babel-core": "^6.22.1", "babel-core": "^6.24.0",
"babel-loader": "^6.2.10", "babel-loader": "^6.4.1",
"babel-plugin-array-includes": "^2.0.3", "babel-preset-vue-app": "^0.5.1",
"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",
"chokidar": "^1.6.1", "chokidar": "^1.6.1",
"co": "^4.6.0", "co": "^4.6.0",
"css-loader": "^0.26.1", "compression": "^1.6.2",
"debug": "^2.6.1", "css-loader": "^0.27.3",
"extract-text-webpack-plugin": "2.0.0-beta.4", "debug": "^2.6.3",
"file-loader": "^0.10.0", "file-loader": "^0.10.1",
"fs-extra": "^2.0.0", "friendly-errors-webpack-plugin": "^1.6.1",
"fs-extra": "^2.1.2",
"glob": "^7.1.1", "glob": "^7.1.1",
"hash-sum": "^1.0.2", "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", "lodash": "^4.17.4",
"lru-cache": "^4.0.2", "lru-cache": "^4.0.2",
"memory-fs": "^0.4.1", "memory-fs": "^0.4.1",
"path-to-regexp": "^1.7.0",
"pify": "^2.3.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", "progress-bar-webpack-plugin": "^1.9.3",
"script-ext-html-webpack-plugin": "^1.7.1",
"serialize-javascript": "^1.3.0", "serialize-javascript": "^1.3.0",
"serve-static": "^1.11.2", "serve-static": "^1.12.1",
"url-loader": "^0.5.7", "url-loader": "^0.5.8",
"vue": "^2.1.10", "vue": "^2.2.5",
"vue-loader": "^10.3.0", "vue-loader": "^11.3.3",
"vue-meta": "^0.5.3", "vue-meta": "^0.5.5",
"vue-router": "^2.2.0", "vue-router": "^2.3.0",
"vue-server-renderer": "^2.1.10", "vue-server-renderer": "^2.2.5",
"vue-ssr-html-stream": "^2.2.0", "vue-ssr-html-stream": "^2.2.0",
"vue-template-compiler": "^2.1.10", "vue-ssr-webpack-plugin": "^1.0.2",
"vuex": "^2.1.2", "vue-template-compiler": "^2.2.5",
"webpack": "^2.2.1", "vuex": "^2.2.1",
"webpack-bundle-analyzer": "^2.2.3", "webpack": "^2.3.1",
"webpack-dev-middleware": "^1.10.0", "webpack-bundle-analyzer": "^2.3.1",
"webpack-hot-middleware": "^2.16.1" "webpack-dev-middleware": "^1.10.1",
"webpack-hot-middleware": "^2.17.1"
}, },
"devDependencies": { "devDependencies": {
"ava": "^0.18.1", "ava": "^0.18.2",
"babel-eslint": "^7.1.1", "babel-eslint": "^7.2.1",
"codecov": "^1.0.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", "copy-webpack-plugin": "^4.0.1",
"eslint": "^3.15.0", "eslint": "^3.18.0",
"eslint-config-standard": "^6.2.1", "eslint-config-standard": "^8.0.0-beta.2",
"eslint-plugin-html": "^2.0.0", "eslint-plugin-html": "^2.0.1",
"eslint-plugin-promise": "^3.4.1", "eslint-plugin-import": "^2.2.0",
"eslint-plugin-standard": "^2.0.1", "eslint-plugin-node": "^4.2.1",
"finalhandler": "^0.5.1", "eslint-plugin-promise": "^3.5.0",
"jsdom": "^9.10.0", "eslint-plugin-standard": "^2.1.1",
"finalhandler": "^1.0.1",
"jsdom": "^9.12.0",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"nyc": "^10.1.2", "nyc": "^10.2.0-candidate.0",
"request": "^2.79.0", "request": "^2.81.0",
"request-promise-native": "^1.0.3", "request-promise-native": "^1.0.3",
"webpack-node-externals": "^1.5.4" "webpack-node-externals": "^1.5.4"
} }

View File

@ -1,45 +1,19 @@
import test from 'ava' import test from 'ava'
import { resolve } from 'path' import { resolve } from 'path'
test('Fail to generate without routeParams', t => { test('Fail with routes() which throw an error', 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 => {
const Nuxt = require('../') const Nuxt = require('../')
const options = { const options = {
rootDir: resolve(__dirname, 'fixtures/basic'), rootDir: resolve(__dirname, 'fixtures/basic'),
dev: false, dev: false,
generate: { generate: {
routeParams: { routes: function () {
'/users/:id': function () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
reject('Not today!') reject(new Error('Not today!'))
}) })
} }
} }
} }
}
const nuxt = new Nuxt(options) const nuxt = new Nuxt(options)
return new Promise((resolve) => { return new Promise((resolve) => {
var oldExit = process.exit var oldExit = process.exit
@ -50,12 +24,12 @@ test('Fail with routeParams which throw an error', t => {
process.exit = oldExit process.exit = oldExit
console.error = oldCE // eslint-disable-line no-console console.error = oldCE // eslint-disable-line no-console
t.is(code, 1) t.is(code, 1)
t.true(_log.includes('Could not resolve routeParams[/users/:id]')) t.true(_log.includes('Could not resolve routes'))
resolve() resolve()
} }
nuxt.generate() nuxt.generate()
.catch((e) => { .catch((e) => {
t.true(e === 'Not today!') t.true(e.message === 'Not today!')
}) })
}) })
}) })

View File

@ -1,11 +1,9 @@
module.exports = { module.exports = {
generate: { generate: {
routeParams: { routes: [
'/users/:id': [ '/users/1',
{ id: 1 }, '/users/2',
{ id: 2 }, '/users/3'
{ id: 3 }
] ]
} }
}
} }

View File

@ -4,7 +4,7 @@
<script> <script>
export default { export default {
data () { asyncData () {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => resolve({ name: 'Nuxt.js' }), 10) setTimeout(() => resolve({ name: 'Nuxt.js' }), 10)
}) })

View File

@ -10,7 +10,7 @@ const fetchData = () => {
} }
export default { export default {
async data () { async asyncData () {
return await fetchData() return await fetchData()
} }
} }

View File

@ -4,7 +4,7 @@
<script> <script>
export default { export default {
async data (context, callback) { async asyncData (context, callback) {
setTimeout(function () { setTimeout(function () {
callback(null, { name: 'Callback Nuxt.js' }) callback(null, { name: 'Callback Nuxt.js' })
}, 10) }, 10)

View File

@ -4,7 +4,7 @@
<script> <script>
export default { export default {
data () { asyncData () {
throw new Error('Error mouahahah') throw new Error('Error mouahahah')
} }
} }

View File

@ -4,7 +4,7 @@
<script> <script>
export default { export default {
data ({ error }) { asyncData ({ error }) {
error({ message: 'Custom error' }) error({ message: 'Custom error' })
} }
} }

View File

@ -4,7 +4,7 @@
<script> <script>
export default { export default {
data ({ params }) { asyncData ({ params }) {
return { id: params.id } return { id: params.id }
} }
} }

10
test/fixtures/with-config/app.html vendored Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
<head>
{{ HEAD }}
</head>
<body {{ BODY_ATTRS }}>
{{ APP }}
<p>Made by Nuxt.js team</p>
</body>
</html>

View File

@ -10,7 +10,10 @@ module.exports = {
} }
}, },
cache: true, cache: true,
plugins: ['~plugins/test.js'], plugins: [
'~plugins/test.js',
{ src: '~plugins/only-client.js', ssr: false }
],
loading: '~components/loading', loading: '~components/loading',
env: { env: {
bool: true, bool: true,
@ -18,6 +21,7 @@ module.exports = {
string: 'Nuxt.js' string: 'Nuxt.js'
}, },
build: { build: {
publicPath: '/orion/',
analyze: { analyze: {
analyzerMode: 'disabled', analyzerMode: 'disabled',
generateStatsFile: true generateStatsFile: true

View File

@ -5,7 +5,7 @@
<script> <script>
export default { export default {
layout: 'custom-env', layout: 'custom-env',
data ({ env }) { asyncData ({ env }) {
return { env } return { env }
} }
} }

View File

@ -5,7 +5,7 @@
<script> <script>
export default { export default {
middleware: 'user-agent', middleware: 'user-agent',
data ({ userAgent }) { asyncData ({ userAgent }) {
return { userAgent } return { userAgent }
} }
} }

View File

@ -0,0 +1 @@
console.log('Only called in client-side!')

View File

@ -37,9 +37,9 @@ test('urlJoin', t => {
t.is(utils.urlJoin('test', '/about'), 'test/about') t.is(utils.urlJoin('test', '/about'), 'test/about')
}) })
test('promisifyRouteParams (array)', t => { test('promisifyRoute (array)', t => {
const array = [1] const array = [1]
const promise = utils.promisifyRouteParams(array) const promise = utils.promisifyRoute(array)
t.is(typeof promise, 'object') t.is(typeof promise, 'object')
return promise return promise
.then((res) => { .then((res) => {
@ -47,12 +47,12 @@ test('promisifyRouteParams (array)', t => {
}) })
}) })
test('promisifyRouteParams (fn => array)', t => { test('promisifyRoute (fn => array)', t => {
const array = [1, 2] const array = [1, 2]
const fn = function () { const fn = function () {
return array return array
} }
const promise = utils.promisifyRouteParams(fn) const promise = utils.promisifyRoute(fn)
t.is(typeof promise, 'object') t.is(typeof promise, 'object')
return promise return promise
.then((res) => { .then((res) => {
@ -60,14 +60,14 @@ test('promisifyRouteParams (fn => array)', t => {
}) })
}) })
test('promisifyRouteParams (fn => promise)', t => { test('promisifyRoute (fn => promise)', t => {
const array = [1, 2, 3] const array = [1, 2, 3]
const fn = function () { const fn = function () {
return new Promise((resolve) => { return new Promise((resolve) => {
resolve(array) resolve(array)
}) })
} }
const promise = utils.promisifyRouteParams(fn) const promise = utils.promisifyRoute(fn)
t.is(typeof promise, 'object') t.is(typeof promise, 'object')
return promise return promise
.then((res) => { .then((res) => {
@ -75,24 +75,24 @@ test('promisifyRouteParams (fn => promise)', t => {
}) })
}) })
test('promisifyRouteParams (fn(cb) with error)', t => { test('promisifyRoute (fn(cb) with error)', t => {
const fn = function (cb) { const fn = function (cb) {
cb('Error here') cb(new Error('Error here'))
} }
const promise = utils.promisifyRouteParams(fn) const promise = utils.promisifyRoute(fn)
t.is(typeof promise, 'object') t.is(typeof promise, 'object')
return promise return promise
.catch((e) => { .catch((e) => {
t.is(e, 'Error here') t.is(e.message, 'Error here')
}) })
}) })
test('promisifyRouteParams (fn(cb) with result)', t => { test('promisifyRoute (fn(cb) with result)', t => {
const array = [1, 2, 3, 4] const array = [1, 2, 3, 4]
const fn = function (cb) { const fn = function (cb) {
cb(null, array) cb(null, array)
} }
const promise = utils.promisifyRouteParams(fn) const promise = utils.promisifyRoute(fn)
t.is(typeof promise, 'object') t.is(typeof promise, 'object')
return promise return promise
.then((res) => { .then((res) => {

View File

@ -24,6 +24,16 @@ test('/', async t => {
t.true(html.includes('<h1>I have custom configurations</h1>')) t.true(html.includes('<h1>I have custom configurations</h1>'))
}) })
test('/ (custom app.html)', async t => {
const { html } = await nuxt.renderRoute('/')
t.true(html.includes('<p>Made by Nuxt.js team</p>'))
})
test('/ (custom build.publicPath)', async t => {
const { html } = await nuxt.renderRoute('/')
t.true(html.includes('src="/test/orion/vendor.bundle'))
})
test('/test/ (router base)', async t => { test('/test/ (router base)', async t => {
const window = await nuxt.renderAndGetWindow(url('/test/')) const window = await nuxt.renderAndGetWindow(url('/test/'))
const html = window.document.body.innerHTML const html = window.document.body.innerHTML
@ -62,7 +72,7 @@ test('/test/about-bis (added with extendRoutes)', async t => {
test('Check stats.json generated by build.analyze', t => { test('Check stats.json generated by build.analyze', t => {
const stats = require(resolve(__dirname, 'fixtures/with-config/.nuxt/dist/stats.json')) const stats = require(resolve(__dirname, 'fixtures/with-config/.nuxt/dist/stats.json'))
t.is(stats.assets.length, 11) t.is(stats.assets.length, 19)
}) })
// Close server and ask nuxt to stop listening to file changes // Close server and ask nuxt to stop listening to file changes

View File

@ -1,3 +1,7 @@
// Until babel-loader 7 is released
process.noDeprecation = true
var nodeExternals = require('webpack-node-externals') var nodeExternals = require('webpack-node-externals')
var ProgressBarPlugin = require('progress-bar-webpack-plugin') var ProgressBarPlugin = require('progress-bar-webpack-plugin')
var CopyWebpackPlugin = require('copy-webpack-plugin') var CopyWebpackPlugin = require('copy-webpack-plugin')

6063
yarn.lock Normal file

File diff suppressed because it is too large Load Diff