Merge remote-tracking branch 'nuxt/master'

This commit is contained in:
Alexandre Chopin 2016-11-10 13:24:34 +01:00
commit bd5ec528c5
21 changed files with 151 additions and 95 deletions

View File

@ -49,7 +49,7 @@ So far, we get:
- Automatic transpilation and bundling (with webpack and babel) - Automatic transpilation and bundling (with webpack and babel)
- Hot code reloading - Hot code reloading
- Server rendering and indexing of `./pages` - Server rendering and indexing of `./pages`
- Static file serving. `./static/` is mapped to `/static/` - Static file serving. `./static/` is mapped to `/`
- Config file `nuxt.config.js` - Config file `nuxt.config.js`
- Code splitting via webpack - Code splitting via webpack
@ -97,8 +97,13 @@ This is mostly used for tests purpose but who knows!
```js ```js
nuxt.renderRoute('/about', context) nuxt.renderRoute('/about', context)
.then(function (html) { .then(function ({ html, error }) {
// HTML // 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:
if (error) {
return res.status(error.statusCode || 500).send(html)
}
res.send(html)
}) })
.catch(function (error) { .catch(function (error) {
// And error appended while rendering the route // And error appended while rendering the route

View File

@ -23,5 +23,5 @@ new Nuxt(options)
}) })
.catch((err) => { .catch((err) => {
console.error(err) console.error(err)
process.exit() process.exit(1)
}) })

View File

@ -15,6 +15,8 @@ if (typeof options.rootDir !== 'string') {
options.rootDir = rootDir options.rootDir = rootDir
} }
options.dev = true // Add hot reloading and watching changes
new Nuxt(options) new Nuxt(options)
.then((nuxt) => { .then((nuxt) => {
new Server(nuxt) new Server(nuxt)
@ -22,5 +24,5 @@ new Nuxt(options)
}) })
.catch((err) => { .catch((err) => {
console.error(err) console.error(err)
process.exit() process.exit(1)
}) })

View File

@ -25,5 +25,5 @@ new Nuxt(options)
}) })
.catch((err) => { .catch((err) => {
console.error(err) console.error(err)
process.exit() process.exit(1)
}) })

View File

@ -1,15 +1,25 @@
<template> <template>
<div class="container"> <div class="container">
<img src="/static/nuxt.png" /> <img src="/nuxt-square.png" />
<h2>About</h2> <h2>Thank you for testing nuxt.js</h2>
<p><router-link to="/">Home</router-link></p> <p><router-link to="/">Back home</router-link></p>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.container { .container {
font-family: serif; position: absolute;
margin-top: 200px; top: 0;
left: 0;
height: 100%;
width: 100%;
background: black;
color: white;
font-family: "Lucida Console", Monaco, monospace;
padding-top: 130px;
text-align: center; text-align: center;
} }
a {
color: silver;
}
</style> </style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="container"> <div class="container">
<img src="/static/nuxt.png" /> <img src="/nuxt.png" />
<h2>Hello World.</h2> <h2>Hello World.</h2>
<p><router-link to="/about">About</router-link></p> <p><router-link to="/about">About</router-link></p>
</div> </div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -40,6 +40,13 @@ async function renderAndGetWindow (route) {
}, },
done (err, window) { done (err, window) {
if (err) return reject(err) if (err) return reject(err)
// 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
})
}
// 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 = function () { window.onNuxtReady = function () {
resolve(window) resolve(window)
@ -54,7 +61,7 @@ async function renderAndGetWindow (route) {
*/ */
test('Route / exits and render HTML', async t => { test('Route / exits and render HTML', async t => {
let context = {} let context = {}
const html = await nuxt.renderRoute('/', context) const { html } = await nuxt.renderRoute('/', context)
t.true(html.includes('<p class="red-color">Hello world!</p>')) t.true(html.includes('<p class="red-color">Hello world!</p>'))
t.is(context.nuxt.error, null) t.is(context.nuxt.error, null)
t.is(context.nuxt.data[0].name, 'world') t.is(context.nuxt.data[0].name, 'world')
@ -75,5 +82,5 @@ test('Route / exits and render HTML', async t => {
// Close server and ask nuxt to stop listening to file changes // Close server and ask nuxt to stop listening to file changes
test.after('Closing server and nuxt.js', t => { test.after('Closing server and nuxt.js', t => {
server.close() server.close()
nuxt.stop() nuxt.close()
}) })

View File

@ -1,7 +1,6 @@
/*! /*
* nuxt.js ** nuxt.js
* MIT Licensed */
*/
'use strict' 'use strict'

View File

@ -1,3 +1,5 @@
'use strict'
require('es6-promise').polyfill() require('es6-promise').polyfill()
require('es6-object-assign').polyfill() require('es6-object-assign').polyfill()
import Vue from 'vue' import Vue from 'vue'

View File

@ -1,3 +1,5 @@
'use strict'
// The Vue build version to load with the `import` command // The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias. // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue' import Vue from 'vue'

View File

@ -1,3 +1,5 @@
'use strict'
import Vue from 'vue' import Vue from 'vue'
import Router from 'vue-router' import Router from 'vue-router'
import Meta from 'vue-meta' import Meta from 'vue-meta'

View File

@ -1,3 +1,5 @@
'use strict'
const debug = require('debug')('nuxt:render') const debug = require('debug')('nuxt:render')
import Vue from 'vue' import Vue from 'vue'
import { pick } from 'lodash' import { pick } from 'lodash'

View File

@ -2,7 +2,9 @@
const debug = require('debug')('nuxt:build') const debug = require('debug')('nuxt:build')
const _ = require('lodash') const _ = require('lodash')
const co = require('co')
const del = require('del') const del = require('del')
const chokidar = require('chokidar')
const fs = require('fs') const fs = require('fs')
const glob = require('glob-promise') const glob = require('glob-promise')
const hash = require('hash-sum') const hash = require('hash-sum')
@ -52,6 +54,10 @@ module.exports = function * () {
if (noBuild) { if (noBuild) {
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, serverConfig.output.filename)
if (!fs.existsSync(bundlePath)) {
console.error('> No build files found, please run `nuxt build` before launching `nuxt start`')
process.exit(1)
}
createRenderer.call(this, fs.readFileSync(bundlePath, 'utf8')) createRenderer.call(this, fs.readFileSync(bundlePath, 'utf8'))
return Promise.resolve() return Promise.resolve()
} }
@ -64,15 +70,15 @@ module.exports = function * () {
} else { } else {
console.error('> Couldn\'t find a `pages` directory. Please create one under the project root') console.error('> Couldn\'t find a `pages` directory. Please create one under the project root')
} }
process.exit() process.exit(1)
} }
if (this.options.store && !fs.existsSync(join(this.dir, 'store'))) { if (this.options.store && !fs.existsSync(join(this.dir, 'store'))) {
console.error('> No `store` directory found (store option activated). Please create on under the project root') console.error('> No `store` directory found (store option activated). Please create on under the project root')
process.exit() process.exit(1)
} }
if (this.options.store && !fs.existsSync(join(this.dir, 'store', 'index.js'))) { if (this.options.store && !fs.existsSync(join(this.dir, 'store', 'index.js'))) {
console.error('> No `store/index.js` file found (store option activated). Please create the file.') console.error('> No `store/index.js` file found (store option activated). Please create the file.')
process.exit() process.exit(1)
} }
debug(`App root: ${this.dir}`) debug(`App root: ${this.dir}`)
debug('Generating .nuxt/ files...') debug('Generating .nuxt/ files...')
@ -86,6 +92,34 @@ module.exports = function * () {
if (!this.dev) { if (!this.dev) {
yield mkdirp(r(this.dir, '.nuxt/dist')) yield mkdirp(r(this.dir, '.nuxt/dist'))
} }
// Resolve custom routes component path
this.options.routes.forEach((route) => {
if (route.component.slice(-4) !== '.vue') {
route.component = route.component + '.vue'
}
route.component = r(this.dir, route.component)
})
// Generate routes and interpret the template files
yield generateRoutesAndFiles.call(this)
/*
** Generate .nuxt/dist/ files
*/
if (this.dev) {
debug('Adding webpack middlewares...')
createWebpackMiddlewares.call(this)
webpackWatchAndUpdate.call(this)
watchPages.call(this)
} else {
debug('Building files...')
yield [
webpackRunClient.call(this),
webpackRunServer.call(this)
]
}
}
function * generateRoutesAndFiles () {
debug('Generating routes...')
/* /*
** Generate routes based on files ** Generate routes based on files
*/ */
@ -94,24 +128,14 @@ module.exports = function * () {
files.forEach((file) => { files.forEach((file) => {
let path = file.replace(/^pages/, '').replace(/index\.vue$/, '/').replace(/\.vue$/, '').replace(/\/{2,}/g, '/') let path = file.replace(/^pages/, '').replace(/index\.vue$/, '/').replace(/\.vue$/, '').replace(/\/{2,}/g, '/')
if (path[1] === '_') return if (path[1] === '_') return
routes.push({ path: path, component: file }) routes.push({ path: path, component: r(this.dir, file) })
})
this.options.routes.forEach((route) => {
if (route.component.slice(-4) !== '.vue') {
route.component = route.component + '.vue'
}
route.component = r(this.dir, route.component)
})
this.options.routes = routes.concat(this.options.routes)
// TODO: check .children
this.options.routes.forEach((route) => {
route._component = r(this.dir, route.component)
route._name = '_' + hash(route._component)
route.component = route._name
}) })
// Concat pages routes and custom routes in this.routes
this.routes = routes.concat(this.options.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',
@ -131,8 +155,15 @@ module.exports = function * () {
Loading: r(__dirname, '..', 'app', 'components', 'Loading.vue'), Loading: r(__dirname, '..', 'app', 'components', 'Loading.vue'),
ErrorPage: r(__dirname, '..', '..', 'pages', (this.dev ? '_error-debug.vue' : '_error.vue')) ErrorPage: r(__dirname, '..', '..', 'pages', (this.dev ? '_error-debug.vue' : '_error.vue'))
}, },
routes: this.options.routes routes: this.routes
} }
// Format routes for the lib/app/router.js template
// TODO: check .children
templateVars.routes.forEach((route) => {
route._component = route.component
route._name = '_' + hash(route._component)
route.component = route._name
})
if (this.options.store) { if (this.options.store) {
templateVars.storePath = r(this.dir, 'store') templateVars.storePath = r(this.dir, 'store')
} }
@ -153,21 +184,6 @@ module.exports = function * () {
}) })
}) })
yield moveTemplates yield moveTemplates
debug('Files moved!')
/*
** Generate .nuxt/dist/ files
*/
if (this.dev) {
debug('Adding webpack middlewares...')
createWebpackMiddlewares.call(this)
webpackWatchAndUpdate.call(this)
} else {
debug('Building files...')
yield [
webpackRunClient.call(this),
webpackRunServer.call(this)
]
}
} }
function getWebpackClientConfig () { function getWebpackClientConfig () {
@ -183,7 +199,7 @@ function getWebpackServerConfig () {
function createWebpackMiddlewares () { function createWebpackMiddlewares () {
const clientConfig = getWebpackClientConfig.call(this) const clientConfig = getWebpackClientConfig.call(this)
// setup on the fly compilation + hot-reload // setup on the fly compilation + hot-reload
clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app] clientConfig.entry.app = ['webpack-hot-middleware/client?reload=true', clientConfig.entry.app]
clientConfig.plugins.push( clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin() new webpack.NoErrorsPlugin()
@ -261,3 +277,22 @@ function createRenderer (bundle) {
this.renderToString = pify(this.renderer.renderToString) this.renderToString = pify(this.renderer.renderToString)
this.renderToStream = this.renderer.renderToStream this.renderToStream = this.renderer.renderToStream
} }
function watchPages () {
const patterns = [ r(this.dir, 'pages/*.vue'), r(this.dir, 'pages/**/*.vue') ]
const options = {
ignored: '**/_*.vue',
ignoreInitial: true
}
const refreshFiles = _.debounce(() => {
console.log('Reload files', this.routes.length)
var d = Date.now()
co(generateRoutesAndFiles.bind(this))
.then(() => {
console.log('Time to gen:' + (Date.now() - d) + 'ms')
})
}, 200)
this.pagesFilesWatcher = chokidar.watch(patterns, options)
.on('add', refreshFiles)
.on('unlink', refreshFiles)
}

View File

@ -1,3 +1,5 @@
'use strict'
const vueLoaderConfig = require('./vue-loader.config') const vueLoaderConfig = require('./vue-loader.config')
const { join } = require('path') const { join } = require('path')

View File

@ -1,3 +1,5 @@
'use strict'
const webpack = require('webpack') const webpack = require('webpack')
const ExtractTextPlugin = require('extract-text-webpack-plugin') const ExtractTextPlugin = require('extract-text-webpack-plugin')
const base = require('./base.config') const base = require('./base.config')

View File

@ -1,3 +1,5 @@
'use strict'
const webpack = require('webpack') const webpack = require('webpack')
const base = require('./base.config') const base = require('./base.config')
const { uniq } = require('lodash') const { uniq } = require('lodash')

View File

@ -1,3 +1,5 @@
'use strict'
module.exports = function () { module.exports = function () {
let config = { let config = {
postcss: [ postcss: [

View File

@ -45,8 +45,11 @@ class Nuxt {
}) })
// 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 /
this.serveStatic = pify(serveStatic(resolve(this.dir, 'static')))
// For serving .nuxt/dist/ files // For serving .nuxt/dist/ files
this.serveStatic = pify(serveStatic(resolve(this.dir, '.nuxt', 'dist'))) this._nuxtRegexp = /^\/_nuxt\//
this.serveStaticNuxt = pify(serveStatic(resolve(this.dir, '.nuxt', 'dist')))
// Add this.build // Add this.build
this.build = build.bind(this) this.build = build.bind(this)
// Add this.generate // Add this.generate
@ -77,25 +80,27 @@ class Nuxt {
// Call webpack middlewares only in development // Call webpack middlewares only in development
yield self.webpackDevMiddleware(req, res) yield self.webpackDevMiddleware(req, res)
yield self.webpackHotMiddleware(req, res) yield self.webpackHotMiddleware(req, res)
return
} }
if (req.url.includes('/_nuxt/')) { // Serve static/ files
yield self.serveStatic(req, res)
// Serve .nuxt/dist/ files (only for production)
if (!self.dev && self._nuxtRegexp.test(req.url)) {
const url = req.url const url = req.url
req.url = req.url.replace('/_nuxt/', '/') req.url = req.url.replace(self._nuxtRegexp, '/')
yield self.serveStatic(req, res) yield self.serveStaticNuxt(req, res)
req.url = url req.url = url
} }
}) })
.then(() => { .then(() => {
if (this.dev && req.url.includes('/_nuxt/') && req.url.includes('.hot-update.json')) { if (this.dev && this._nuxtRegexp.test(req.url) && req.url.includes('.hot-update.json')) {
res.statusCode = 404 res.statusCode = 404
return res.end() return res.end()
} }
return this.renderRoute(req.url, context) return this.renderRoute(req.url, context)
}) })
.then((html) => { .then(({ html, error }) => {
if (context.nuxt.error && context.nuxt.error.statusCode) { if (error) {
res.statusCode = context.nuxt.error.statusCode res.statusCode = context.nuxt.error.statusCode || 500
} }
res.end(html, 'utf8') res.end(html, 'utf8')
}) })
@ -113,13 +118,13 @@ class Nuxt {
// 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 * () {
const html = yield self.renderToString(context) const app = yield self.renderToString(context)
if (context.nuxt && context.nuxt.error instanceof Error) { if (context.nuxt && context.nuxt.error instanceof Error) {
context.nuxt.error = { statusCode: 500, message: context.nuxt.error.message } context.nuxt.error = { statusCode: 500, message: context.nuxt.error.message }
} }
const app = self.appTemplate({ const html = self.appTemplate({
dev: self.dev, // Use to add the extracted CSS <link> in production dev: self.dev, // Use to add the extracted CSS <link> in production
APP: html, APP: app,
context: context, context: context,
files: { files: {
css: join('/_nuxt/', self.options.build.filenames.css), css: join('/_nuxt/', self.options.build.filenames.css),
@ -127,11 +132,11 @@ class Nuxt {
app: join('/_nuxt/', self.options.build.filenames.app) app: join('/_nuxt/', self.options.build.filenames.app)
} }
}) })
return app return { html, error: context.nuxt.error }
}) })
} }
stop (callback) { close (callback) {
let promises = [] let promises = []
if (this.webpackDevMiddleware) { if (this.webpackDevMiddleware) {
const p = new Promise((resolve, reject) => { const p = new Promise((resolve, reject) => {
@ -145,6 +150,9 @@ class Nuxt {
}) })
promises.push(p) promises.push(p)
} }
if (this.pagesFilesWatcher) {
this.pagesFilesWatcher.close()
}
return co(function * () { return co(function * () {
yield promises yield promises
}) })

View File

@ -1,41 +1,14 @@
'use strict' 'use strict'
const http = require('http') const http = require('http')
const co = require('co')
const pify = require('pify')
const serveStatic = require('serve-static')
const { resolve } = require('path')
class Server { class Server {
constructor (nuxt) { constructor (nuxt) {
this.server = http.createServer(this.handle.bind(this)) this.server = http.createServer(nuxt.render.bind(nuxt))
this.serveStatic = pify(serveStatic(resolve(nuxt.dir, 'static')))
this.nuxt = nuxt
return this return this
} }
handle (req, res) {
const method = req.method.toUpperCase()
const self = this
if (method !== 'GET' && method !== 'HEAD') {
return this.nuxt.render(req, res)
}
co(function * () {
if (req.url.includes('/static/')) {
const url = req.url
req.url = req.url.replace('/static/', '/')
yield self.serveStatic(req, res)
req.url = url
}
})
.then(() => {
// File not found
this.nuxt.render(req, res)
})
}
listen (port, host) { listen (port, host) {
host = host || 'localhost' host = host || 'localhost'
port = port || 3000 port = port || 3000

View File

@ -1,6 +1,6 @@
{ {
"name": "nuxt", "name": "nuxt",
"version": "0.2.6", "version": "0.3.1",
"description": "A minimalistic framework for server-rendered Vue.js applications (inspired by Next.js)", "description": "A minimalistic framework for server-rendered Vue.js applications (inspired by Next.js)",
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
@ -22,6 +22,7 @@
"babel-loader": "^6.2.7", "babel-loader": "^6.2.7",
"babel-preset-es2015": "^6.18.0", "babel-preset-es2015": "^6.18.0",
"babel-preset-stage-2": "^6.18.0", "babel-preset-stage-2": "^6.18.0",
"chokidar": "^1.6.1",
"co": "^4.6.0", "co": "^4.6.0",
"cross-spawn": "^5.0.1", "cross-spawn": "^5.0.1",
"css-loader": "^0.25.0", "css-loader": "^0.25.0",