Prototype 0.1.0 working

Alpha 0.1.0
This commit is contained in:
Sébastien Chopin 2016-11-07 02:34:58 +01:00
parent 6cce8b161a
commit 8ab135af55
45 changed files with 1788 additions and 68 deletions

27
.eslintrc.js Normal file
View File

@ -0,0 +1,27 @@
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
env: {
browser: true,
node: true,
mocha: true
},
extends: 'standard',
// required to lint *.vue files
plugins: [
'html'
],
// add your custom rules here
rules: {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
},
globals: {}
}

3
.gitignore vendored
View File

@ -1,6 +1,3 @@
# build output
dist
# dependencies
yarn.lock
node_modules

View File

@ -47,5 +47,6 @@ So far, we get:
- Hot code reloading
- Server rendering and indexing of `./pages`
- Static file serving. `./static/` is mapped to `/static/`
- Config file nuxt.config.js
To see how simple this is, check out the [sample app - nuxtgram](https://github.com/atinux/nuxtgram)

29
bin/nuxt Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env node --harmony_proxies
const { join } = require('path')
const { spawn } = require('cross-spawn')
const defaultCommand = 'start'
const commands = new Set([
defaultCommand,
'init'
])
let cmd = process.argv[2]
let args
if (commands.has(cmd)) {
args = process.argv.slice(3)
} else {
cmd = defaultCommand
args = process.argv.slice(2)
}
const bin = join(__dirname, 'nuxt-' + cmd)
const proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] })
proc.on('close', (code) => process.exit(code))
proc.on('error', (err) => {
console.error(err)
process.exit(1)
})

81
bin/nuxt-init Executable file
View File

@ -0,0 +1,81 @@
#!/usr/bin/env node --harmony_proxies
const co = require('co')
const mkdirp = require('mkdirp-then')
const pify = require('pify')
const { resolve, join, basename } = require('path')
const { existsSync, writeFile } = require('fs')
const rootDir = resolve(process.argv.slice(2)[0] || '.')
if (basename(rootDir) === 'pages') {
console.warn('Your root directory is named "pages". This looks suspicious. You probably want to go one directory up.')
process.exit(0)
}
co(function * () {
yield new Promise((resolve) => setTimeout(resolve, 0)) // avoid undefined variables basePackage, etc.
if (!existsSync(rootDir)) {
yield mkdirp(rootDir)
}
if (!existsSync(join(rootDir, 'package.json'))) {
yield pify(writeFile)(join(rootDir, 'package.json'), basePackage.replace(/my-app/g, basename(rootDir)))
}
if (!existsSync(join(rootDir, 'nuxt.config.js'))) {
yield pify(writeFile)(join(rootDir, 'nuxt.config.js'), baseConfig)
}
if (!existsSync(join(rootDir, 'static'))) {
yield mkdirp(join(rootDir, 'static'))
}
if (!existsSync(join(rootDir, 'pages'))) {
yield mkdirp(join(rootDir, 'pages'))
yield pify(writeFile)(join(rootDir, 'pages', 'index.vue'), basePage)
}
})
.then(() => {
console.log('Nuxt project [' + basename(rootDir) + '] created')
})
.catch((err) => {
console.error(err)
process.exit(1)
})
const basePackage = `{
"name": "my-app",
"description": "",
"dependencies": {
"nuxt": "latest"
},
"scripts": {
"start": "nuxt"
}
}
`
const baseConfig = `module.exports = {
// Nuxt.js configuration file
// Please look at https://nuxtjs.org/docs/config-file
}
`
const basePage = `
<template>
<p>Hello {{ name }}!</p>
</template>
<script>
export default {
data () {
return { name: 'world' }
}
}
</script>
<style>
p {
font-size: 20px;
text-align: center;
padding: 100px;
}
</style>
`

70
bin/nuxt-start Executable file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env node --harmony_proxies
const http = require('http')
const fs = require('fs')
const serveStatic = require('serve-static')
const Nuxt = require('../')
const { resolve } = require('path')
const rootDir = resolve(process.argv.slice(2)[0] || '.')
const nuxtConfigFile = resolve(rootDir, 'nuxt.config.js')
let options = {}
if (fs.existsSync(nuxtConfigFile)) {
options = require(nuxtConfigFile)
}
if (typeof options.rootDir !== 'string') {
options.rootDir = rootDir
}
new Nuxt(options)
.then((nuxt) => {
new Server(nuxt)
.listen(process.env.PORT, process.env.HOST)
})
.catch((err) => {
console.error(err)
process.exit()
})
class Server {
constructor (nuxt) {
this.server = http.createServer(this.handle.bind(this))
this.staticServer = serveStatic('static', { fallthrough: false })
this.nuxt = nuxt
return this
}
handle (req, res) {
const method = req.method.toUpperCase()
if (method !== 'GET' && method !== 'HEAD') {
return this.nuxt.render(req, res)
}
this._staticHandler(req, res)
.catch((e) => {
// File not found
this.nuxt.render(req, res)
})
}
listen (port, host) {
host = host || 'localhost'
port = port || 3000
this.server.listen(port, host, () => {
console.log('Ready on http://%s:%s', host, port)
})
}
_staticHandler (req, res) {
return new Promise((resolve, reject) => {
this.staticServer(req, res, (error) => {
if (!error) {
return resolve()
}
error.message = `Route ${error.message} while resolving ${req.url}`
reject(error)
})
})
}
}

View File

@ -0,0 +1,18 @@
<template>
<div class="basic-css">
<p>Hello World</p>
</div>
</template>
<style>
.basic-css {
font: 15px Helvetica, Arial, sans-serif;
background: #eee;
padding: 100px;
text-align: center;
transition: 100ms ease-in background;
}
.basic-css:hover {
background: #ccc
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<h1>This page has a title 🤔</h1>
</template>
<script>
export default {
metaInfo: {
title: 'This page has a title 🤔',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
]
}
}
</script>

View File

@ -0,0 +1,3 @@
<template>
<div>About us</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div>Hello World. <router-link to='/about'>About</router-link></div>
</template>

View File

@ -0,0 +1,12 @@
<template>
<p class="p">
<slot></slot>
</p>
</template>
<style scoped>
.p {
font: 13px Helvetica, Arial;
margin: 10px 0;
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<div class="main">
<h1 class="title">{{ title }}</h1>
<slot></slot>
</div>
</template>
<script>
export default {
props: ['title']
}
</script>
<style scoped>
.main {
font: 15px Helvetica, Arial;
border: 1px solid #eee;
padding: 0 10px;
}
.title {
font-size: 16px;
font-weight: bold;
margin: 10px 0
}
</style>

View File

@ -0,0 +1,54 @@
<template lang="html">
<div class="main">
<post title="My first blog post">
<v-p>Hello there</v-p>
<v-p>This is an example of a componentized blog post</v-p>
</post>
<v-hr/>
<post title="My second blog post">
<v-p>Hello there</v-p>
<v-p>This is another example.</v-p>
<v-p>Wa-hoo!</v-p>
</post>
<v-hr/>
<post title="The final blog post">
<v-p>C'est la fin !</v-p>
</post>
</div>
</template>
<script>
import Post from '../components/post.vue'
import vP from '../components/paragraph.vue'
const vHr = { render: (h) => h('hr', { class: 'hr' }) }
export default {
components: {
Post,
vP,
vHr
}
}
</script>
<style scoped>
.main {
margin: auto;
max-width: 420px;
padding: 10px;
}
.hr {
width: 100px;
border-width: 0;
margin: 20px auto;
text-align: center;
}
hr::before {
content: '***';
color: #ccc;
}
</style>

25
examples/with-ava/Readme.md Executable file
View File

@ -0,0 +1,25 @@
## Add testing to your `nuxt` app using `ava` and `jsdom`
[`ava`](https://github.com/avajs/ava) is a powerful JavaScript testing framework, mixed with [`jsdom`](https://github.com/tmpvar/jsdom), we can use them to do end-to-end testing easily for `nuxt` applications.
```bash
npm install --save-dev ava jsdom
```
Add test script to the `package.json`
__package.json__
```javascript
// ...
"scripts": {
"test": "ava",
}
// ...
```
Launch the tests:
```bash
npm test
```

11
examples/with-ava/package.json Executable file
View File

@ -0,0 +1,11 @@
{
"name": "ava-tests",
"scripts": {
"start": "../../bin/nuxt .",
"test": "ava"
},
"devDependencies": {
"ava": "^0.16.0",
"jsdom": "^9.8.3"
}
}

View File

@ -0,0 +1,19 @@
<template>
<div>
<p class="red-color">Hello {{ name }}!</p>
</div>
</template>
<script>
export default {
data () {
return { name: 'world' }
}
}
</script>
<style>
.red-color {
color: red;
}
</style>

View File

@ -0,0 +1,77 @@
/*
** Test with Ava can be written in ES6 \o/
*/
import test from 'ava'
import jsdom from 'jsdom'
import { createServer } from 'http'
import { resolve } from 'path'
let nuxt = null
let server = null
// Init nuxt.js and create server listening on localhost:4000
test.before('Init nuxt.js', (t) => {
process.env.NODE_ENV = 'test'
const Nuxt = require('../../../')
const options = {
rootDir: resolve(__dirname, '..')
}
return new Nuxt(options)
.then(function (_nuxt) {
nuxt = _nuxt
server = createServer((req, res) => nuxt.render(req, res))
return new Promise((resolve, reject) => {
server.listen(4000, 'localhost', () => {
resolve()
})
})
})
})
// Function used to do dom checking via jsdom
async function renderAndGetWindow (route) {
return new Promise((resolve, reject) => {
const url = 'http://localhost:4000' + route
jsdom.env({
url: url,
features: {
FetchExternalResources: ['script', 'link'],
ProcessExternalResources: ['script']
},
done (err, window) {
if (err) return reject(err)
// Used by nuxt.js to say when the components are loaded and the app ready
window.onNuxtReady = function () {
resolve(window)
}
}
})
})
}
/*
** Example of testing only the html
*/
test('Route / exits and render HTML', async t => {
let context = {}
const html = await nuxt.renderRoute('/', context)
t.true(html.includes('<p class="red-color">Hello world!</p>'))
t.is(context.nuxt.error, null)
t.is(context.nuxt.data[0].name, 'world')
})
/*
** Example of testing via dom checking
*/
test('Route / exits and render HTML', async t => {
const window = await renderAndGetWindow('/')
t.is(window.document.querySelector('p').textContent, 'Hello world!')
t.is(window.document.querySelector('p').className, 'red-color')
t.true(window.document.querySelectorAll('style')[2].textContent.includes('.red-color {\n color: red;\n}'))
})
// Close server and ask nuxt to stop listening to file changes
test.after('Closing server and nuxt.js', t => {
server.close()
nuxt.stop()
})

8
index.js Normal file
View File

@ -0,0 +1,8 @@
/*!
* nuxt.js
* MIT Licensed
*/
'use strict'
module.exports = require('./lib/nuxt')

40
lib/app/App.vue Normal file
View File

@ -0,0 +1,40 @@
<template>
<div id="app">
<% if (loading) { %><loading ref="loading"></loading><% } %>
<router-view v-if="!err"></router-view>
<error-page v-if="err" :error="err"></error-page>
</div>
</template>
<script>
import ErrorPage from '<%= components.ErrorPage %>'
<% if (loading) { %>import Loading from '<%= (typeof loading === "string" ? loading : "./components/Loading.vue") %>'<% } %>
export default {
data () {
return {
err: null
}
},
<% if (loading) { %>
mounted () {
this.$loading = this.$refs.loading
},
<% } %>
methods: {
error (err) {
err = err || null
this.err = err || null
<% if (loading) { %>
if (this.err && this.$loading) {
this.$loading.fail && this.$loading.fail()
}
<% } %>
return this.err
}
},
components: {
ErrorPage
}
}
</script>

179
lib/app/client.js Normal file
View File

@ -0,0 +1,179 @@
require('es6-promise').polyfill()
require('es6-object-assign').polyfill()
import Vue from 'vue'
import { app, router<%= (store ? ', store' : '') %> } from './index'
import { getMatchedComponents, flatMapComponents, getContext, getLocation } from './utils'
const noopData = () => { return {} }
const noopFetch = () => {}
function loadAsyncComponents (to, from, next) {
const resolveComponents = flatMapComponents(to, (Component, _, match, key) => {
if (typeof Component === 'function' && !Component.options) {
return new Promise(function (resolve, reject) {
const _resolve = (Component) => {
// console.log('Component loaded', Component, match.path, key)
match.components[key] = Component
resolve(Component)
}
Component().then(_resolve).catch(reject)
})
}
// console.log('Return Component', match)
return Component
})
<%= (loading ? 'this.$loading.start && this.$loading.start()' : '') %>
Promise.all(resolveComponents)
.then(() => next())
.catch((err) => {
this.error({ statusCode: 500, message: err.message })
next(false)
})
}
function render (to, from, next) {
let Components = getMatchedComponents(to)
if (!Components.length) {
this.error({ statusCode: 404, message: 'This page could not be found.', url: to.path })
return next()
}
// console.log('Load components', Components, to.path)
// Update ._data and other properties if hot reloaded
Components.forEach(function (Component) {
if (!Component._data) {
Component._data = Component.data || noopData
}
if (Component._Ctor && Component._Ctor.options) {
Component.fetch = Component._Ctor.options.fetch
const originalDataFn = Component._data.toString().replace(/\s/g, '')
const dataFn = (Component.data || noopData).toString().replace(/\s/g, '')
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.error()
Promise.all(Components.map((Component) => {
let promises = []
const context = getContext({ to<%= (store ? ', store' : '') %>, isClient: true })
if (Component._data && typeof Component._data === 'function') {
var promise = Component._data(context)
if (!(promise instanceof Promise)) promise = Promise.resolve(promise)
promise.then((data) => {
Component.data = () => data
if (Component._Ctor && Component._Ctor.options) {
Component._Ctor.options.data = Component.data
}
<%= (loading ? 'this.$loading.start && this.$loading.increase(30)' : '') %>
})
promises.push(promise)
}
if (Component.fetch) {
var p = Component.fetch(context)
<%= (loading ? 'p.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %>
promises.push(p)
}
return Promise.all(promises)
}))
.then(() => {
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
next()
})
.catch(function (error) {
this.error(error)
next(false)
})
}
// Special hot reload with data(context)
function hotReloadAPI (_app) {
var _forceUpdate = _app.$forceUpdate.bind(_app)
_app.$forceUpdate = function () {
let Component = getMatchedComponents(router.currentRoute)[0]
if (!Component) return _forceUpdate()
<%= (loading ? 'this.$loading.start && this.$loading.start()' : '') %>
let promises = []
const context = getContext({ route: router.currentRoute<%= (store ? ', store' : '') %>, isClient: true })
// 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
let p = Component._data(context)
if (!(p instanceof Promise)) { p = Promise.resolve(p) }
p.then((data) => {
Component.data = () => data
Component._Ctor.options.data = Component.data
<%= (loading ? 'this.$loading.increase && this.$loading.increase(30)' : '') %>
})
promises.push(p)
}
// Check if fetch has been updated
const originalFetchFn = (Component.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.fetch = Component._Ctor.options.fetch
let p = Component.fetch(context)
if (!(p instanceof Promise)) { p = Promise.resolve(p) }
<%= (loading ? 'p.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %>
promises.push(p)
}
return Promise.all(promises).then(() => {
<%= (loading ? 'this.$loading.finish && this.$loading.finish(30)' : '') %>
_forceUpdate()
})
}
}
// Load vue app
const NUXT = window.__NUXT__ || {}
if (!NUXT) {
throw new Error('[nuxt.js] cannot find the global variable __NUXT__, make sure the server is working.')
}
<% if (store) { %>
// Replace store state
if (NUXT.state) {
store.replaceState(NUXT.state)
}
<% } %>
// Get matched components
const path = getLocation(router.options.base)
const resolveComponents = flatMapComponents(router.match(path), (Component, _, match, key, index) => {
if (typeof Component === 'function' && !Component.options) {
return new Promise(function (resolve, reject) {
const _resolve = (Component) => {
if (Component.data && typeof Component.data === 'function') {
Component._data = Component.data
Component.data = () => NUXT.data[index]
if (Component._Ctor && Component._Ctor.options) {
Component._Ctor.options.data = Component.data
}
}
match.components[key] = Component
resolve(Component)
}
Component().then(_resolve).catch(reject)
})
}
return Component
})
Promise.all(resolveComponents)
.then((Components) => {
const _app = new Vue(app)
if (NUXT.error) _app.error(NUXT.error)
if (module.hot) hotReloadAPI(_app)
_app.$mount('#app')
// Add router hooks
router.beforeEach(loadAsyncComponents.bind(_app))
router.beforeEach(render.bind(_app))
// Call window.onModulesLoaded for jsdom testing (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading)
if (typeof window.onNuxtReady === 'function') {
window.onNuxtReady()
}
})
.catch((err) => {
console.error('[Nuxt.js] Cannot load components', err)
})

View File

@ -0,0 +1,31 @@
<template>
<p>LOADING...</p>
</template>
<script>
export default {
data () {
return {
duration: <%= loading.duration %>,
loadingColor: '<%= loading.loadingColor %>',
errorColor: '<%= loading.errorColor %>',
}
},
methods: {
start () {},
increase () {},
decrease () {},
set () {},
finish () {},
pause () {},
hide () {},
fail () {}
}
}
</script>
<style scoped>
p {
color: grey;
}
</style>

24
lib/app/index.js Normal file
View File

@ -0,0 +1,24 @@
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import router from './router'
<% if (store && storePath) { %>import store from '<%= storePath %>'<% } %>
// import VueProgressBar from './plugins/vue-progressbar'
// Vue.use(VueProgressBar, {
// color: '#efc14e',
// failedColor: 'red',
// height: '2px'
// })
import App from './App.vue'
// create the app instance.
// here we inject the router and store to all child components,
// making them available everywhere as `this.$router` and `this.$store`.
const app = {
router,
<%= (store ? 'store,' : '') %>
...App
}
export { app, router<%= (store ? ', store' : '') %> }

38
lib/app/router.js Normal file
View File

@ -0,0 +1,38 @@
import Vue from 'vue'
import Router from 'vue-router'
import Meta from 'vue-meta'
Vue.use(Router)
Vue.use(Meta)
<% routes.forEach(function (route) { %>
const <%= route._name %> = process.BROWSER ? () => System.import('<%= route._component %>') : require('<%= route._component %>')
<% }) %>
const scrollBehavior = (to, from, savedPosition) => {
if (savedPosition) {
// savedPosition is only available for popstate navigations.
return savedPosition
} else {
// Scroll to the top by default
let position = { x: 0, y: 0 }
// if link has anchor, scroll to anchor by returning the selector
if (to.hash) {
position = { selector: to.hash }
}
return position
}
}
export default new Router({
mode: 'history',
scrollBehavior,
routes: [
<% routes.forEach((route, i) => { %>
{
path: '<%= route.path %>',
component: <%= route._name %>
}<%= (i + 1 === routes.length ? '' : ',') %>
<% }) %>
]
})

70
lib/app/server.js Normal file
View File

@ -0,0 +1,70 @@
const debug = require('debug')('nuxt:render')
import Vue from 'vue'
import { pick } from 'lodash'
import { app, router<%= (store ? ', store' : '') %> } from './index'
import { getMatchedComponents, getContext } from './utils'
const isDev = process.env.NODE_ENV !== 'production'
const _app = new Vue(app)
// This exported function will be called by `bundleRenderer`.
// This is where we perform data-prefetching to determine the
// state of our application before actually rendering it.
// Since data fetching is async, this function is expected to
// return a Promise that resolves to the app instance.
export default context => {
// set router's location
router.push(context.url)
// Add route to the context
context.route = router.currentRoute
// Add meta infos
context.meta = _app.$meta()
// Add store to the context
<%= (store ? 'context.store = store' : '') %>
// Nuxt object
context.nuxt = { data: [], error: null<%= (store ? ', state: null' : '') %> }
<%= (isDev ? 'const s = isDev && Date.now()' : '') %>
// Call data & fecth hooks on components matched by the route.
let Components = getMatchedComponents(context.route)
if (!Components.length) {
context.nuxt.error = _app.error({ statusCode: 404, message: 'This page could not be found.', url: context.route.path })
<%= (store ? 'context.nuxt.state = store.state' : '') %>
return Promise.resolve(_app)
}
return Promise.all(Components.map((Component) => {
let promises = []
if (Component.data && typeof Component.data === 'function') {
Component._data = Component.data
var promise = Component.data(getContext(context))
if (!(promise instanceof Promise)) promise = Promise.resolve(promise)
promise.then((data) => {
Component.data = () => data
})
promises.push(promise)
} else {
promises.push(null)
}
if (Component.fetch) {
promises.push(Component.fetch(getContext(context)))
}
return Promise.all(promises)
}))
.then((res) => {
<% if (isDev) { %>
debug('Data fetch ' + context.req.url + ': ' + (Date.now() - s) + 'ms')
<% } %>
// datas are the first row of each
context.nuxt.data = res.map((tab) => tab[0])
<%= (store ? '// Add the state from the vuex store' : '') %>
<%= (store ? 'context.nuxt.state = store.state' : '') %>
return _app
})
.catch(function (error) {
context.nuxt.error = _app.error(error)
<%= (store ? 'context.nuxt.state = store.state' : '') %>
return _app
})
}

38
lib/app/utils.js Normal file
View File

@ -0,0 +1,38 @@
'use strict'
export function getMatchedComponents (route) {
return [].concat.apply([], route.matched.map(function (m) {
return Object.keys(m.components).map(function (key) {
return m.components[key]
})
}))
}
export function flatMapComponents (route, fn) {
return Array.prototype.concat.apply([], route.matched.map(function (m, index) {
return Object.keys(m.components).map(function (key) {
return fn(m.components[key], m.instances[key], m, key, index)
})
}))
}
export function getContext (context) {
let ctx = {
isServer: !!context.isServer,
isClient: !!context.isClient,
<%= (store ? 'store: context.store,' : '') %>
route: (context.to ? context.to : context.route)
}
if (context.req) ctx.req = context.req
if (context.res) ctx.req = context.res
return ctx
}
// Imported from vue-router
export function getLocation (base) {
var path = window.location.pathname
if (base && path.indexOf(base) === 0) {
path = path.slice(base.length)
}
return (path || '/') + window.location.search + window.location.hash
}

248
lib/build/index.js Normal file
View File

@ -0,0 +1,248 @@
'use strict'
const debug = require('debug')('nuxt:build')
const _ = require('lodash')
const del = require('del')
const fs = require('fs')
const glob = require('glob-promise')
const hash = require('hash-sum')
const mkdirp = require('mkdirp-then')
const pify = require('pify')
const webpack = require('webpack')
const { createBundleRenderer } = require('vue-server-renderer')
const { join, resolve } = require('path')
const r = resolve
module.exports = function * () {
/*
** Check if pages dir exists and warn if not
*/
if (!fs.existsSync(join(this.dir, 'pages'))) {
if (fs.existsSync(join(this.dir, '..', 'pages'))) {
console.error('> No `pages` directory found. Did you mean to run `next` in the parent (`../`) directory?')
} else {
console.error('> Couldn\'t find a `pages` directory. Please create one under the project root')
}
process.exit()
}
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')
process.exit()
}
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.')
process.exit()
}
debug(`App root: ${this.dir}`)
debug('Generating .nuxt/ files...')
/*
** Create .nuxt/, .nuxt/components and .nuxt/dist folders
*/
yield del(r(this.dir, '.nuxt'), { force: process.env.NODE_ENV === 'test' })
yield mkdirp(r(this.dir, '.nuxt/components'))
if (this.isProd) {
yield mkdirp(r(this.dir, '.nuxt/dist'))
}
/*
** Generate routes based on files
*/
const files = yield glob('pages/**/*.vue', { cwd: this.dir })
let routes = []
files.forEach((file) => {
let path = file.replace(/^pages/, '').replace(/index\.vue$/, '/').replace(/\.vue$/, '').replace(/\/{2,}/g, '/')
if (path[1] === '_') return
routes.push({ path: path, component: file })
})
this.options.routes.forEach((route) => {
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
})
/*
** Interpret and move template files to .nuxt/
*/
let templatesFiles = [
'App.vue',
'client.js',
'index.js',
'router.js',
'server.js',
'utils.js',
'components/Loading.vue'
]
let templateVars = {
isDev: this.isDev,
store: this.options.store,
loading: (this.options.loading === 'string' ? r(this.dir, this.options.loading) : this.options.loading),
components: {
Loading: r(__dirname, '..', 'app', 'components', 'Loading.vue'),
ErrorPage: r(__dirname, '..', '..', 'pages', (this.isDev ? '_error-debug.vue' : '_error.vue'))
},
routes: this.options.routes
}
if (this.options.store) {
templateVars.storePath = r(this.dir, 'store')
}
if (this.isDev && files.includes('pages/_error-debug.vue')) {
templateVars.components.ErrorPage = r(this.dir, 'pages/_error-debug.vue')
}
if (!this.isDev && files.includes('pages/_error.vue')) {
templateVars.components.ErrorPage = r(this.dir, 'pages/_error.vue')
}
const readFile = pify(fs.readFile)
const writeFile = pify(fs.writeFile)
let moveTemplates = templatesFiles.map((file) => {
return readFile(r(__dirname, '..', 'app', file), 'utf8')
.then((fileContent) => {
const template = _.template(fileContent)
const content = template(templateVars)
return writeFile(r(this.dir, '.nuxt', file), content, 'utf8')
})
})
yield moveTemplates
debug('Files moved!')
/*
** Generate .nuxt/dist/ files
*/
if (this.isDev) {
debug('Adding webpack middlewares...')
createWebpackMiddlewares.call(this)
webpackWatchAndUpdate.call(this)
} else {
debug('Building files...')
yield [
webpackRunClient.call(this),
webpackRunServer.call(this)
]
}
return this
}
function getWebpackClientConfig () {
var config = require(r(__dirname, 'webpack', 'client.config.js'))
// Entry
config.entry.app = r(this.dir, '.nuxt', 'client.js')
// Add vendors
if (this.options.store) config.entry.vendor.push('vuex')
config.entry.vendor = config.entry.vendor.concat(this.options.vendor)
// extract vendor chunks for better caching
config.plugins.push(
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: this.options.filenames.vendor
})
)
// Output
config.output.path = r(this.dir, '.nuxt', 'dist')
config.output.filename = this.options.filenames.app
// Extract text plugin
if (this.isProd) {
const ExtractTextPlugin = require('extract-text-webpack-plugin')
let plugin = config.plugins.find((plugin) => plugin instanceof ExtractTextPlugin)
if (plugin) plugin.filename = this.options.filenames.css
}
return config
}
function getWebpackServerConfig () {
var config = require(r(__dirname, 'webpack', 'server.config.js'))
// Entry
config.entry = r(this.dir, '.nuxt', 'server.js')
// Output
config.output.path = r(this.dir, '.nuxt', 'dist')
// Externals
config.externals = Object.keys(require(r(__dirname, '..', '..', 'package.json')).dependencies || {})
const projectPackageJson = r(this.dir, 'package.json')
if (fs.existsSync(projectPackageJson)) {
config.externals = [].concat(Object.keys(require(r(this.dir, 'package.json')).dependencies || {}))
}
config.externals = _.uniq(config.externals)
return config
}
function createWebpackMiddlewares () {
const clientConfig = getWebpackClientConfig.call(this)
// setup on the fly compilation + hot-reload
clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
)
const clientCompiler = webpack(clientConfig)
// Add the middlewares to the instance context
this.webpackDevMiddleware = pify(require('webpack-dev-middleware')(clientCompiler, {
publicPath: clientConfig.output.publicPath,
stats: {
colors: true,
chunks: false
},
quiet: true,
noInfo: true
}))
this.webpackHotMiddleware = pify(require('webpack-hot-middleware')(clientCompiler))
}
function webpackWatchAndUpdate () {
const MFS = require('memory-fs') // <- dependencies of webpack
const mfs = new MFS()
const serverConfig = getWebpackServerConfig.call(this)
const serverCompiler = webpack(serverConfig)
const outputPath = join(serverConfig.output.path, serverConfig.output.filename)
serverCompiler.outputFileSystem = mfs
this.webpackServerWatcher = serverCompiler.watch({}, (err, stats) => {
if (err) throw err
stats = stats.toJson()
stats.errors.forEach(err => console.error(err))
stats.warnings.forEach(err => console.warn(err))
createRenderer.call(this, 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) => {
if (err) return reject(err)
debug('[webpack:build:client]\n', stats.toString({ chunks: false, colors: true }))
resolve()
})
})
}
function webpackRunServer () {
return new Promise((resolve, reject) => {
const serverConfig = getWebpackServerConfig.call(this)
const serverCompiler = webpack(serverConfig)
serverCompiler.run((err, stats) => {
if (err) return reject(err)
debug('[webpack:build:server]\n', stats.toString({ chunks: false, colors: true }))
const bundlePath = join(serverConfig.output.path, serverConfig.output.filename)
createRenderer.call(this, fs.readFileSync(bundlePath, 'utf8'))
resolve()
})
})
}
function createRenderer (bundle) {
process.env.VUE_ENV = (process.env.VUE_ENV ? process.env.VUE_ENV : 'server')
// Create bundle renderer to give a fresh context for every request
let cacheConfig = false
if (this.options.cache) {
this.options.cache = (typeof this.options.cache !== 'object' ? {} : this.options.cache)
cacheConfig = require('lru-cache')(_.defaults(this.options.cache, {
max: 1000,
maxAge: 1000 * 60 * 15
}))
}
this.renderer = createBundleRenderer(bundle, {
cache: cacheConfig
})
this.renderToString = pify(this.renderer.renderToString)
this.renderToStream = this.renderer.renderToStream
}

View File

@ -0,0 +1,52 @@
const vueLoaderConfig = require('./vue-loader.config')
/*
|--------------------------------------------------------------------------
| Webpack Shared Config
|
| This is the config which is extented by the server and client
| webpack config files
|--------------------------------------------------------------------------
*/
module.exports = {
devtool: 'source-map',
entry: {
vendor: ['vue', 'vue-router', 'vue-meta', 'es6-promise', 'es6-object-assign']
},
output: {
publicPath: '/_nuxt/'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules/,
options: {
presets: ['es2015', 'stage-2']
}
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'url',
options: {
limit: 1000, // 1KO
name: 'img/[name].[ext]?[hash]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url',
query: {
limit: 1000, // 1 KO
name: 'fonts/[name].[hash:7].[ext]'
}
}
]
}
}

View File

@ -0,0 +1,55 @@
const webpack = require('webpack')
const base = require('./base.config')
const vueConfig = require('./vue-loader.config')
/*
|--------------------------------------------------------------------------
| Webpack Client Config
|
| Generate public/dist/client-vendor-bundle.js
| Generate public/dist/client-bundle.js
|
| In production, will generate public/dist/style.css
|--------------------------------------------------------------------------
*/
const config = Object.assign({}, base, {
plugins: (base.plugins || []).concat([
// strip comments in Vue code
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.BROWSER': true
})
])
})
if (process.env.NODE_ENV === 'production') {
// Use ExtractTextPlugin to extract CSS into a single file
// so it's applied on initial render
const ExtractTextPlugin = require('extract-text-webpack-plugin')
// vueConfig is already included in the config via LoaderOptionsPlugin
// here we overwrite the loader config for <style lang='stylus'>
// so they are extracted.
vueConfig.loaders.css = ExtractTextPlugin.extract({ loader: 'css-loader' })
vueConfig.loaders.scss = ExtractTextPlugin.extract({ loader: 'css-loader!sass-loader', fallbackLoader: 'vue-style-loader' })
vueConfig.loaders.sass = ExtractTextPlugin.extract({ loader: 'css-loader!sass-loader?indentedSyntax', fallbackLoader: 'vue-style-loader' })
vueConfig.loaders.stylus = ExtractTextPlugin.extract({ loader: 'css-loader!stylus-loader', fallbackLoader: 'vue-style-loader' })
vueConfig.loaders.less = ExtractTextPlugin.extract({ loader: 'css-loader!less-loader', fallbackLoader: 'vue-style-loader' })
config.plugins.push(
new ExtractTextPlugin('style.css'),
// this is needed in webpack 2 for minifying CSS
new webpack.LoaderOptionsPlugin({
minimize: true
}),
// minify JS
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
)
}
module.exports = config

View File

@ -0,0 +1,23 @@
const webpack = require('webpack')
const base = require('./base.config')
/*
|--------------------------------------------------------------------------
| Webpack Server Config
|--------------------------------------------------------------------------
*/
module.exports = Object.assign({}, base, {
target: 'node',
devtool: false,
output: Object.assign({}, base.output, {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
}),
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"',
'process.BROWSER': false
})
]
})

View File

@ -0,0 +1,16 @@
module.exports = {
postcss: [
require('autoprefixer')({
browsers: ['last 3 versions']
})
],
loaders: {
'js': 'babel-loader?presets[]=es2015&presets[]=stage-2',
'postcss': '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',
'stylus': 'vue-style-loader!css-loader!stylus-loader',
'styl': 'vue-style-loader!css-loader!stylus-loader'
}
}

155
lib/nuxt.js Normal file
View File

@ -0,0 +1,155 @@
'use strict'
const debug = require('debug')('nuxt:render')
const _ = require('lodash')
const co = require('co')
const fs = require('fs')
const pify = require('pify')
const ansiHTML = require('ansi-html')
const serialize = require('serialize-javascript')
const build = require('./build')
const serveStatic = require('serve-static')
const { resolve, join } = require('path')
const { encodeHtml, getContext, setAnsiColors } = require('./utils')
setAnsiColors(ansiHTML)
class Nuxt {
constructor (options = {}, cb) {
var defaults = {
filenames: {
css: 'style.css',
vendor: 'vendor.bundle.js',
app: 'nuxt.bundle.js'
},
routes: [],
vendor: [],
css: [],
store: false,
cache: false,
loading: {
loadingColor: 'black',
errorColor: 'red',
duration: 5000
}
}
if (options.loading === true) delete options.loading
this.options = _.defaultsDeep(options, defaults)
// Env variables
this.isProd = process.env.NODE_ENV === 'production'
this.isDev = !process.env.NODE_ENV || process.env.NODE_ENV === 'development'
this.dir = (typeof options.rootDir === 'string' && options.rootDir ? options.rootDir : process.cwd())
// 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 }
})
// renderer used by Vue.js (via createBundleRenderer)
this.renderer = null
this.getContext = (this.options.getContext === 'function' ? this.options.getContext : getContext)
// For serving .nuxt/dist/ files
this.serveStatic = pify(serveStatic(resolve(this.dir, '.nuxt', 'dist')))
// Add this.build
this.build = build.bind(this)
// Launch build and set this.renderer
return co(this.build)
// .then((nuxt) => {
// if (typeof cb === 'function') cb(null, nuxt)
// })
// .catch((err) => {
// if (typeof cb === 'function') cb(err)
// })
}
render (req, res) {
if (!this.renderer) {
setTimeout(() => {
this.render(req, res)
}, 1000)
return
}
const self = this
const context = this.getContext(req, res)
co(function * () {
if (self.isDev) {
// Call webpack middlewares only in development
yield self.webpackDevMiddleware(req, res)
yield self.webpackHotMiddleware(req, res)
return
}
if (req.url.includes('/_nuxt/')) {
const url = req.url
req.url = req.url.replace('/_nuxt/', '/')
yield self.serveStatic(req, res)
req.url = url
}
})
.then(() => {
if (this.isDev && req.url.includes('/_nuxt/') && req.url.includes('.hot-update.json')) {
res.statusCode = 404
return res.end()
}
return this.renderRoute(req.url, context)
})
.then((html) => {
if (context.nuxt.error && context.nuxt.error.statusCode) {
res.statusCode = context.nuxt.error.statusCode
}
res.end(html, 'utf8')
})
.catch((err) => {
res.statusCode = 500
res.end(this.errorTemplate({ err }), 'utf8')
})
}
renderRoute (url, context = {}) {
debug(`Rendering url ${url}`)
// Add url and isSever to the context
context.url = url
context.isServer = true
// Call rendertoSting from the bundleRenderer and generate the HTML (will update the context as well)
const self = this
return co(function * () {
const html = yield self.renderToString(context)
const app = self.appTemplate({
isProd: self.isProd, // Use to add the extracted CSS <link> in production
APP: html,
context: context,
files: {
css: join('/_nuxt/', self.options.filenames.css),
vendor: join('/_nuxt/', self.options.filenames.vendor),
app: join('/_nuxt/', self.options.filenames.app)
}
})
return app
})
}
stop (callback) {
let promises = []
if (this.webpackDevMiddleware) {
const p = new Promise((resolve, reject) => {
this.webpackDevMiddleware.close(() => resolve())
})
promises.push(p)
}
if (this.webpackServerWatcher) {
const p = new Promise((resolve, reject) => {
this.webpackServerWatcher.close(() => resolve())
})
promises.push(p)
}
return co(function * () {
yield promises
})
.then(function () {
if (typeof callback === 'function') callback()
})
}
}
module.exports = Nuxt

19
lib/render.js Normal file
View File

@ -0,0 +1,19 @@
'use strict'
const debug = require('debug')('nuxt:render')
const { join } = require('path')
const { getRoute, waitFor } = require('./utils')
function * render (req, res, next) {
if (!this.renderer) {
yield waitFor(1000)
yield this.render(req, res, next)
return
}
debug(`Start rendering ${req.url}...`)
const route = getRoute(req.url)
const path = join('pages', (route === '/' ? 'index' : route)).replace('.vue', '')
debug(`Find ${path}.vue`)
}
module.exports = render

7
lib/renderRoute.js Normal file
View File

@ -0,0 +1,7 @@
'use strict'
const debug = require('debug')('nuxt:render-route')
module.exports = function (url, context) {
debug(`Rendering route ${url}`)
}

29
lib/utils.js Normal file
View File

@ -0,0 +1,29 @@
'use strict'
exports.encodeHtml = (str) => str.replace(/</g, '&lt;').replace(/>/g, '&gt;')
exports.getContext = function (req, res) {
return {
req: req,
res: res
}
}
exports.setAnsiColors = function (ansiHTML) {
ansiHTML.setColors({
reset: ['efefef', 'a6004c'],
darkgrey: '5a012b',
yellow: 'ffab07',
green: 'aeefba',
magenta: 'ff84bf',
blue: '3505a0',
cyan: '56eaec',
red: '4e053a'
})
}
exports.waitFor = function * (ms) {
return new Promise(function (resolve) {
setTimeout(resolve, (ms || 0))
})
}

19
lib/views/app.html Normal file
View File

@ -0,0 +1,19 @@
<% const {
title,
htmlAttrs,
bodyAttrs,
meta
} = context.meta.inject()
%><!DOCTYPE html data-vue-meta-server-rendered>
<html>
<head>
${title.toString()}
<% if (isProd) { %><link rel="stylesheet" href="<%= files.css %>"><% } %>
</head>
<body>
<%= APP %>
<script type="text/javascript" defer>window.__NUXT__=<%= serialize(context.nuxt) %></script>
<script src="<%= files.vendor %>" defer></script>
<script src="<%= files.app %>" defer></script>
</body>
</html>

11
lib/views/error.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue.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>
</body>
</html>

View File

@ -1,6 +1,6 @@
{
"name": "nuxt",
"version": "0.0.1",
"version": "0.1.0",
"description": "A minimalistic framework for server-rendered Vue.js applications (inspired by Next.js)",
"main": "./dist/lib/index.js",
"license": "MIT",
@ -9,76 +9,58 @@
"dist"
],
"bin": {
"next": "./dist/bin/nuxt"
"nuxt": "./dist/bin/nuxt"
},
"scripts": {
"build": "gulp",
"test": "standard && gulp test",
"lint": "standard",
"prepublish": "gulp release",
"start": "DEBUG=nuxt:* bin/nuxt",
"dev": "DEBUG=nuxt:* nodemon bin/nuxt",
"test": "npm run lint",
"lint": "eslint --ext .js,.vue bin lib pages test index.js --ignore-pattern lib/app",
"precommit": "npm run lint"
},
"ava": {
"babel": {
"presets": [
"es2015",
"react"
],
"plugins": [
"transform-async-to-generator",
"transform-object-rest-spread",
"transform-class-properties",
"transform-runtime"
]
}
},
"standard": {
"parser": "babel-eslint"
},
"dependencies": {
"babel-core": "6.17.0",
"babel-generator": "6.17.0",
"babel-loader": "6.2.5",
"babel-plugin-module-alias": "1.6.0",
"babel-plugin-transform-async-to-generator": "6.16.0",
"babel-plugin-transform-class-properties": "6.16.0",
"babel-plugin-transform-object-rest-spread": "6.16.0",
"babel-plugin-transform-runtime": "6.15.0",
"babel-preset-es2015": "6.16.0",
"babel-preset-react": "6.16.0",
"babel-runtime": "6.11.6",
"cross-spawn": "4.0.2",
"del": "2.2.2",
"glamor": "2.17.10",
"glob-promise": "1.0.6",
"htmlescape": "1.1.1",
"loader-utils": "0.2.16",
"minimist": "1.2.0",
"mz": "2.4.0",
"path-match": "1.2.4",
"react": "15.3.2",
"react-dom": "15.3.2",
"react-hot-loader": "3.0.0-beta.6",
"send": "0.14.1",
"strip-ansi": "3.0.1",
"url": "0.11.0",
"webpack": "1.13.2",
"webpack-dev-server": "1.16.2",
"write-file-webpack-plugin": "3.3.0"
"ansi-html": "^0.0.6",
"autoprefixer": "^6.5.1",
"babel-core": "^6.18.2",
"babel-loader": "^6.2.7",
"babel-preset-es2015": "^6.18.0",
"babel-preset-stage-2": "^6.18.0",
"co": "^4.6.0",
"cross-spawn": "^5.0.1",
"css-loader": "^0.25.0",
"debug": "^2.2.0",
"del": "^2.2.2",
"es6-object-assign": "^1.0.3",
"es6-promise": "^4.0.5",
"extract-text-webpack-plugin": "2.0.0-beta.4",
"file-loader": "^0.9.0",
"glob-promise": "^1.0.6",
"hash-sum": "^1.0.2",
"lodash": "^4.16.6",
"lru-cache": "^4.0.1",
"mkdirp-then": "^1.2.0",
"pify": "^2.3.0",
"serialize-javascript": "^1.3.0",
"serve-static": "^1.11.1",
"url-loader": "^0.5.7",
"vue": "^2.0.5",
"vue-loader": "^9.8.0",
"vue-meta": "^0.0.0",
"vue-router": "^2.0.1",
"vue-server-renderer": "^2.0.5",
"vuex": "^2.0.0",
"webpack": "2.1.0-beta.25",
"webpack-dev-middleware": "^1.8.4",
"webpack-hot-middleware": "^2.13.1"
},
"devDependencies": {
"babel-eslint": "7.0.0",
"babel-plugin-transform-remove-strict-mode": "0.0.2",
"benchmark": "2.1.1",
"gulp": "3.9.1",
"gulp-ava": "0.14.1",
"gulp-babel": "6.1.2",
"gulp-benchmark": "1.1.1",
"gulp-cached": "1.1.0",
"gulp-notify": "2.2.0",
"husky": "0.11.9",
"run-sequence": "1.2.2",
"standard": "^8.4.0",
"webpack-stream": "3.2.0"
"babel-eslint": "^7.1.0",
"benchmark": "^2.1.2",
"eslint": "^3.8.1",
"eslint-config-standard": "^6.2.1",
"eslint-plugin-html": "^1.5.5",
"eslint-plugin-promise": "^3.3.0",
"eslint-plugin-standard": "^2.0.1",
"nodemon": "^1.11.0"
}
}

61
pages/_error-debug.vue Executable file
View File

@ -0,0 +1,61 @@
<template>
<div class="error-page">
<div>
<h1 class="error-code">{{ error.statusCode }}</h1>
<div class="error-wrapper-message">
<h2 class="error-message">{{ error.message }}</h2>
</div>
<pre v-if="error.stack">{{ error.stack }}</pre>
<p v-if="error.statusCode === 404"><router-link class="error-link" to="/">Back to the home page</router-link></p>
</div>
</div>
</template>
<script>
export default {
props: ['error']
}
</script>
<style scoped>
.error-page {
color: #000;
background: #fff;
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
font-family: "SF UI Text", "Helvetica Neue", "Lucida Grande";
text-align: center;
padding-top: 20%;
}
.error-code {
display: inline-block;
font-size: 24px;
font-weight: 500;
vertical-align: top;
border-right: 1px solid rgba(0, 0, 0, 0.298039);
margin: 0px 20px 0px 0px;
padding: 10px 23px;
}
.error-wrapper-message {
display: inline-block;
text-align: left;
line-height: 49px;
height: 49px;
vertical-align: middle;
}
.error-message {
font-size: 14px;
font-weight: normal;
margin: 0px;
padding: 0px;
}
.error-link {
color: #42b983;
font-weight: normal;
text-decoration: none;
font-size: 14px;
}
</style>

60
pages/_error.vue Normal file
View File

@ -0,0 +1,60 @@
<template>
<div class="error-page">
<div>
<h1 class="error-code">{{ error.statusCode }}</h1>
<div class="error-wrapper-message">
<h2 class="error-message">{{ error.message }}</h2>
</div>
<p v-if="error.statusCode === 404"><router-link class="error-link" to="/">Back to the home page</router-link></p>
</div>
</div>
</template>
<script>
export default {
props: ['error']
}
</script>
<style scoped>
.error-page {
color: #000;
background: #fff;
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
font-family: "SF UI Text", "Helvetica Neue", "Lucida Grande";
text-align: center;
padding-top: 20%;
}
.error-code {
display: inline-block;
font-size: 24px;
font-weight: 500;
vertical-align: top;
border-right: 1px solid rgba(0, 0, 0, 0.298039);
margin: 0px 20px 0px 0px;
padding: 10px 23px;
}
.error-wrapper-message {
display: inline-block;
text-align: left;
line-height: 49px;
height: 49px;
vertical-align: middle;
}
.error-message {
font-size: 14px;
font-weight: normal;
margin: 0px;
padding: 0px;
}
.error-link {
color: #42b983;
font-weight: normal;
text-decoration: none;
font-size: 14px;
}
</style>

13
test/fixtures/basic/pages/async-props.vue vendored Executable file
View File

@ -0,0 +1,13 @@
<template>
<p>{{ name }}</p>
</template>
<script>
export default {
data () {
return new Promise((resolve) => {
setTimeout(() => resolve({ name: 'Kobe Bryant' }), 10)
})
}
}
</script>

9
test/fixtures/basic/pages/css.vue vendored Executable file
View File

@ -0,0 +1,9 @@
<template>
<div class="red">This is red</div>
</template>
<style>
.red {
color: red;
}
</style>

8
test/fixtures/basic/pages/head.vue vendored Executable file
View File

@ -0,0 +1,8 @@
<template>
<div>
<Head>
<meta content='my meta' />
</Head>
<h1>I can haz meta tags</h1>
</div>
</template>

16
test/fixtures/basic/pages/stateful.vue vendored Executable file
View File

@ -0,0 +1,16 @@
<template>
<div>
<p>The answer is {{ answer }}</p>
</div>
</template>
<script>
export default {
data () {
return { answer: null }
},
beforeMount () {
this.answer = 42
}
}
</script>

3
test/fixtures/basic/pages/stateless.vue vendored Executable file
View File

@ -0,0 +1,3 @@
<template>
<h1>My component!</h1>
</template>

39
test/index.js Executable file
View File

@ -0,0 +1,39 @@
import test from 'ava'
import { join } from 'path'
import build from '../server/build'
import { render as _render } from '../server/render'
const dir = join(__dirname, 'fixtures', 'basic')
test.before(() => build(dir))
test(async t => {
const html = await render('/stateless')
t.true(html.includes('<h1>My component!</h1>'))
})
test(async t => {
const html = await render('/css')
t.true(html.includes('.red{color:red;}'))
t.true(html.includes('<div class="red">This is red</div>'))
})
test(async t => {
const html = await render('/stateful')
t.true(html.includes('<div><p>The answer is 42</p></div>'))
})
test(async t => {
const html = await (render('/head'))
t.true(html.includes('<meta content="my meta" class="next-head"/>'))
t.true(html.includes('<div><h1>I can haz meta tags</h1></div>'))
})
test(async t => {
const html = await render('/async-props')
t.true(html.includes('<p>Kobe Bryant</p>'))
})
function render (url, ctx) {
return _render(url, ctx, { dir, staticMarkup: true })
}