Merge branch 'master' into offline-plugin-integration

This commit is contained in:
Sébastien Chopin 2017-04-08 11:33:32 +02:00 committed by GitHub
commit 2560bfb512
95 changed files with 7235 additions and 559 deletions

2
.gitignore vendored
View File

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

View File

@ -10,15 +10,17 @@
<a href="https://donorbox.org/nuxt"><img src="https://img.shields.io/badge/Support%20us-donate-41B883.svg" alt="Support us"></a>
</p>
> Nuxt.js is a framework for server-rendered Vue applications (inspired by [Next.js](https://github.com/zeit/next.js))
## 🚧 Under active development, 1.0 will be released soon :fire:
## 🚧 Under active development, [1.0](https://github.com/nuxt/nuxt.js/projects/1) will be released soon :fire:
## Links
- 📘 Documentation: [https://nuxtjs.org](https://nuxtjs.org)
- 🎬 Video: [1 minute demo](https://www.youtube.com/watch?v=kmf-p-pTi40)
- 🐦 Twitter: [@nuxt_js](https://twitter.com/nuxt_js)
- 👉 [Play with Nuxt.js online](https://glitch.com/edit/#!/nuxt-hello-world)
## Getting started
@ -119,7 +121,7 @@ This is mostly used for `nuxt generate` and test purposes but you might find ano
nuxt.renderRoute('/about', context)
.then(function ({ html, error }) {
// You can check error to know if your app displayed the error page for this route
// Useful to set the correct status status code if an error appended:
// Useful to set the correct status code if an error appended:
if (error) {
return res.status(error.statusCode || 500).send(html)
}

View File

@ -15,13 +15,32 @@ if (process.argv.indexOf('--analyze') !== -1 || process.argv.indexOf('-a') !== -
process.argv = without(process.argv, '--analyze', '-a')
}
var nuxtConfigFileName = 'nuxt.config.js'
// --config-file option
var indexOfConfig = false
if (process.argv.indexOf('--config-file') !== -1) {
indexOfConfig = process.argv.indexOf('--config-file')
} else if (process.argv.indexOf('-c') !== -1) {
indexOfConfig = process.argv.indexOf('-c')
}
if (indexOfConfig !== false) {
nuxtConfigFileName = process.argv.slice(indexOfConfig)[1]
process.argv = without(process.argv, '--config-file', '-c', nuxtConfigFileName)
}
// Root directory parameter
var rootDir = resolve(process.argv.slice(2)[0] || '.')
var nuxtConfigFile = resolve(rootDir, 'nuxt.config.js')
var nuxtConfigFilePath = resolve(rootDir, nuxtConfigFileName)
var options = {}
if (fs.existsSync(nuxtConfigFile)) {
options = require(nuxtConfigFile)
if (fs.existsSync(nuxtConfigFilePath)) {
options = require(nuxtConfigFilePath)
} else {
console.log(`Could not locate ${nuxtConfigFilePath}`) // eslint-disable-line no-console
}
if (typeof options.rootDir !== 'string') {
options.rootDir = rootDir
}

View File

@ -63,6 +63,6 @@ function listenOnConfigChanges (nuxt, server) {
})
}, 200)
var nuxtConfigFile = resolve(rootDir, 'nuxt.config.js')
chokidar.watch(nuxtConfigFile, { ignoreInitial: true })
chokidar.watch(nuxtConfigFile, Object.assign({}, nuxt.options.watchers.chokidar, { ignoreInitial: true }))
.on('all', build)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -1,11 +1,11 @@
{
"name": "nuxt-global-css",
"dependencies": {
"bulma": "^0.2.3",
"hover.css": "^2.0.2",
"node-sass": "^3.11.2",
"nuxt": "latest",
"sass-loader": "^4.0.2"
"bulma": "^0.4.0",
"hover.css": "^2.2.0",
"node-sass": "^4.5.1",
"nuxt": "^0.10.0",
"sass-loader": "^6.0.3"
},
"scripts": {
"dev": "nuxt",

View File

@ -20,6 +20,3 @@ export default {
}
}
</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 start"
}
}

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>
export default {
data ({ req }) {
asyncData ({ req }) {
return {
name: req ? 'server' : 'client'
}

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ module.exports = {
vendor: ['axios', 'mini-toastr', 'vue-notifications']
},
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'
export default {
data () {
asyncData () {
return axios.get('https://jsonplaceholder.typicode.com/photos/4').then(res => res.data)
}
}

View File

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

View File

@ -1,26 +1,23 @@
// This code will be injected before initializing the root App
import Vue from 'vue'
import VueNotifications from 'vue-notifications'
// Include mini-toaster (or any other UI-notification library
import miniToastr from 'mini-toastr'
if (process.BROWSER_BUILD) {
// Include mini-toaster (or any other UI-notification library
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)
}
// 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)
// All not-specified events (types) would be piped to output in console.
const options = {
success: toast,
error: toast,
info: toast,
warn: toast
}
// Activate plugin
Vue.use(VueNotifications, options)
// Here we setup messages output to `mini-toastr`
const toast = function ({ title, message, type, 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.
// 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.
const options = {
success: toast,
error: toast,
info: toast,
warn: toast
}
// Activate plugin
Vue.use(VueNotifications, options)

View File

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

View File

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

View File

@ -1,11 +1,18 @@
<template>
<div>
<p>
<h3>Index Module</h3>
<button @click="increment">{{ counter }}</button>
<br>
<nuxt-link to="/about">About</nuxt-link>
<br>
<br>
<h3>Todo Module</h3>
<nuxt-link to="/todos">Todos</nuxt-link>
<br>
<br>
<h3>Nested Modules</h3>
<nuxt-link to="/website">Website</nuxt-link>
</p>
</div>
</template>

View File

@ -0,0 +1,33 @@
<template>
<div>
<h2>Articles</h2>
<ul>
<li v-for="article in articles">
<span>{{article}}</span>
</li>
</ul>
<h2>Comments <small>(nested under articles)</small></h2>
<ul>
<li v-for="comment in comments">
<span>{{comment}}</span>
</li>
</ul>
<nuxt-link to="/">Home</nuxt-link>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: mapGetters({
articles: 'articles/get',
comments: 'articles/comments/get'
}),
methods: {
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,19 @@
export const state = {
list: [
'Lorem ipsum',
'dolor sit amet',
'consetetur sadipscing elitr'
]
}
export const mutations = {
add (state, title) {
state.list.push(title)
}
}
export const getters = {
get (state) {
return state.list
}
}

View File

@ -0,0 +1,19 @@
export const state = {
list: [
'Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.',
'Lorem ipsum dolor sit amet, consetetur sadipscing elit.',
'Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.'
]
}
export const mutations = {
add (state, title) {
state.list.push(title)
}
}
export const getters = {
get (state) {
return state.list
}
}

View File

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

View File

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

View File

@ -0,0 +1,4 @@
# Using Vuetify.js with Nuxt.js
https://nuxtjs.org/examples/with-vuetify<br>
https://vuetifyjs.com/

View File

@ -0,0 +1 @@
@require './vendor/vuetify.styl'

View File

@ -0,0 +1,14 @@
// Specify overrides (theme and/or base variables etc.)
// See https://vuetifyjs.com/quick-start
$theme := {
primary: #9c27b0
accent: #ce93d8
secondary: #424242
info: #0D47A1
warning: #ffb300
error: #B71C1C
success: #2E7D32
}
// Import Vuetify styling
@require '~vuetify/src/stylus/main.styl'

View File

@ -0,0 +1,18 @@
const { join } = require('path')
module.exports = {
build: {
vendor: ['vuetify']
},
plugins: ['~plugins/vuetify.js'],
css: [
{ src: join(__dirname, 'css/app.styl'), lang: 'styl' }
],
head: {
link: [
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Roboto' },
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' }
]
}
}

View File

@ -0,0 +1,16 @@
{
"name": "with-vuetify",
"dependencies": {
"nuxt": "0.10",
"vuetify": "0.9.4"
},
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start"
},
"devDependencies": {
"stylus": "^0.54.5",
"stylus-loader": "^3.0.1"
}
}

View File

@ -0,0 +1,45 @@
<template>
<v-app top-toolbar left-fixed-sidebar>
<v-toolbar>
<v-toolbar-side-icon @click.native.stop="sidebar = !sidebar" />
<v-toolbar-logo>Toolbar</v-toolbar-logo>
</v-toolbar>
<main>
<v-sidebar left fixed drawer v-model="sidebar">
<v-list>
<v-list-item v-for="i in 3" :key="i">
<v-list-tile>
<v-list-tile-title>Item {{ i }}</v-list-tile-title>
</v-list-tile>
</v-list-item>
</v-list>
</v-sidebar>
<v-content>
<v-container fluid>
<div class="title">
<h2>Main content</h2>
<v-btn primary>Primary button</v-btn>
<v-btn secondary>Secondary button</v-btn>
<v-btn success>Success button</v-btn>
</div>
</v-container>
</v-content>
</main>
</v-app>
</template>
<script>
export default {
asyncData() {
return {
sidebar: false
}
}
}
</script>
<style scoped>
.title {
padding-left: 20px;
}
</style>

View File

@ -0,0 +1,4 @@
import Vue from 'vue'
import Vuetify from 'vuetify'
Vue.use(Vuetify)

View File

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

View File

@ -12,7 +12,7 @@ let layouts = {
<%
var layoutsKeys = Object.keys(layouts);
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'
this.layoutName = layout
let _layout = '_' + layout
if (typeof layouts[_layout] === 'function') {
return this.loadLayout(_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]()
.then((Component) => {
layouts[_layout] = Component
this.layout = layouts[_layout]
return this.layout
return layouts[_layout]
})
.catch((e) => {
if (this.$nuxt) {

View File

@ -2,7 +2,7 @@
import Vue from 'vue'
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'
const noopData = () => { return {} }
const noopFetch = () => {}
@ -55,23 +55,28 @@ function loadAsyncComponents (to, from, next) {
}
function callMiddleware (Components, context, layout) {
// Call middleware
// if layout is undefined, only call global middleware
let midd = <%= serialize(router.middleware, { isJSON: true }) %>
if (layout.middleware) {
midd = midd.concat(layout.middleware)
}
Components.forEach((Component) => {
if (Component.options.middleware) {
midd = midd.concat(Component.options.middleware)
let unknownMiddleware = false
if (typeof layout !== 'undefined') {
midd = [] // exclude global middleware if layout defined (already called before)
if (layout.middleware) {
midd = midd.concat(layout.middleware)
}
})
Components.forEach((Component) => {
if (Component.options.middleware) {
midd = midd.concat(Component.options.middleware)
}
})
}
midd = midd.map((name) => {
if (typeof middleware[name] !== 'function') {
unknownMiddleware = true
this.error({ statusCode: 500, message: 'Unknown middleware ' + name })
}
return middleware[name]
})
if (this.$options._nuxt.err) return
if (unknownMiddleware) return
return promiseSeries(midd, context)
}
@ -82,13 +87,15 @@ function render (to, from, next) {
nextCalled = true
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)
this._context = context
this._dateLastError = this.$options._nuxt.dateErr
this._hadError = !!this.$options._nuxt.err
if (!Components.length) {
// 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(() => {
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
Components.forEach(function (Component) {
if (!Component._data) {
Component._data = Component.options.data || noopData
}
if (Component._Ctor && Component._Ctor.options) {
Component.options.asyncData = Component._Ctor.options.asyncData
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))
let nextCalled = false
// 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(() => {
// Pass validation?
@ -127,7 +130,7 @@ function render (to, from, next) {
if (typeof Component.options.validate !== 'function') return
isValid = Component.options.validate({
params: to.params || {},
query: to.query || {}
query : to.query || {}<%= (store ? ', store: context.store' : '') %>
})
})
if (!isValid) {
@ -141,13 +144,19 @@ function render (to, from, next) {
return Promise.resolve()
}
let promises = []
// Validate method
if (Component._data && typeof Component._data === 'function') {
var promise = promisify(Component._data, context)
promise.then((data) => {
Component._cData = () => data || {}
Component.options.data = Component._cData
Component._dataFn = Component.options.data.toString().replace(/\s/g, '')
// asyncData method
if (Component.options.asyncData && typeof Component.options.asyncData === 'function') {
var promise = promisify(Component.options.asyncData, context)
promise.then((asyncDataResult) => {
let data = {}
// Call data() if defined
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) {
Component._Ctor.options.data = Component.options.data
}
@ -157,7 +166,7 @@ function render (to, from, next) {
}
if (Component.options.fetch) {
var p = Component.options.fetch(context)
if (!(p instanceof Promise)) { p = Promise.resolve(p) }
if (!p || (!(p instanceof Promise) && (typeof p.then !== 'function'))) { p = Promise.resolve(p) }
<%= (loading ? 'p.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %>
promises.push(p)
}
@ -175,8 +184,15 @@ function render (to, from, next) {
.catch((error) => {
_lastPaths = []
error.statusCode = error.statusCode || error.status || (error.response && error.response.status) || 500
this.error(error)
next(false)
let layout = NuxtError.layout
if (typeof layout === 'function') {
layout = layout(context)
}
this.loadLayout(layout)
.then(() => {
this.error(error)
next(false)
})
})
}
@ -213,73 +229,104 @@ function fixPrepatch (to, ___) {
if (this._hadError && this._dateLastError === this.$options._nuxt.dateErr) {
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
hotReloadAPI(this)
setTimeout(() => hotReloadAPI(this), 100)
})
}
// Special hot reload with data(context)
// Special hot reload with asyncData(context)
function hotReloadAPI (_app) {
if (!module.hot) return
const $nuxt = _app.$nuxt
var _forceUpdate = $nuxt.$forceUpdate.bind($nuxt)
$nuxt.$forceUpdate = function () {
let Component = getMatchedComponents(router.currentRoute)[0]
let $components = []
let $nuxt = _app.$nuxt
while ($nuxt && $nuxt.$children && $nuxt.$children.length) {
$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 (typeof Component === 'object' && !Component.options) {
// Updated via vue-router resolveAsyncComponents()
Component = Vue.extend(Component)
Component._Ctor = Component
}
this.error()
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) {
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
router.push(path)
}
const context = getContext({ route: router.currentRoute<%= (store ? ', store' : '') %>, isClient: true, next: next.bind(this), error: _app.error })
// Check if data has been updated
const originalDataFn = (Component._data || noopData).toString().replace(/\s/g, '')
const newDataFn = (Component._Ctor.options.data || noopData).toString().replace(/\s/g, '')
if (originalDataFn !== newDataFn) {
Component._data = Component._Ctor.options.data || noopData
let p = promisify(Component._data, context)
p.then((data) => {
Component._cData = () => data || {}
Component.options.data = Component._cData
Component._dataFn = Component.options.data.toString().replace(/\s/g, '')
let context = getContext({ route: router.currentRoute<%= (store ? ', store' : '') %>, isClient: true, next: next.bind(this), error: this.error })
<%= (loading ? 'this.$loading.start && this.$loading.start()' : '') %>
callMiddleware.call(this, Components, context)
.then(() => {
// If layout changed
if (depth !== 0) return Promise.resolve()
let layout = Component.options.layout || 'default'
if (typeof layout === 'function') {
layout = layout(context)
}
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
<%= (loading ? 'this.$loading.increase && this.$loading.increase(30)' : '') %>
})
promises.push(p)
} else if (Component._cData) {
Component._data = Component.options.data
Component.options.data = Component._cData
Component._Ctor.options.data = Component.options.data
}
// Check if fetch has been updated
const originalFetchFn = (Component.options.fetch || noopFetch).toString().replace(/\s/g, '')
const newFetchFn = (Component._Ctor.options.fetch || noopFetch).toString().replace(/\s/g, '')
// Fetch has been updated, we call it to update the store
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(() => {
promises.push(pAsyncData)
// Call fetch()
Component.options.fetch = Component.options.fetch || noopFetch
let pFetch = Component.options.fetch(context)
if (!pFetch || (!(pFetch instanceof Promise) && (typeof pFetch.then !== 'function'))) { pFetch = Promise.resolve(pFetch) }
<%= (loading ? 'pFetch.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %>
promises.push(pFetch)
return Promise.all(promises)
})
.then(() => {
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
_forceUpdate()
setTimeout(() => hotReloadAPI(this), 100)
})
}
}
@ -302,13 +349,14 @@ const resolveComponents = flatMapComponents(router.match(path), (Component, _, m
Component._Ctor = Component
Component.extendOptions = Component.options
}
if (Component.options.data && typeof Component.options.data === 'function') {
Component._data = Component.options.data
if (NUXT.serverRendered) {
Component._cData = () => NUXT.data[index] || {}
Component.options.data = Component._cData
Component._dataFn = Component.options.data.toString().replace(/\s/g, '')
if (NUXT.serverRendered) {
let data = {}
if (Component.options.data && typeof Component.options.data === 'function') {
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) {
Component._Ctor.options.data = Component.options.data
}
@ -338,25 +386,23 @@ Promise.all(resolveComponents)
.then((Components) => {
const _app = new Vue(app)
return _app.setLayout(Components.length ? Components[0].options.layout : '')
let layout = NUXT.layout || 'default'
return _app.loadLayout(layout)
.then(() => {
_app.setLayout(layout)
return { _app, Components }
})
})
.then(({ _app, Components }) => {
const mountApp = () => {
_app.$mount('#__nuxt')
// Hot reloading
hotReloadAPI(_app)
// Call window.onNuxtReady callbacks
Vue.nextTick(() => nuxtReady(_app))
Vue.nextTick(() => {
// Hot reloading
hotReloadAPI(_app)
// Call window.onNuxtReady callbacks
nuxtReady(_app)
})
}
<% if (store) { %>
// Replace store state
if (NUXT.state) {
store.replaceState(NUXT.state)
}
<% } %>
_app.setTransitions = _app.$options._nuxt.setTransitions.bind(_app)
if (Components.length) {
_app.setTransitions(mapTransitions(Components, router.currentRoute))

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import router from './router.js'
<% if (store) { %>import store from './store.js'<% } %>
import NuxtChild from './components/nuxt-child.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 App from '<%= appPath %>'
@ -19,12 +20,18 @@ Vue.component(Nuxt.name, Nuxt)
// vue-meta configuration
Vue.use(Meta, {
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
ssrAttribute: 'n-head-ssr', // the attribute name that lets vue-meta know that meta info has already been server-rendered
attribute: 'data-n-head', // the attribute name vue-meta adds to the tags it observes
ssrAttribute: 'data-n-head-ssr', // the attribute name that lets vue-meta know that meta info has already been server-rendered
tagIDKeyName: 'hid' // the property name that vue-meta uses to determine whether to overwrite or append a tag
})
if (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
// Useful for jsdom testing or plugins (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading)
window._nuxtReadyCbs = []
@ -34,8 +41,16 @@ if (process.BROWSER_BUILD) {
}
// Includes external plugins
<% plugins.forEach(function (pluginPath) { %>
require('<%= pluginPath %>')
<% plugins.forEach(function (plugin) {
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
@ -85,4 +100,4 @@ const 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 _routes = recursiveRoutes(router.routes, '\t\t', _components)
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) { %>

View File

@ -3,10 +3,11 @@
const debug = require('debug')('nuxt:render')
debug.color = 4 // force blue color
import Vue from 'vue'
import { stringify } from 'querystring'
import { omit } from 'lodash'
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'
const isDev = <%= isDev %>
@ -21,13 +22,13 @@ export default context => {
// Add store to the context
<%= (store ? 'context.store = store' : '') %>
// Nuxt object
context.nuxt = { data: [], error: null<%= (store ? ', state: null' : '') %>, serverRendered: true }
context.nuxt = { layout: 'default', data: [], error: null<%= (store ? ', state: null' : '') %>, serverRendered: true }
// create context.next for simulate next() of beforeEach() when wanted to redirect
context.redirected = false
context.next = function (opts) {
context.redirected = opts
// if nuxt generate
if (!context.res) {
context.redirected = opts
context.nuxt.serverRendered = false
return
}
@ -39,25 +40,57 @@ export default context => {
})
context.res.end()
}
// set router's location
router.push(context.url)
// Add route to the context
context.route = router.currentRoute
// Add meta infos
context.meta = _app.$meta()
// Error function
context.error = _app.$options._nuxt.error.bind(_app)
<%= (isDev ? 'const s = isDev && Date.now()' : '') %>
const ctx = getContext(context)
let Components = getMatchedComponents(context.route)
<% if (store) { %>
let promise = (store._actions && store._actions.nuxtServerInit ? store.dispatch('nuxtServerInit', omit(getContext(context), 'redirect', 'error')) : null)
if (!(promise instanceof Promise)) promise = Promise.resolve()
<% } else { %>
let promise = Promise.resolve()
<% } %>
return promise
let ctx = null
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
}
// For debugging purpose
if (!Component.options.name && Component.options.__file) {
Component.options.name = Component.options.__file
}
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) { %>
let promise = (store._actions && store._actions.nuxtServerInit ? store.dispatch('nuxtServerInit', omit(getContext(context), 'redirect', 'error')) : null)
if (!promise || (!(promise instanceof Promise) && (typeof promise.then !== 'function'))) promise = Promise.resolve()
<% } else { %>
let promise = Promise.resolve()
<% } %>
return promise
})
.then(() => {
// Sanitize Components
Components = Components.map((Component) => {
@ -71,12 +104,30 @@ export default context => {
}
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
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) => {
// Call middleware
let midd = <%= serialize(router.middleware, { isJSON: true }) %>
// Set layout to __NUXT__
context.nuxt.layout = _app.layoutName
// Call middleware (layout + pages)
let midd = []
if (layout.middleware) {
midd = midd.concat(layout.middleware)
}
@ -102,7 +153,7 @@ export default context => {
if (typeof Component.options.validate !== 'function') return
isValid = Component.options.validate({
params: context.route.params || {},
query: context.route.query || {}
query: context.route.query || {}<%= (store ? ', store: ctx.store' : '') %>
})
})
if (!isValid) {
@ -114,13 +165,20 @@ export default context => {
Components = []
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) => {
let promises = []
if (Component.options.data && typeof Component.options.data === 'function') {
Component._data = Component.options.data
let promise = promisify(Component._data, ctx)
promise.then((data) => {
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') {
data = Component.options.data()
}
// Merge data() and asyncData() results
data = Object.assign(data, asyncDataResult)
Component.options.data = () => data
Component._Ctor.options.data = Component.options.data
})
@ -130,6 +188,8 @@ export default context => {
}
if (Component.options.fetch) {
promises.push(Component.options.fetch(ctx))
} else {
promises.push(null)
}
return Promise.all(promises)
}))
@ -141,16 +201,28 @@ export default context => {
return _app
}
<% 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
context.nuxt.data = res.map((tab) => tab[0])
context.nuxt.data = res.map((r) => (r[0] || {}))
context.nuxt.error = _app.$options._nuxt.err
<%= (store ? '// Add the state from the vuex store' : '') %>
<%= (store ? 'context.nuxt.state = store.state' : '') %>
return _app
// If no error, return main app
if (!context.nuxt.error) {
return _app
}
// Load layout for error page
let layout = (typeof NuxtError.layout === 'function' ? NuxtError.layout(ctx) : NuxtError.layout)
context.nuxt.layout = layout || ''
return _app.loadLayout(layout)
.then(() => _app.setLayout(layout))
.then(() => _app)
})
.catch(function (error) {
if (!componentsLoaded && error instanceof Error) {
return Promise.reject(error)
}
if (error && (error instanceof Error || error.constructor.toString().indexOf('Error()') !== -1)) {
let statusCode = error.statusCode || error.status || (error.response && error.response.status) || 500
error = { statusCode, message: error.message }

View File

@ -2,7 +2,7 @@ import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
let files = require.context('~/store', false, /^\.\/.*\.js$/)
let files = require.context('~/store', true, /^\.\/.*\.js$/)
let filenames = files.keys()
function getModule (filename) {
@ -12,6 +12,17 @@ function getModule (filename) {
: file
}
function getModuleNamespace (storeData, namePath) {
if (namePath.length === 1) {
return storeData.modules
}
let namespace = namePath.shift()
storeData.modules[namespace] = storeData.modules[namespace] || {}
storeData.modules[namespace].namespaced = true
storeData.modules[namespace].modules = storeData.modules[namespace].modules || {}
return getModuleNamespace(storeData.modules[namespace], namePath)
}
let store
let storeData = {}
@ -31,8 +42,13 @@ if (store == null) {
for (let filename of filenames) {
let name = filename.replace(/^\.\//, '').replace(/\.js$/, '')
if (name === 'index') continue
storeData.modules[name] = getModule(filename)
storeData.modules[name].namespaced = true
let namePath = name.split(/\//)
let module = getModuleNamespace(storeData, namePath)
name = namePath.pop()
module[name] = getModule(filename)
module[name].namespaced = true
}
store = new Vuex.Store(storeData)
}

View File

@ -84,7 +84,7 @@ export function promisify (fn, context) {
} else {
promise = fn(context)
}
if (!(promise instanceof Promise)) {
if (!promise || (!(promise instanceof Promise) && (typeof promise.then !== 'function'))) {
promise = Promise.resolve(promise)
}
return promise

View File

@ -1,6 +1,5 @@
'use strict'
const debug = require('debug')('nuxt:build')
import _ from 'lodash'
import co from 'co'
import chokidar from 'chokidar'
@ -8,15 +7,17 @@ import fs from 'fs-extra'
import hash from 'hash-sum'
import pify from 'pify'
import webpack from 'webpack'
import PostCompilePlugin from 'post-compile-webpack-plugin'
import serialize from 'serialize-javascript'
import { createBundleRenderer } from 'vue-server-renderer'
import { join, resolve, sep } from 'path'
import { isUrl } from './utils'
import clientWebpackConfig from './webpack/client.config.js'
import serverWebpackConfig from './webpack/server.config.js'
import chalk from 'chalk'
import PostCompilePlugin from 'post-compile-webpack-plugin'
const debug = require('debug')('nuxt:build')
const remove = pify(fs.remove)
const readFile = pify(fs.readFile)
const utimes = pify(fs.utimes)
const writeFile = pify(fs.writeFile)
const mkdirp = pify(fs.mkdirp)
const glob = pify(require('glob'))
@ -38,21 +39,17 @@ const r = function () {
args = args.map(normalize)
return wp(resolve.apply(null, args))
}
const webpackStats = {
chunks: false,
children: false,
modules: false,
colors: true
}
let webpackStats = 'none'
// force green color
debug.color = 2
const defaults = {
analyze: false,
publicPath: '/_nuxt/',
filenames: {
css: 'style.css',
vendor: 'vendor.bundle.js',
app: 'nuxt.bundle.js'
manifest: 'manifest.[hash].js',
vendor: 'vendor.bundle.[hash].js',
app: 'nuxt.bundle.[chunkhash].js'
},
vendor: [],
loaders: [],
@ -90,13 +87,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.postcss)) extraDefaults.postcss = defaultsPostcss
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
if (!this.dev) {
webpackStats = {
chunks: false,
children: false,
modules: false,
colors: true
}
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)) {
const bundle = fs.readFileSync(bundlePath, 'utf8')
createRenderer.call(this, bundle)
createRenderer.call(this, JSON.parse(bundle))
addAppTemplate.call(this)
}
}
}
@ -138,6 +146,16 @@ function * buildFiles () {
webpackRunClient.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 +169,8 @@ function * generateRoutesAndFiles () {
if (name === 'error') return
layouts[name] = r(this.srcDir, file)
})
// Generate routes based on files
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/
debug('Generating files...')
let templatesFiles = [
'App.vue',
'client.js',
@ -176,6 +185,7 @@ function * generateRoutesAndFiles () {
'components/nuxt-link.js',
'components/nuxt.vue'
]
this.options.store = fs.existsSync(join(this.srcDir, 'store'))
let templateVars = {
uniqBy: _.uniqBy,
isDev: this.dev,
@ -188,24 +198,32 @@ function * generateRoutesAndFiles () {
},
env: this.options.env,
head: this.options.head,
middleware: this.options.middleware,
middleware: fs.existsSync(join(this.srcDir, 'middleware')),
store: this.options.store,
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',
layouts: layouts,
loading: (typeof this.options.loading === 'string' ? r(this.srcDir, this.options.loading) : this.options.loading),
transition: this.options.transition,
components: {
ErrorPage: './nuxt-error.vue'
ErrorPage: null
}
}
// Format routes for the lib/app/router.js template
templateVars.router.routes = createRoutes(files, this.srcDir)
if (typeof this.options.router.extendRoutes === 'function') {
// 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')) {
templateVars.components.ErrorPage = r(this.srcDir, 'layouts/error.vue')
}
@ -229,7 +247,13 @@ function * generateRoutesAndFiles () {
}
})
const content = template(templateVars)
return writeFile(r(this.dir, '.nuxt', file), content, 'utf8')
const path = r(this.dir, '.nuxt', file)
return writeFile(path, content, 'utf8')
.then(() => {
// Fix webpack loop (https://github.com/webpack/watchpack/issues/25#issuecomment-287789288)
const dateFS = Date.now() / 1000 - 30
return utimes(path, dateFS, dateFS)
})
})
})
yield moveTemplates
@ -325,6 +349,19 @@ function cleanChildrenRoutes (routes, isChild = false) {
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 () {
return clientWebpackConfig.call(this)
}
@ -335,26 +372,17 @@ function getWebpackServerConfig () {
function createWebpackMiddleware () {
const clientConfig = getWebpackClientConfig.call(this)
const host = process.env.HOST || '127.0.0.1'
const port = process.env.PORT || '3000'
const host = process.env.HOST || process.env.npm_package_config_nuxt_host || '127.0.0.1'
const port = process.env.PORT || process.env.npm_package_config_nuxt_port || '3000'
// setup on the fly compilation + hot-reload
clientConfig.entry.app = _.flatten(['webpack-hot-middleware/client?reload=true', clientConfig.entry.app])
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
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
if (!stats.hasErrors() && !stats.hasWarnings()) {
console.log(`> Open http://${host}:${port}\n`) // eslint-disable-line no-console
}
console.log() // eslint-disable-line no-console
})
)
const clientCompiler = webpack(clientConfig)
@ -363,11 +391,22 @@ function createWebpackMiddleware () {
publicPath: clientConfig.output.publicPath,
stats: webpackStats,
quiet: true,
noInfo: true
noInfo: true,
watchOptions: this.options.watchers.webpack
}))
this.webpackHotMiddleware = pify(require('webpack-hot-middleware')(clientCompiler, {
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 () {
@ -375,22 +414,22 @@ function webpackWatchAndUpdate () {
const mfs = new MFS()
const serverConfig = getWebpackServerConfig.call(this)
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
this.webpackServerWatcher = serverCompiler.watch({}, (err) => {
this.webpackServerWatcher = serverCompiler.watch(this.options.watchers.webpack, (err) => {
if (err) throw err
createRenderer.call(this, mfs.readFileSync(outputPath, 'utf-8'))
createRenderer.call(this, JSON.parse(mfs.readFileSync(outputPath, 'utf-8')))
})
}
function webpackRunClient () {
return new Promise((resolve, reject) => {
const clientConfig = getWebpackClientConfig.call(this)
const serverCompiler = webpack(clientConfig)
serverCompiler.run((err, stats) => {
const clientCompiler = webpack(clientConfig)
clientCompiler.run((err, stats) => {
if (err) return reject(err)
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()
})
})
@ -403,11 +442,11 @@ function webpackRunServer () {
serverCompiler.run((err, stats) => {
if (err) return reject(err)
console.log('[nuxt:build:server]\n', stats.toString(webpackStats)) // eslint-disable-line no-console
if (stats.hasErrors()) return reject('Webpack build exited with errors')
const bundlePath = join(serverConfig.output.path, serverConfig.output.filename)
if (stats.hasErrors()) return reject(new Error('Webpack build exited with errors'))
const bundlePath = join(serverConfig.output.path, 'server-bundle.json')
readFile(bundlePath, 'utf8')
.then((bundle) => {
createRenderer.call(this, bundle)
createRenderer.call(this, JSON.parse(bundle))
resolve()
})
})
@ -434,22 +473,20 @@ function createRenderer (bundle) {
function watchPages () {
const patterns = [
r(this.srcDir, 'pages'),
r(this.srcDir, 'layouts'),
r(this.srcDir, 'store'),
r(this.srcDir, 'middleware'),
r(this.srcDir, 'pages/*.vue'),
r(this.srcDir, 'pages/**/*.vue'),
r(this.srcDir, 'layouts'),
r(this.srcDir, 'layouts/*.vue'),
r(this.srcDir, 'layouts/**/*.vue')
]
const options = {
const options = Object.assign({}, this.options.watchers.chokidar, {
ignoreInitial: true
}
})
/* istanbul ignore next */
const refreshFiles = _.debounce(() => {
var d = Date.now()
co(generateRoutesAndFiles.bind(this))
.then(() => {
console.log('Time to gen:' + (Date.now() - d) + 'ms') // eslint-disable-line no-console
})
}, 200)
this.pagesFilesWatcher = chokidar.watch(patterns, options)
.on('add', refreshFiles)

View File

@ -1,14 +1,13 @@
'use strict'
const debug = require('debug')('nuxt:generate')
import fs from 'fs-extra'
import co from 'co'
import pify from 'pify'
import pathToRegexp from 'path-to-regexp'
import _ from 'lodash'
import { resolve, join, dirname, sep } from 'path'
import { promisifyRouteParams } from './utils'
import { isUrl, promisifyRoute } from './utils'
import { minify } from 'html-minifier'
const debug = require('debug')('nuxt:generate')
const copy = pify(fs.copy)
const remove = pify(fs.remove)
const writeFile = pify(fs.writeFile)
@ -16,21 +15,32 @@ const mkdirp = pify(fs.mkdirp)
const defaults = {
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 () {
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
*/
this.options.generate = _.defaultsDeep(this.options.generate, defaults)
@ -38,7 +48,7 @@ export default function () {
var srcStaticPath = resolve(this.srcDir, 'static')
var srcBuiltPath = resolve(this.dir, '.nuxt', 'dist')
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 * () {
/*
** Launch build process
@ -61,55 +71,31 @@ export default function () {
debug('Static & build files copied')
})
.then(() => {
// Resolve config.generate.routesParams promises before generating the routes
return resolveRouteParams(this.options.generate.routeParams)
// Resolve config.generate.routes promises before generating the routes
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
*/
let routes = []
this.routes.forEach((route) => {
if (route.includes(':') || route.includes('*')) {
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)
generateRoutes.forEach((route) => {
if (this.routes.indexOf(route) < 0) {
this.routes.push(route)
}
})
let routes = this.routes
return co(function * () {
while (routes.length) {
yield routes.splice(0, 500).map((route) => {
return co(function * () {
var { html } = yield self.renderRoute(route, { _generate: true })
html = minify(html, {
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
})
html = minify(html, self.options.generate.minify)
var path = join(route, sep, 'index.html') // /about -> /about/index.html
debug('Generate file: ' + path)
path = join(distPath, path)
@ -133,20 +119,3 @@ export default function () {
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 co from 'co'
import compression from 'compression'
import fs from 'fs-extra'
import pify from 'pify'
import ansiHTML from 'ansi-html'
import serialize from 'serialize-javascript'
import Server from './server'
import * as build from './build'
import * as render from './render'
@ -13,10 +12,8 @@ import generate from './generate'
import serveStatic from 'serve-static'
import { resolve, join } from 'path'
import * as utils from './utils'
utils.setAnsiColors(ansiHTML)
class Nuxt {
constructor (options = {}) {
var defaults = {
dev: true,
@ -43,6 +40,16 @@ class Nuxt {
extendRoutes: null,
scrollBehavior: null
},
performance: {
gzip: {
threshold: 0
},
prefetch: true
},
watchers: {
webpack: {},
chokidar: {}
},
build: {}
}
// Sanitization
@ -58,33 +65,32 @@ class Nuxt {
if (fs.existsSync(join(this.srcDir, 'store'))) {
this.options.store = true
}
// If middleware defined, update middleware option to true
this.options.middleware = false
if (fs.existsSync(join(this.srcDir, 'middleware'))) {
this.options.middleware = true
// If app.html is defined, set the template path to the user template
this.options.appTemplatePath = resolve(__dirname, 'views/app.template.html')
if (fs.existsSync(join(this.srcDir, 'app.html'))) {
this.options.appTemplatePath = join(this.srcDir, 'app.html')
}
// Template
this.appTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'app.html'), 'utf8'), {
imports: { serialize }
})
this.errorTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'), {
imports: {
ansiHTML,
encodeHtml: utils.encodeHtml
}
})
// renderer used by Vue.js (via createBundleRenderer)
this.renderer = null
// For serving static/ files to /
this.serveStatic = pify(serveStatic(resolve(this.srcDir, 'static')))
// For serving .nuxt/dist/ files
this._nuxtRegexp = /^\/_nuxt\//
this.serveStaticNuxt = pify(serveStatic(resolve(this.dir, '.nuxt', 'dist')))
// For serving .nuxt/dist/ files (only when build.publicPath is not an URL)
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.options.performance.gzip) {
this.gzipMiddleware = pify(compression(this.options.performance.gzip))
}
// Add this.Server Class
this.Server = Server
// Add this.build
build.options.call(this) // Add build options
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
this.render = render.render.bind(this)
this.renderRoute = render.renderRoute.bind(this)
@ -123,7 +129,6 @@ class Nuxt {
if (typeof callback === 'function') callback()
})
}
}
export default Nuxt

View File

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

View File

@ -3,7 +3,6 @@
const http = require('http')
class Server {
constructor (nuxt) {
this.nuxt = nuxt
this.server = http.createServer(this.render.bind(this))
@ -16,7 +15,7 @@ class Server {
}
listen (port, host) {
host = host || 'localhost'
host = host || '127.0.0.1'
port = port || 3000
this.server.listen(port, host, () => {
console.log('Ready on http://%s:%s', host, port) // eslint-disable-line no-console
@ -27,7 +26,6 @@ class Server {
close (cb) {
return this.server.close(cb)
}
}
export default Server

View File

@ -28,15 +28,19 @@ export function * waitFor (ms) {
}
export function urlJoin () {
return [].slice.call(arguments).join('/').replace(/\/+/g, '/')
return [].slice.call(arguments).join('/').replace(/\/+/g, '/').replace(':/', '://')
}
export function promisifyRouteParams (fn) {
// If routeParams[route] is an array
export function isUrl (url) {
return (url.indexOf('http') === 0 || url.indexOf('//') === 0)
}
export function promisifyRoute (fn) {
// If routes is an array
if (Array.isArray(fn)) {
return Promise.resolve(fn)
}
// If routeParams[route] is a function expecting a callback
// If routes is a function expecting a callback
if (fn.length === 1) {
return new Promise((resolve, reject) => {
fn(function (err, routeParams) {
@ -48,7 +52,7 @@ export function promisifyRouteParams (fn) {
})
}
let promise = fn()
if (!(promise instanceof Promise)) {
if (!promise || (!(promise instanceof Promise) && (typeof promise.then !== 'function'))) {
promise = Promise.resolve(promise)
}
return promise

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

View File

@ -3,7 +3,7 @@
import vueLoaderConfig from './vue-loader.config'
import { defaults } from 'lodash'
import { join } from 'path'
import { urlJoin } from '../utils'
import { isUrl, urlJoin } from '../utils'
/*
|--------------------------------------------------------------------------
@ -16,14 +16,16 @@ import { urlJoin } from '../utils'
export default function ({ isClient, isServer }) {
const nodeModulesDir = join(__dirname, '..', 'node_modules')
let config = {
devtool: 'source-map',
devtool: (this.dev ? 'cheap-module-eval-source-map' : false),
entry: {
vendor: ['vue', 'vue-router', 'vue-meta']
},
output: {
publicPath: urlJoin(this.options.router.base, '/_nuxt/')
publicPath: (isUrl(this.options.build.publicPath) ? this.options.build.publicPath : urlJoin(this.options.router.base, this.options.build.publicPath))
},
performance: {
maxEntrypointSize: 300000,
maxAssetSize: 300000,
hints: (this.dev ? false : 'warning')
},
resolve: {
@ -64,17 +66,15 @@ export default function ({ isClient, isServer }) {
loader: 'babel-loader',
exclude: /node_modules/,
query: defaults(this.options.build.babel, {
plugins: [
'transform-async-to-generator',
'transform-runtime'
],
presets: [
['es2015', { modules: false }],
'stage-2'
],
presets: ['vue-app'],
cacheDirectory: !!this.dev
})
}
},
{ test: /\.css$/, loader: 'vue-style-loader!css-loader' },
{ test: /\.less$/, loader: 'vue-style-loader!css-loader!less-loader' },
{ test: /\.sass$/, loader: 'vue-style-loader!css-loader!sass-loader?indentedSyntax' },
{ test: /\.scss$/, loader: 'vue-style-loader!css-loader!sass-loader' },
{ test: /\.styl(us)?$/, loader: 'vue-style-loader!css-loader!stylus-loader' }
]
},
plugins: this.options.build.plugins

View File

@ -2,7 +2,10 @@
import { each } from 'lodash'
import webpack from 'webpack'
import ExtractTextPlugin from 'extract-text-webpack-plugin'
import HTMLPlugin from 'html-webpack-plugin'
import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin'
import ScriptExtHtmlWebpackPlugin from 'script-ext-html-webpack-plugin'
import PreloadWebpackPlugin from 'preload-webpack-plugin'
import ProgressBarPlugin from 'progress-bar-webpack-plugin'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import OfflinePlugin from 'offline-plugin'
@ -74,38 +77,60 @@ export default function () {
})
// Webpack plugins
config.plugins = (config.plugins || []).concat([
// strip comments in Vue code
// Strip comments in Vue code
new webpack.DefinePlugin(Object.assign(env, {
'process.env.NODE_ENV': JSON.stringify(this.dev ? 'development' : 'production'),
'process.BROWSER_BUILD': true,
'process.SERVER_BUILD': false
'process.SERVER_BUILD': false,
'process.browser': true,
'process.server': true
})),
// Extract vendor chunks for better caching
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: this.options.build.filenames.vendor
}),
// Extract manifest
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
filename: this.options.build.filenames.manifest
}),
// Generate output HTML
new HTMLPlugin({
template: this.options.appTemplatePath
}),
// Add defer to scripts
new ScriptExtHtmlWebpackPlugin({
defaultAttribute: 'defer'
})
])
if (!this.dev && this.options.performance.prefetch === true) {
// Add prefetch code-splitted routes
config.plugins.push(
new PreloadWebpackPlugin({
rel: 'prefetch'
})
)
}
// client bundle progress bar
config.plugins.push(
new ProgressBarPlugin()
)
// Add friendly error plugin
if (this.dev) {
config.plugins.push(new FriendlyErrorsWebpackPlugin())
}
// Production client build
if (!this.dev) {
config.plugins.push(
// Use ExtractTextPlugin to extract CSS into a single file
new ExtractTextPlugin({
filename: this.options.build.filenames.css,
allChunks: true
}),
// This is needed in webpack 2 for minifying CSS
new webpack.LoaderOptionsPlugin({
minimize: true
}),
// Minify JS
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
compress: {
warnings: false
}
@ -114,7 +139,7 @@ export default function () {
}
// Extend config
if (typeof this.options.build.extend === 'function') {
this.options.build.extend(config, {
this.options.build.extend.call(this, config, {
dev: this.dev,
isClient: true
})

View File

@ -1,6 +1,7 @@
'use strict'
import webpack from 'webpack'
import VueSSRPlugin from 'vue-ssr-webpack-plugin'
import base from './base.config.js'
import { each, uniq } from 'lodash'
import { existsSync, readFileSync } from 'fs'
@ -22,22 +23,37 @@ export default function () {
config = Object.assign(config, {
target: 'node',
devtool: false,
devtool: (this.dev ? 'source-map' : false),
entry: resolve(this.dir, '.nuxt', 'server.js'),
output: Object.assign({}, config.output, {
path: resolve(this.dir, '.nuxt', 'dist'),
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
}),
performance: {
hints: false
},
plugins: (config.plugins || []).concat([
new VueSSRPlugin({
filename: 'server-bundle.json'
}),
new webpack.DefinePlugin(Object.assign(env, {
'process.env.NODE_ENV': JSON.stringify(this.dev ? 'development' : 'production'),
'process.BROWSER_BUILD': false,
'process.SERVER_BUILD': true
'process.BROWSER_BUILD': false, // deprecated
'process.SERVER_BUILD': true, // deprecated
'process.browser': false,
'process.server': true
}))
])
})
// This is needed in webpack 2 for minifying CSS
if (!this.dev) {
config.plugins.push(
new webpack.LoaderOptionsPlugin({
minimize: true
})
)
}
// Externals
const nuxtPackageJson = require('../../package.json')
const projectPackageJsonPath = resolve(this.dir, 'package.json')
@ -48,6 +64,7 @@ export default function () {
config.externals = config.externals.concat(Object.keys(projectPackageJson.dependencies || {}))
} catch (e) {}
}
config.externals = config.externals.concat(this.options.build.vendor)
config.externals = uniq(config.externals)
// Extend config

View File

@ -4,19 +4,14 @@ import { defaults } from 'lodash'
export default function ({ isClient }) {
let babelOptions = JSON.stringify(defaults(this.options.build.babel, {
plugins: [
'transform-async-to-generator',
'transform-runtime'
],
presets: [
['es2015', { modules: false }],
'stage-2'
]
presets: ['vue-app'],
cacheDirectory: !!this.dev
}))
let config = {
postcss: this.options.build.postcss,
loaders: {
'js': 'babel-loader?' + babelOptions,
'css': 'vue-style-loader!css-loader',
'less': 'vue-style-loader!css-loader!less-loader',
'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax',
'scss': 'vue-style-loader!css-loader!sass-loader',
@ -25,17 +20,6 @@ export default function ({ isClient }) {
},
preserveWhitespace: false
}
if (!this.dev && isClient) {
// Use ExtractTextPlugin to extract CSS into a single file
const ExtractTextPlugin = require('extract-text-webpack-plugin')
config.loaders.css = ExtractTextPlugin.extract({ loader: 'css-loader' })
config.loaders.scss = ExtractTextPlugin.extract({ loader: 'css-loader!sass-loader', fallbackLoader: 'vue-style-loader' })
config.loaders.sass = ExtractTextPlugin.extract({ loader: 'css-loader!sass-loader?indentedSyntax', fallbackLoader: 'vue-style-loader' })
config.loaders.stylus = ExtractTextPlugin.extract({ loader: 'css-loader!stylus-loader', fallbackLoader: 'vue-style-loader' })
config.loaders.less = ExtractTextPlugin.extract({ loader: 'css-loader!less-loader', fallbackLoader: 'vue-style-loader' })
}
// Return the config
return config
}

View File

@ -1,6 +1,6 @@
{
"name": "nuxt",
"version": "0.9.9",
"version": "0.10.5",
"description": "A minimalistic framework for server-rendered Vue.js applications (inspired by Next.js)",
"contributors": [
{
@ -38,7 +38,7 @@
"nuxt": "./bin/nuxt"
},
"scripts": {
"test": "nyc ava --serial test/",
"test": "nyc ava --verbose --serial test/",
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
"lint": "eslint --ext .js,.vue bin lib pages test/*.js --ignore-pattern lib/app",
"build": "webpack",
@ -52,63 +52,70 @@
},
"dependencies": {
"ansi-html": "^0.0.7",
"autoprefixer": "^6.7.2",
"babel-core": "^6.22.1",
"babel-loader": "^6.2.10",
"babel-plugin-array-includes": "^2.0.3",
"babel-plugin-transform-async-to-generator": "^6.22.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-es2015": "^6.22.0",
"babel-preset-stage-2": "^6.22.0",
"chalk": "^1.1.3",
"autoprefixer": "^6.7.7",
"babel-core": "^6.24.0",
"babel-loader": "^6.4.1",
"babel-preset-es2015": "^6.24.0",
"babel-preset-vue-app": "^1.1.1",
"chokidar": "^1.6.1",
"co": "^4.6.0",
"css-loader": "^0.26.1",
"debug": "^2.6.1",
"extract-text-webpack-plugin": "2.0.0-beta.4",
"file-loader": "^0.10.0",
"fs-extra": "^2.0.0",
"compression": "^1.6.2",
"css-loader": "^0.27.3",
"debug": "^2.6.3",
"file-loader": "^0.10.1",
"friendly-errors-webpack-plugin": "^1.6.1",
"fs-extra": "^2.1.2",
"glob": "^7.1.1",
"hash-sum": "^1.0.2",
"html-minifier": "^3.3.1",
"html-minifier": "^3.4.2",
"html-webpack-plugin": "^2.28.0",
"lodash": "^4.17.4",
"lru-cache": "^4.0.2",
"memory-fs": "^0.4.1",
"path-to-regexp": "^1.7.0",
"offline-plugin": "^4.6.1",
"pify": "^2.3.0",
"post-compile-webpack-plugin": "^0.1.1",
"preload-webpack-plugin": "^1.2.1",
"progress-bar-webpack-plugin": "^1.9.3",
"script-ext-html-webpack-plugin": "^1.7.1",
"serialize-javascript": "^1.3.0",
"serve-static": "^1.11.2",
"url-loader": "^0.5.7",
"vue": "^2.1.10",
"vue-loader": "^10.3.0",
"vue-meta": "^0.5.3",
"vue-router": "^2.2.0",
"vue-server-renderer": "^2.1.10",
"vue-template-compiler": "^2.1.10",
"vuex": "^2.1.2",
"webpack": "^2.2.1",
"webpack-bundle-analyzer": "^2.2.3",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.16.1"
"serve-static": "^1.12.1",
"url-loader": "^0.5.8",
"vue": "^2.2.5",
"vue-loader": "^11.3.3",
"vue-meta": "^0.5.5",
"vue-router": "^2.3.0",
"vue-server-renderer": "^2.2.5",
"vue-ssr-html-stream": "^2.2.0",
"vue-ssr-webpack-plugin": "^1.0.2",
"vue-template-compiler": "^2.2.5",
"vuex": "^2.2.1",
"webpack": "^2.3.2",
"webpack-bundle-analyzer": "^2.3.1",
"webpack-dev-middleware": "^1.10.1",
"webpack-hot-middleware": "^2.17.1"
},
"devDependencies": {
"ava": "^0.18.1",
"babel-eslint": "^7.1.1",
"codecov": "^1.0.1",
"ava": "^0.18.2",
"babel-eslint": "^7.2.1",
"babel-plugin-array-includes": "^2.0.3",
"babel-plugin-transform-async-to-generator": "^6.22.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-stage-2": "^6.22.0",
"codecov": "^2.1.0",
"copy-webpack-plugin": "^4.0.1",
"eslint": "^3.15.0",
"eslint-config-standard": "^6.2.1",
"eslint-plugin-html": "^2.0.0",
"eslint-plugin-promise": "^3.4.1",
"eslint-plugin-standard": "^2.0.1",
"finalhandler": "^0.5.1",
"jsdom": "^9.10.0",
"eslint": "^3.18.0",
"eslint-config-standard": "^8.0.0-beta.2",
"eslint-plugin-html": "^2.0.1",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-node": "^4.2.1",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^2.1.1",
"finalhandler": "^1.0.1",
"jsdom": "^9.12.0",
"json-loader": "^0.5.4",
"nyc": "^10.1.2",
"offline-plugin": "^4.6.1",
"request": "^2.79.0",
"nyc": "^10.2.0-candidate.0",
"request": "^2.81.0",
"request-promise-native": "^1.0.3",
"webpack-node-externals": "^1.5.4"
}

View File

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

View File

@ -42,6 +42,11 @@ test('/stateful', async t => {
t.true(html.includes('<div><p>The answer is 42</p></div>'))
})
test('/store', async t => {
const { html } = await nuxt.renderRoute('/store')
t.true(html.includes('<h1>Vuex Nested Modules</h1>'))
})
test('/head', async t => {
const window = await nuxt.renderAndGetWindow(url('/head'), { virtualConsole: false })
const html = window.document.body.innerHTML

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

11
test/fixtures/basic/pages/store.vue vendored Normal file
View File

@ -0,0 +1,11 @@
<template>
<h1>{{ baz }}</h1>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: mapGetters('foo/bar', ['baz'])
}
</script>

View File

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

9
test/fixtures/basic/store/foo/bar.js vendored Normal file
View File

@ -0,0 +1,9 @@
export const state = {
baz: 'Vuex Nested Modules'
}
export const getters = {
baz (state) {
return state.baz
}
}

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

@ -1,3 +1,9 @@
<template>
<h1>Error page</h1>
</template>
<script>
export default {
layout: 'custom'
}
</script>

View File

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

View File

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

View File

@ -0,0 +1,11 @@
<template>
<p>Never displayed</p>
</template>
<script>
export default {
fetch ({ error }) {
error({ message: 'Nuxt Error', statusCode: 300 })
}
}
</script>

View File

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

View File

@ -0,0 +1 @@
// Custom plugin (only included on client-side)

View File

@ -37,9 +37,9 @@ test('urlJoin', t => {
t.is(utils.urlJoin('test', '/about'), 'test/about')
})
test('promisifyRouteParams (array)', t => {
test('promisifyRoute (array)', t => {
const array = [1]
const promise = utils.promisifyRouteParams(array)
const promise = utils.promisifyRoute(array)
t.is(typeof promise, 'object')
return promise
.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 fn = function () {
return array
}
const promise = utils.promisifyRouteParams(fn)
const promise = utils.promisifyRoute(fn)
t.is(typeof promise, 'object')
return promise
.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 fn = function () {
return new Promise((resolve) => {
resolve(array)
})
}
const promise = utils.promisifyRouteParams(fn)
const promise = utils.promisifyRoute(fn)
t.is(typeof promise, 'object')
return promise
.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) {
cb('Error here')
cb(new Error('Error here'))
}
const promise = utils.promisifyRouteParams(fn)
const promise = utils.promisifyRoute(fn)
t.is(typeof promise, 'object')
return promise
.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 fn = function (cb) {
cb(null, array)
}
const promise = utils.promisifyRouteParams(fn)
const promise = utils.promisifyRoute(fn)
t.is(typeof promise, 'object')
return promise
.then((res) => {

View File

@ -24,9 +24,20 @@ test('/', async t => {
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 => {
const window = await nuxt.renderAndGetWindow(url('/test/'))
const html = window.document.body.innerHTML
t.is(window.__NUXT__.layout, 'default')
t.true(html.includes('<h1>Default layout</h1>'))
t.true(html.includes('<h1>I have custom configurations</h1>'))
})
@ -34,6 +45,7 @@ test('/test/ (router base)', async t => {
test('/test/about (custom layout)', async t => {
const window = await nuxt.renderAndGetWindow(url('/test/about'))
const html = window.document.body.innerHTML
t.is(window.__NUXT__.layout, 'custom')
t.true(html.includes('<h1>Custom layout</h1>'))
t.true(html.includes('<h1>About page</h1>'))
})
@ -47,6 +59,14 @@ test('/test/env', async t => {
t.true(html.includes('"string": "Nuxt.js"'))
})
test('/test/error', async t => {
const window = await nuxt.renderAndGetWindow(url('/test/error'))
const html = window.document.body.innerHTML
t.is(window.__NUXT__.layout, 'custom')
t.true(html.includes('<h1>Custom layout</h1>'))
t.true(html.includes('Error page'))
})
test('/test/user-agent', async t => {
const window = await nuxt.renderAndGetWindow(url('/test/user-agent'))
const html = window.document.body.innerHTML
@ -62,7 +82,7 @@ test('/test/about-bis (added with extendRoutes)', async t => {
test('Check stats.json generated by build.analyze', t => {
const stats = require(resolve(__dirname, 'fixtures/with-config/.nuxt/dist/stats.json'))
t.is(stats.assets.length, 11)
t.is(stats.assets.length, 23)
})
// 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 ProgressBarPlugin = require('progress-bar-webpack-plugin')
var CopyWebpackPlugin = require('copy-webpack-plugin')

6057
yarn.lock Normal file

File diff suppressed because it is too large Load Diff